diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml
index 07deb74f..ceccc785 100644
--- a/.github/workflows/go.yaml
+++ b/.github/workflows/go.yaml
@@ -7,13 +7,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- - name: Set up Go
- uses: actions/setup-go@v2
+ - name: Checkout
+ uses: actions/checkout@v3
with:
- go-version: 1.16
+ fetch-depth: 0
+
+ - name: Get Go version
+ run: echo "GO_VERSION=$(grep '^go ' go.mod | cut -d ' ' -f 2)" >> $GITHUB_ENV && cat $GITHUB_ENV
- - name: Check out code into the Go module directory
- uses: actions/checkout@v2
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: ${{ env.GO_VERSION }}
- name: Test
run: go test -v ./...
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index e2ba986c..32194f36 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -8,19 +8,25 @@ on:
jobs:
goreleaser:
+ name: Goreleaser
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
- - name: Checkout submodules
- run: git submodule update --init --recursive
+ - name: Get Go version
+ run: echo "GO_VERSION=$(grep '^go ' go.mod | cut -d ' ' -f 2)" >> $GITHUB_ENV && cat $GITHUB_ENV
- name: Set up Go
- uses: actions/setup-go@v2
+ uses: actions/setup-go@v3
with:
- go-version: 1.16
+ go-version: ${{ env.GO_VERSION }}
+
+ - name: Checkout submodules
+ run: git submodule update --init --recursive
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
@@ -28,4 +34,4 @@ jobs:
version: latest
args: release
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/sh.yaml b/.github/workflows/sh.yaml
index fdd34f56..0dcfb475 100644
--- a/.github/workflows/sh.yaml
+++ b/.github/workflows/sh.yaml
@@ -7,11 +7,13 @@ jobs:
runs-on: ubuntu-latest
steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
- name: Set up
run: sudo apt-get install -y shellcheck zsh
- - name: Check out code into the Go module directory
- uses: actions/checkout@v2
-
- name: Test
run: scripts/test.sh
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 11c048dd..b364a830 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -76,55 +76,46 @@ builds:
- arm
- arm64
-
- id: "evaluate"
- main: ./cmd/evaluate
- binary: bin/resh-evaluate
- goarch:
- - 386
- - amd64
- - arm
- - arm64
- -
- id: "event"
- main: ./cmd/event
- binary: bin/resh-event
+ id: "postcollect"
+ main: ./cmd/postcollect
+ binary: bin/resh-postcollect
goarch:
- 386
- amd64
- arm
- arm64
-
- id: "inspect"
- main: ./cmd/inspect
- binary: bin/resh-inspect
+ id: "session-init"
+ main: ./cmd/session-init
+ binary: bin/resh-session-init
goarch:
- 386
- amd64
- arm
- arm64
-
- id: "postcollect"
- main: ./cmd/postcollect
- binary: bin/resh-postcollect
- goarch:
+ id: "install-utils"
+ main: ./cmd/install-utils
+ binary: bin/resh-install-utils
+ goarch:
- 386
- amd64
- arm
- arm64
-
- id: "sanitize"
- main: ./cmd/sanitize
- binary: bin/resh-sanitize
- goarch:
+ id: "generate-uuid"
+ main: ./cmd/generate-uuid
+ binary: bin/resh-generate-uuid
+ goarch:
- 386
- amd64
- arm
- arm64
-
- id: "session-init"
- main: ./cmd/session-init
- binary: bin/resh-session-init
- goarch:
+ id: "get-epochtime"
+ main: ./cmd/get-epochtime
+ binary: bin/resh-get-epochtime
+ goarch:
- 386
- amd64
- arm
diff --git a/Makefile b/Makefile
index 02bb7e04..a67c36d3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,16 +1,25 @@
SHELL=/bin/bash
LATEST_TAG=$(shell git describe --tags)
-REVISION=$(shell [ -z "$(git status --untracked-files=no --porcelain)" ] && git rev-parse --short=12 HEAD || echo "no_revision")
-VERSION="${LATEST_TAG}-DEV"
-GOFLAGS=-ldflags "-X main.version=${VERSION} -X main.commit=${REVISION}"
+VERSION:="${LATEST_TAG}-$(shell date +%s)"
+COMMIT:=$(shell [ -z "$(git status --untracked-files=no --porcelain)" ] && git rev-parse --short=12 HEAD || echo "no_commit")
+GOFLAGS=-ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.development=true"
+build: submodules bin/resh-session-init bin/resh-collect bin/resh-postcollect\
+ bin/resh-daemon bin/resh-control bin/resh-config bin/resh-cli\
+ bin/resh-install-utils bin/resh-generate-uuid bin/resh-get-epochtime
-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 bin/resh-cli
-
+# We disable jobserver for the actual installation because we want it to run serially
+# Make waits to the daemon process we launch during install and hangs
install: build
scripts/install.sh
+# Rebuild binaries and install
+# Very useful to ensure that all binaries get new VERSION variable which is used for shell config reloading
+clean_install:
+ make clean
+ make build
+ make install
+
test:
go test -v ./...
go vet ./...
@@ -21,19 +30,19 @@ rebuild:
make build
clean:
- rm -f bin/resh-*
+ rm -f -- bin/*
uninstall:
# Uninstalling ...
- -rm -rf ~/.resh/
+ -rm -rf -- ~/.resh/
-bin/resh-%: cmd/%/*.go pkg/*/*.go cmd/control/cmd/*.go cmd/control/status/status.go
+go_files = $(shell find -name '*.go')
+bin/resh-%: $(go_files)
grep $@ .goreleaser.yml -q # all build targets need to be included in .goreleaser.yml
go build ${GOFLAGS} -o $@ cmd/$*/*.go
.PHONY: submodules build install rebuild uninstall clean test
-
submodules: | submodules/bash-preexec/bash-preexec.sh submodules/bash-zsh-compat-widgets/bindfunc.sh
@# sets submodule.recurse to true if unset
@# sets status.submoduleSummary to true if unset
diff --git a/README.md b/README.md
index b2e12a73..ee7cc973 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,16 @@
-![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/curusarn/resh?sort=semver)
-![Go test](https://github.com/curusarn/resh/actions/workflows/go.yaml/badge.svg)
-![Shell test](https://github.com/curusarn/resh/actions/workflows/sh.yaml/badge.svg)
+[![Latest version](https://img.shields.io/github/v/tag/curusarn/resh?sort=semver)](https://github.com/curusarn/resh/releases)
+[![Go Report Card](https://goreportcard.com/badge/github.com/curusarn/resh)](https://goreportcard.com/report/github.com/curusarn/resh)
+[![Go test](https://github.com/curusarn/resh/actions/workflows/go.yaml/badge.svg)](https://github.com/curusarn/resh/actions/workflows/go.yaml)
+[![Shell test](https://github.com/curusarn/resh/actions/workflows/sh.yaml/badge.svg)](https://github.com/curusarn/resh/actions/workflows/sh.yaml)
-# Rich Enhanced Shell History
+# RESH
+
+Context-based replacement for `zsh` and `bash` shell history.
+
+**Full-text search your shell history.**
+Relevant results are displayed first based on current directory, git repo, and exit status.
-Context-based replacement/enhancement for zsh and bash shell history
@@ -17,114 +22,36 @@ Context-based replacement/enhancement for zsh and bash shell history
-**Search your history by commands and get relevant results based on current directory, git repo, exit status, and host.**
-
-## Installation
-
-### Prerequisites
-
-Standard stuff: `bash(4.3+)`, `curl`, `tar`, ...
-
-Bash completions will only work if you have `bash-completion` installed
+## Install
-MacOS: `coreutils` (`brew install coreutils`)
-
-### Simplest installation
-
-Run this command.
+Install RESH with one command:
```sh
-curl -fsSL https://raw.githubusercontent.com/curusarn/resh/master/scripts/rawinstall.sh | bash
+curl -fsSL https://raw.githubusercontent.com/curusarn/resh/master/scripts/rawinstall.sh | sh
```
-### Simple installation
-
-Run
-
-```shell
-git clone https://github.com/curusarn/resh.git
-cd resh && scripts/rawinstall.sh
-```
-
-### Update
-
-Check for updates and update
-
-```sh
-reshctl update
-```
-
-## Roadmap
-
-[Overview of the features of the project](./roadmap.md)
-
-## RESH SEARCH application
-
-This is the most important part of this project.
-
-RESH SEARCH app searches your history by commands. It uses host, directories, git remote, and exit status to show you relevant results first.
+ℹ️ You will need to have `curl` and `tar` installed.
-All this context is not in the regular shell history. RESH records shell history with context to use it when searching.
+More options on [Installation page ⇗](./installation.md)
-At first, the search application will look something like this. Some history with context and most of it without. As you can see, you can still search the history just fine.
+## Search your history
-![resh search app](img/screen-resh-cli-v2-7-init.png)
+Press Ctrl + R to search:
-Eventually most of your history will have context and RESH SEARCH app will get more useful.
-
-![resh search app](img/screen-resh-cli-v2-7.png)
-
-Without a query, RESH SEARCH app shows you the latest history based on the current context (host, directory, git).
-
-![resh search app](img/screen-resh-cli-v2-7-no-query.png)
-
-RESH SEARCH app replaces the standard reverse search - launch it using Ctrl+R.
-
-Enable/disable the Ctrl+R keybinding:
-
-```sh
-reshctl enable ctrl_r_binding
-reshctl disable ctrl_r_binding
-```
+
### In-app key bindings
-- Type to search/filter
-- Up/Down or Ctrl+P/Ctrl+N to select results
-- Right to paste selected command onto the command line so you can edit it before execution
-- Enter to execute
-- Ctrl+C/Ctrl+D to quit
-- Ctrl+G to abort and paste the current query onto the command line
-- Ctrl+R to switch between RAW and NORMAL mode
-
-### View the recorded history
-
-Resh history is saved to `~/.resh_history.json`
-
-Each line is a JSON that represents one executed command line.
-
-This is how I view it `tail -f ~/.resh_history.json | jq` or `jq < ~/.resh_history.json`.
-
-You can install `jq` using your favourite package manager or you can use other JSON parser to view the history.
-
-![screenshot](img/screen.png)
-
-*Recorded metadata will be reduced to only include useful information in the future.*
-
-## Known issues
-
-### Q: I use bash on macOS and resh doesn't work
-
-**A:** You have to add `[ -f ~/.bashrc ] && . ~/.bashrc` to your `~/.bash_profile`.
-
-**Long Answer:** Under macOS bash shell only loads `~/.bash_profile` because every shell runs as login shell. I will definitely work around this in the future but since this doesn't affect many people I decided to not solve this issue at the moment.
-
-## Issues and ideas
-
-Please do create issues if you encounter any problems or if you have a suggestions: https://github.com/curusarn/resh/issues
+- Type to search
+- Up / Down or Ctrl + P / Ctrl + N to select results
+- Enter to execute selected command
+- Right to paste selected command onto the command line so you can edit it before execution
+- Ctrl + C or Ctrl + D to quit
+- Ctrl + G to abort and paste the current query onto the command line
+- Ctrl + R to search without context (toggle)
-## Uninstallation
+## Issues & ideas
-You can uninstall this project at any time by running `rm -rf ~/.resh/`.
+Find help on [Troubleshooting page ⇗](./troubleshooting.md)
-You won't lose any recorded history by removing `~/.resh` directory because history is saved in `~/.resh_history.json`.
+Problem persists? [Create an issue ⇗](https://github.com/curusarn/resh/issues)
diff --git a/cmd/cli/main.go b/cmd/cli/main.go
index a58ff107..222f738d 100644
--- a/cmd/cli/main.go
+++ b/cmd/cli/main.go
@@ -4,10 +4,8 @@ import (
"bytes"
"encoding/json"
"errors"
- "flag"
"fmt"
- "io/ioutil"
- "log"
+ "io"
"net/http"
"os"
"sort"
@@ -15,84 +13,81 @@ import (
"sync"
"time"
- "github.com/BurntSushi/toml"
"github.com/awesome-gocui/gocui"
- "github.com/curusarn/resh/pkg/cfg"
- "github.com/curusarn/resh/pkg/msg"
- "github.com/curusarn/resh/pkg/records"
- "github.com/curusarn/resh/pkg/searchapp"
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/datadir"
+ "github.com/curusarn/resh/internal/device"
+ "github.com/curusarn/resh/internal/logger"
+ "github.com/curusarn/resh/internal/msg"
+ "github.com/curusarn/resh/internal/opt"
+ "github.com/curusarn/resh/internal/output"
+ "github.com/curusarn/resh/internal/recordint"
+ "github.com/curusarn/resh/internal/searchapp"
+ "github.com/spf13/pflag"
+ "go.uber.org/zap"
- "os/user"
- "path/filepath"
"strconv"
)
-// version from git set during build
+// info passed during build
var version string
-
-// commit from git set during build
var commit string
+var development string
// special constant recognized by RESH wrappers
const exitCodeExecute = 111
-var debug bool
-
func main() {
- output, exitCode := runReshCli()
+ config, errCfg := cfg.New()
+ logger, err := logger.New("search-app", config.LogLevel, development)
+ if err != nil {
+ fmt.Printf("Error while creating logger: %v", err)
+ }
+ defer logger.Sync() // flushes buffer, if any
+ if errCfg != nil {
+ logger.Error("Error while getting configuration", zap.Error(errCfg))
+ }
+ out := output.New(logger, "resh-search-app ERROR")
+
+ output, exitCode := runReshCli(out, config)
fmt.Print(output)
os.Exit(exitCode)
}
-func runReshCli() (string, int) {
- usr, _ := user.Current()
- dir := usr.HomeDir
- configPath := filepath.Join(dir, "/.config/resh.toml")
- logPath := filepath.Join(dir, ".resh/cli.log")
-
- f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
- if err != nil {
- log.Fatal("Error opening file:", err)
- }
- defer f.Close()
+func runReshCli(out *output.Output, config cfg.Config) (string, int) {
+ args := opt.HandleVersionOpts(out, os.Args, version, commit)
- log.SetOutput(f)
+ const missing = ""
+ flags := pflag.NewFlagSet("", pflag.ExitOnError)
+ sessionID := flags.String("session-id", missing, "Resh generated session ID")
+ pwd := flags.String("pwd", missing, "$PWD - present working directory")
+ gitOriginRemote := flags.String("git-remote", missing, "> git remote get-url origin")
+ query := flags.String("query", "", "Search query")
+ flags.Parse(args)
- var config cfg.Config
- if _, err := toml.DecodeFile(configPath, &config); err != nil {
- log.Fatal("Error reading config:", err)
+ // TODO: These errors should tell the user that they should not be running the command directly
+ errMsg := "Failed to get required command-line arguments"
+ if *sessionID == missing {
+ out.FatalE(errMsg, errors.New("missing required option --session-id"))
}
- if config.Debug {
- debug = true
- log.SetFlags(log.LstdFlags | log.Lmicroseconds)
- log.Println("DEBUG is ON")
+ if *pwd == missing {
+ out.FatalE(errMsg, errors.New("missing required option --pwd"))
}
-
- sessionID := flag.String("sessionID", "", "resh generated session id")
- host := flag.String("host", "", "host")
- pwd := flag.String("pwd", "", "present working directory")
- gitOriginRemote := flag.String("gitOriginRemote", "DEFAULT", "git origin remote")
- query := flag.String("query", "", "search query")
- testHistory := flag.String("test-history", "", "load history from a file instead from the daemon (for testing purposes only!)")
- testHistoryLines := flag.Int("test-lines", 0, "the number of lines to load from a file passed with --test-history (for testing purposes only!)")
- flag.Parse()
-
- if *sessionID == "" {
- log.Println("Error: you need to specify sessionId")
- }
- if *host == "" {
- log.Println("Error: you need to specify HOST")
+ if *gitOriginRemote == missing {
+ out.FatalE(errMsg, errors.New("missing required option --git-origin-remote"))
}
- if *pwd == "" {
- log.Println("Error: you need to specify PWD")
+ dataDir, err := datadir.GetPath()
+ if err != nil {
+ out.FatalE("Could not get user data directory", err)
}
- if *gitOriginRemote == "DEFAULT" {
- log.Println("Error: you need to specify gitOriginRemote")
+ deviceName, err := device.GetName(dataDir)
+ if err != nil {
+ out.FatalE("Could not get device name", err)
}
g, err := gocui.NewGui(gocui.OutputNormal, false)
if err != nil {
- log.Panicln(err)
+ out.FatalE("Failed to launch TUI", err)
}
defer g.Close()
@@ -102,80 +97,79 @@ func runReshCli() (string, int) {
g.Highlight = true
var resp msg.CliResponse
- if *testHistory == "" {
- mess := msg.CliMsg{
- SessionID: *sessionID,
- PWD: *pwd,
- }
- resp = SendCliMsg(mess, strconv.Itoa(config.Port))
- } else {
- resp = searchapp.LoadHistoryFromFile(*testHistory, *testHistoryLines)
+ mess := msg.CliMsg{
+ SessionID: *sessionID,
+ PWD: *pwd,
}
+ resp = SendCliMsg(out, mess, strconv.Itoa(config.Port))
st := state{
// lock sync.Mutex
- cliRecords: resp.CliRecords,
+ cliRecords: resp.Records,
initialQuery: *query,
}
+ // TODO: Use device ID
layout := manager{
+ out: out,
+ config: config,
sessionID: *sessionID,
- host: *host,
+ host: deviceName,
pwd: *pwd,
- gitOriginRemote: records.NormalizeGitRemote(*gitOriginRemote),
- config: config,
+ gitOriginRemote: *gitOriginRemote,
s: &st,
}
g.SetManager(layout)
+ errMsg = "Failed to set keybindings"
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, layout.Next); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, layout.Next); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyCtrlN, gocui.ModNone, layout.Next); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, layout.Prev); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyCtrlP, gocui.ModNone, layout.Prev); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyArrowRight, gocui.ModNone, layout.SelectPaste); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, layout.SelectExecute); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyCtrlG, gocui.ModNone, layout.AbortPaste); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyCtrlD, gocui.ModNone, quit); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyCtrlR, gocui.ModNone, layout.SwitchModes); err != nil {
- log.Panicln(err)
+ out.FatalE(errMsg, err)
}
layout.UpdateData(*query)
layout.UpdateRawData(*query)
err = g.MainLoop()
if err != nil && !errors.Is(err, gocui.ErrQuit) {
- log.Panicln(err)
+ out.FatalE("Main application loop finished with error", err)
}
return layout.s.output, layout.s.exitCode
}
type state struct {
lock sync.Mutex
- cliRecords []records.CliRecord
+ cliRecords []recordint.SearchApp
data []searchapp.Item
rawData []searchapp.RawItem
highlightedItem int
@@ -190,11 +184,13 @@ type state struct {
}
type manager struct {
+ out *output.Output
+ config cfg.Config
+
sessionID string
host string
pwd string
gitOriginRemote string
- config cfg.Config
s *state
}
@@ -204,13 +200,13 @@ func (m manager) SelectExecute(g *gocui.Gui, v *gocui.View) error {
defer m.s.lock.Unlock()
if m.s.rawMode {
if m.s.highlightedItem < len(m.s.rawData) {
- m.s.output = m.s.rawData[m.s.highlightedItem].CmdLine
+ m.s.output = m.s.rawData[m.s.highlightedItem].CmdLineOut
m.s.exitCode = exitCodeExecute
return gocui.ErrQuit
}
} else {
if m.s.highlightedItem < len(m.s.data) {
- m.s.output = m.s.data[m.s.highlightedItem].CmdLine
+ m.s.output = m.s.data[m.s.highlightedItem].CmdLineOut
m.s.exitCode = exitCodeExecute
return gocui.ErrQuit
}
@@ -223,13 +219,13 @@ func (m manager) SelectPaste(g *gocui.Gui, v *gocui.View) error {
defer m.s.lock.Unlock()
if m.s.rawMode {
if m.s.highlightedItem < len(m.s.rawData) {
- m.s.output = m.s.rawData[m.s.highlightedItem].CmdLine
+ m.s.output = m.s.rawData[m.s.highlightedItem].CmdLineOut
m.s.exitCode = 0 // success
return gocui.ErrQuit
}
} else {
if m.s.highlightedItem < len(m.s.data) {
- m.s.output = m.s.data[m.s.highlightedItem].CmdLine
+ m.s.output = m.s.data[m.s.highlightedItem].CmdLineOut
m.s.exitCode = 0 // success
return gocui.ErrQuit
}
@@ -248,18 +244,13 @@ func (m manager) AbortPaste(g *gocui.Gui, v *gocui.View) error {
return nil
}
-type dedupRecord struct {
- dataIndex int
- score float32
-}
-
func (m manager) UpdateData(input string) {
- if debug {
- log.Println("EDIT start")
- log.Println("len(fullRecords) =", len(m.s.cliRecords))
- log.Println("len(data) =", len(m.s.data))
- }
- query := searchapp.NewQueryFromString(input, m.host, m.pwd, m.gitOriginRemote, m.config.Debug)
+ sugar := m.out.Logger.Sugar()
+ sugar.Debugw("Starting data update ...",
+ "recordCount", len(m.s.cliRecords),
+ "itemCount", len(m.s.data),
+ )
+ query := searchapp.NewQueryFromString(sugar, input, m.host, m.pwd, m.gitOriginRemote, m.config.Debug)
var data []searchapp.Item
itemSet := make(map[string]int)
m.s.lock.Lock()
@@ -268,7 +259,7 @@ func (m manager) UpdateData(input string) {
itm, err := searchapp.NewItemFromRecordForQuery(rec, query, m.config.Debug)
if err != nil {
// records didn't match the query
- // log.Println(" * continue (no match)", rec.Pwd)
+ // sugar.Println(" * continue (no match)", rec.Pwd)
continue
}
if idx, ok := itemSet[itm.Key]; ok {
@@ -285,9 +276,9 @@ func (m manager) UpdateData(input string) {
itemSet[itm.Key] = len(data)
data = append(data, itm)
}
- if debug {
- log.Println("len(tmpdata) =", len(data))
- }
+ sugar.Debugw("Got new items from records for query, sorting items ...",
+ "itemCount", len(data),
+ )
sort.SliceStable(data, func(p, q int) bool {
return data[p].Score > data[q].Score
})
@@ -299,19 +290,18 @@ func (m manager) UpdateData(input string) {
m.s.data = append(m.s.data, itm)
}
m.s.highlightedItem = 0
- if debug {
- log.Println("len(fullRecords) =", len(m.s.cliRecords))
- log.Println("len(data) =", len(m.s.data))
- log.Println("EDIT end")
- }
+ sugar.Debugw("Done with data update",
+ "recordCount", len(m.s.cliRecords),
+ "itemCount", len(m.s.data),
+ )
}
func (m manager) UpdateRawData(input string) {
- if m.config.Debug {
- log.Println("EDIT start")
- log.Println("len(fullRecords) =", len(m.s.cliRecords))
- log.Println("len(data) =", len(m.s.data))
- }
+ sugar := m.out.Logger.Sugar()
+ sugar.Debugw("Starting RAW data update ...",
+ "recordCount", len(m.s.cliRecords),
+ "itemCount", len(m.s.data),
+ )
query := searchapp.GetRawTermsFromString(input, m.config.Debug)
var data []searchapp.RawItem
itemSet := make(map[string]bool)
@@ -321,20 +311,20 @@ func (m manager) UpdateRawData(input string) {
itm, err := searchapp.NewRawItemFromRecordForQuery(rec, query, m.config.Debug)
if err != nil {
// records didn't match the query
- // log.Println(" * continue (no match)", rec.Pwd)
+ // sugar.Println(" * continue (no match)", rec.Pwd)
continue
}
if itemSet[itm.Key] {
- // log.Println(" * continue (already present)", itm.key(), itm.pwd)
+ // sugar.Println(" * continue (already present)", itm.key(), itm.pwd)
continue
}
itemSet[itm.Key] = true
data = append(data, itm)
- // log.Println("DATA =", itm.display)
- }
- if debug {
- log.Println("len(tmpdata) =", len(data))
+ // sugar.Println("DATA =", itm.display)
}
+ sugar.Debugw("Got new RAW items from records for query, sorting items ...",
+ "itemCount", len(data),
+ )
sort.SliceStable(data, func(p, q int) bool {
return data[p].Score > data[q].Score
})
@@ -346,11 +336,10 @@ func (m manager) UpdateRawData(input string) {
m.s.rawData = append(m.s.rawData, itm)
}
m.s.highlightedItem = 0
- if debug {
- log.Println("len(fullRecords) =", len(m.s.cliRecords))
- log.Println("len(data) =", len(m.s.data))
- log.Println("EDIT end")
- }
+ sugar.Debugw("Done with RAW data update",
+ "recordCount", len(m.s.cliRecords),
+ "itemCount", len(m.s.data),
+ )
}
func (m manager) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
gocui.DefaultEditor.Edit(v, key, ch, mod)
@@ -398,7 +387,7 @@ func (m manager) Layout(g *gocui.Gui) error {
v, err := g.SetView("input", 0, 0, maxX-1, 2, b)
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
- log.Panicln(err.Error())
+ m.out.FatalE("Failed to set view 'input'", err)
}
v.Editable = true
@@ -421,7 +410,7 @@ func (m manager) Layout(g *gocui.Gui) error {
v, err = g.SetView("body", 0, 2, maxX-1, maxY, b)
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
- log.Panicln(err.Error())
+ m.out.FatalE("Failed to set view 'body'", err)
}
v.Frame = false
v.Autoscroll = false
@@ -438,13 +427,14 @@ func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
-const smallTerminalTresholdWidth = 110
+const smallTerminalThresholdWidth = 110
func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
+ sugar := m.out.Logger.Sugar()
maxX, maxY := g.Size()
compactRenderingMode := false
- if maxX < smallTerminalTresholdWidth {
+ if maxX < smallTerminalThresholdWidth {
compactRenderingMode = true
}
@@ -459,7 +449,7 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
if i == maxY {
break
}
- ic := itm.DrawItemColumns(compactRenderingMode, debug)
+ ic := itm.DrawItemColumns(compactRenderingMode, m.config.Debug)
data = append(data, ic)
if i > maxPossibleMainViewHeight {
// do not stretch columns because of results that will end up outside of the page
@@ -505,7 +495,7 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
// header
// header := getHeader()
// error is expected for header
- dispStr, _, _ := header.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, true, true, debug)
+ dispStr, _, _ := header.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, true, true, m.config.Debug)
dispStr = searchapp.DoHighlightHeader(dispStr, maxX*2)
v.WriteString(dispStr + "\n")
@@ -513,33 +503,24 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
for index < len(data) {
itm := data[index]
if index >= mainViewHeight {
- if debug {
- log.Printf("Finished drawing page. mainViewHeight: %v, predictedMax: %v\n",
- mainViewHeight, maxPossibleMainViewHeight)
- }
+ sugar.Debugw("Reached bottom of the page while producing lines",
+ "mainViewHeight", mainViewHeight,
+ "predictedMaxViewHeight", maxPossibleMainViewHeight,
+ )
// page is full
break
}
- displayStr, _, err := itm.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, false, true, debug)
+ displayStr, _, err := itm.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, false, true, m.config.Debug)
if err != nil {
- log.Printf("produceLine error: %v\n", err)
+ sugar.Error("Error while drawing item", zap.Error(err))
}
if m.s.highlightedItem == index {
// maxX * 2 because there are escape sequences that make it hard to tell the real string length
displayStr = searchapp.DoHighlightString(displayStr, maxX*3)
- if debug {
- log.Println("### HightlightedItem string :", displayStr)
- }
- } else if debug {
- log.Println(displayStr)
}
if strings.Contains(displayStr, "\n") {
- log.Println("display string contained \\n")
displayStr = strings.ReplaceAll(displayStr, "\n", "#")
- if debug {
- log.Println("display string contained \\n")
- }
}
v.WriteString(displayStr + "\n")
index++
@@ -553,59 +534,46 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
v.WriteString(line)
}
v.WriteString(helpLine)
- if debug {
- log.Println("len(data) =", len(m.s.data))
- log.Println("highlightedItem =", m.s.highlightedItem)
- }
+ sugar.Debugw("Done drawing page",
+ "itemCount", len(m.s.data),
+ "highlightedItemIndex", m.s.highlightedItem,
+ )
return nil
}
func (m manager) rawMode(g *gocui.Gui, v *gocui.View) error {
+ sugar := m.out.Logger.Sugar()
maxX, maxY := g.Size()
topBoxSize := 3
m.s.displayedItemsCount = maxY - topBoxSize
for i, itm := range m.s.rawData {
if i == maxY {
- if debug {
- log.Println(maxY)
- }
break
}
displayStr := itm.CmdLineWithColor
if m.s.highlightedItem == i {
- // use actual min requried length instead of 420 constant
+ // Use actual min required length instead of 420 constant
displayStr = searchapp.DoHighlightString(displayStr, maxX*2)
- if debug {
- log.Println("### HightlightedItem string :", displayStr)
- }
- } else if debug {
- log.Println(displayStr)
}
if strings.Contains(displayStr, "\n") {
- log.Println("display string contained \\n")
displayStr = strings.ReplaceAll(displayStr, "\n", "#")
- if debug {
- log.Println("display string contained \\n")
- }
}
v.WriteString(displayStr + "\n")
- // if m.s.highlightedItem == i {
- // v.SetHighlight(m.s.highlightedItem, true)
- // }
- }
- if debug {
- log.Println("len(data) =", len(m.s.data))
- log.Println("highlightedItem =", m.s.highlightedItem)
}
+ sugar.Debugw("Done drawing page in RAW mode",
+ "itemCount", len(m.s.data),
+ "highlightedItemIndex", m.s.highlightedItem,
+ )
return nil
}
// SendCliMsg to daemon
-func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse {
+func SendCliMsg(out *output.Output, m msg.CliMsg, port string) msg.CliResponse {
+ sugar := out.Logger.Sugar()
recJSON, err := json.Marshal(m)
if err != nil {
- log.Fatalf("Failed to marshal message: %v\n", err)
+ out.FatalE("Failed to marshal message", err)
}
req, err := http.NewRequest(
@@ -613,7 +581,7 @@ func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse {
"http://localhost:"+port+"/dump",
bytes.NewBuffer(recJSON))
if err != nil {
- log.Fatalf("Failed to build request: %v\n", err)
+ out.FatalE("Failed to build request", err)
}
req.Header.Set("Content-Type", "application/json")
@@ -622,22 +590,22 @@ func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse {
}
resp, err := client.Do(req)
if err != nil {
- log.Fatal("resh-daemon is not running - try restarting this terminal")
+ out.FatalDaemonNotRunning(err)
}
defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
+ body, err := io.ReadAll(resp.Body)
if err != nil {
- log.Fatalf("Read response error: %v\n", err)
+ out.FatalE("Failed read response", err)
}
- // log.Println(string(body))
+ // sugar.Println(string(body))
response := msg.CliResponse{}
err = json.Unmarshal(body, &response)
if err != nil {
- log.Fatalf("Unmarshal resp error: %v\n", err)
- }
- if debug {
- log.Printf("Recieved %d records from daemon\n", len(response.CliRecords))
+ out.FatalE("Failed decode response", err)
}
+ sugar.Debugw("Received records from daemon",
+ "recordCount", len(response.Records),
+ )
return response
}
diff --git a/cmd/collect/main.go b/cmd/collect/main.go
index d2d2815f..cd8bb2f3 100644
--- a/cmd/collect/main.go
+++ b/cmd/collect/main.go
@@ -1,255 +1,91 @@
package main
import (
- "flag"
"fmt"
- "log"
"os"
- "github.com/BurntSushi/toml"
- "github.com/curusarn/resh/pkg/cfg"
- "github.com/curusarn/resh/pkg/collect"
- "github.com/curusarn/resh/pkg/records"
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/collect"
+ "github.com/curusarn/resh/internal/logger"
+ "github.com/curusarn/resh/internal/opt"
+ "github.com/curusarn/resh/internal/output"
+ "github.com/curusarn/resh/internal/recordint"
+ "github.com/curusarn/resh/record"
+ "github.com/spf13/pflag"
+ "go.uber.org/zap"
- // "os/exec"
- "os/user"
"path/filepath"
"strconv"
)
-// version tag from git set during build
+// info passed during build
var version string
-
-// Commit hash from git set during build
var commit string
+var development string
func main() {
- usr, _ := user.Current()
- dir := usr.HomeDir
- configPath := filepath.Join(dir, "/.config/resh.toml")
- reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid")
-
- machineIDPath := "/etc/machine-id"
-
- var config cfg.Config
- 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")
- recordID := flag.String("recordId", "", "resh generated record id")
-
- // recall metadata
- recallActions := flag.String("recall-actions", "", "recall actions that took place before executing the command")
- recallStrategy := flag.String("recall-strategy", "", "recall strategy used during recall actions")
- recallLastCmdLine := flag.String("recall-last-cmdline", "", "last recalled cmdline")
-
- // posix variables
- cols := flag.String("cols", "-1", "$COLUMNS")
- lines := flag.String("lines", "-1", "$LINES")
- home := flag.String("home", "", "$HOME")
- lang := flag.String("lang", "", "$LANG")
- lcAll := flag.String("lcAll", "", "$LC_ALL")
- login := flag.String("login", "", "$LOGIN")
- // path := flag.String("path", "", "$PATH")
- pwd := flag.String("pwd", "", "$PWD - present working directory")
- shellEnv := flag.String("shellEnv", "", "$SHELL")
- term := flag.String("term", "", "$TERM")
-
- // non-posix
- pid := flag.Int("pid", -1, "$$")
- sessionPid := flag.Int("sessionPid", -1, "$$ at session start")
- shlvl := flag.Int("shlvl", -1, "$SHLVL")
-
- host := flag.String("host", "", "$HOSTNAME")
- hosttype := flag.String("hosttype", "", "$HOSTTYPE")
- ostype := flag.String("ostype", "", "$OSTYPE")
- machtype := flag.String("machtype", "", "$MACHTYPE")
- gitCdup := flag.String("gitCdup", "", "git rev-parse --show-cdup")
- gitRemote := flag.String("gitRemote", "", "git remote get-url origin")
-
- gitCdupExitCode := flag.Int("gitCdupExitCode", -1, "... $?")
- gitRemoteExitCode := flag.Int("gitRemoteExitCode", -1, "... $?")
-
- // before after
- timezoneBefore := flag.String("timezoneBefore", "", "")
-
- osReleaseID := flag.String("osReleaseId", "", "/etc/os-release ID")
- osReleaseVersionID := flag.String("osReleaseVersionId", "",
- "/etc/os-release ID")
- osReleaseIDLike := flag.String("osReleaseIdLike", "", "/etc/os-release ID")
- osReleaseName := flag.String("osReleaseName", "", "/etc/os-release ID")
- osReleasePrettyName := flag.String("osReleasePrettyName", "",
- "/etc/os-release ID")
-
- rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME")
- rtsess := flag.String("realtimeSession", "-1",
- "on session start $EPOCHREALTIME")
- rtsessboot := flag.String("realtimeSessSinceBoot", "-1",
- "on session start $EPOCHREALTIME")
- flag.Parse()
-
- if *showVersion == true {
- fmt.Println(version)
- os.Exit(0)
- }
- if *showRevision == true {
- fmt.Println(commit)
- os.Exit(0)
- }
- if *requireVersion != "" && *requireVersion != version {
- fmt.Println("Please restart/reload this terminal session " +
- "(resh version: " + version +
- "; resh version of this terminal session: " + *requireVersion +
- ")")
- os.Exit(3)
- }
- if *requireRevision != "" && *requireRevision != commit {
- fmt.Println("Please restart/reload this terminal session " +
- "(resh revision: " + commit +
- "; resh revision of this terminal session: " + *requireRevision +
- ")")
- os.Exit(3)
- }
- if *recallPrefix != "" && *recall == false {
- log.Println("Option '--prefix-search' only works with '--recall' option - exiting!")
- os.Exit(4)
- }
-
- realtimeBefore, err := strconv.ParseFloat(*rtb, 64)
+ config, errCfg := cfg.New()
+ logger, err := logger.New("collect", config.LogLevel, development)
if err != nil {
- log.Fatal("Flag Parsing error (rtb):", err)
+ fmt.Printf("Error while creating logger: %v", err)
}
- realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64)
- if err != nil {
- log.Fatal("Flag Parsing error (rt sess):", err)
+ defer logger.Sync() // flushes buffer, if any
+ if errCfg != nil {
+ logger.Error("Error while getting configuration", zap.Error(errCfg))
}
- realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64)
+ out := output.New(logger, "resh-collect ERROR")
+
+ args := opt.HandleVersionOpts(out, os.Args, version, commit)
+
+ flags := pflag.NewFlagSet("", pflag.ExitOnError)
+ cmdLine := flags.String("cmd-line", "", "Command line")
+ gitRemote := flags.String("git-remote", "", "> git remote get-url origin")
+ home := flags.String("home", "", "$HOME")
+ pwd := flags.String("pwd", "", "$PWD - present working directory")
+ recordID := flags.String("record-id", "", "Resh generated record ID")
+ sessionID := flags.String("session-id", "", "Resh generated session ID")
+ sessionPID := flags.Int("session-pid", -1, "$$ - Shell session PID")
+ shell := flags.String("shell", "", "Current shell")
+ shlvl := flags.Int("shlvl", -1, "$SHLVL")
+ timeStr := flags.String("time", "-1", "$EPOCHREALTIME")
+ flags.Parse(args)
+
+ time, err := strconv.ParseFloat(*timeStr, 64)
if err != nil {
- log.Fatal("Flag Parsing error (rt sess boot):", err)
+ out.FatalE("Error while parsing flag --time", err)
}
- realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart
- realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart
-
- timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore)
- realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset
realPwd, err := filepath.EvalSymlinks(*pwd)
if err != nil {
- log.Println("err while handling pwd realpath:", err)
+ out.ErrorE("Error while evaluating symlinks in PWD", err)
realPwd = ""
}
- gitDir, gitRealDir := collect.GetGitDirs(*gitCdup, *gitCdupExitCode, *pwd)
- if *gitRemoteExitCode != 0 {
- *gitRemote = ""
- }
+ rec := recordint.Collect{
+ SessionID: *sessionID,
+ Shlvl: *shlvl,
+ SessionPID: *sessionPID,
+
+ Shell: *shell,
+
+ Rec: record.V1{
+ SessionID: *sessionID,
+ RecordID: *recordID,
+
+ CmdLine: *cmdLine,
- // if *osReleaseID == "" {
- // *osReleaseID = "linux"
- // }
- // if *osReleaseName == "" {
- // *osReleaseName = "Linux"
- // }
- // if *osReleasePrettyName == "" {
- // *osReleasePrettyName = "Linux"
- // }
-
- if *recall {
- rec := records.SlimRecord{
- SessionID: *sessionID,
- RecallHistno: *recallHistno,
- RecallPrefix: *recallPrefix,
- }
- str, found := collect.SendRecallRequest(rec, strconv.Itoa(config.Port))
- if found == false {
- os.Exit(1)
- }
- fmt.Println(str)
- } 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,
- RecordID: *recordID,
-
- // 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: commit,
-
- RecallActionsRaw: *recallActions,
- RecallPrefix: *recallPrefix,
- RecallStrategy: *recallStrategy,
- RecallLastCmdLine: *recallLastCmdLine,
- },
- }
- collect.SendRecord(rec, strconv.Itoa(config.Port), "/record")
+ Home: *home,
+ Pwd: *pwd,
+ RealPwd: realPwd,
+
+ GitOriginRemote: *gitRemote,
+
+ Time: fmt.Sprintf("%.4f", time),
+
+ PartOne: true,
+ PartsNotMerged: true,
+ },
}
+ collect.SendRecord(out, rec, strconv.Itoa(config.Port), "/record")
}
diff --git a/cmd/config/main.go b/cmd/config/main.go
index e67ee7b9..e397901a 100644
--- a/cmd/config/main.go
+++ b/cmd/config/main.go
@@ -4,24 +4,24 @@ import (
"flag"
"fmt"
"os"
- "os/user"
- "path/filepath"
"strings"
- "github.com/BurntSushi/toml"
- "github.com/curusarn/resh/pkg/cfg"
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/logger"
+ "go.uber.org/zap"
)
+// info passed during build
+var version string
+var commit string
+var development string
+
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)
+ config, errCfg := cfg.New()
+ logger, _ := logger.New("config", config.LogLevel, development)
+ defer logger.Sync() // flushes buffer, if any
+ if errCfg != nil {
+ logger.Error("Error while getting configuration", zap.Error(errCfg))
}
configKey := flag.String("key", "", "Key of the requested config entry")
@@ -39,9 +39,9 @@ func main() {
case "port":
fmt.Println(config.Port)
case "sesswatchperiodseconds":
- fmt.Println(config.SesswatchPeriodSeconds)
+ fmt.Println(config.SessionWatchPeriodSeconds)
case "sesshistinithistorysize":
- fmt.Println(config.SesshistInitHistorySize)
+ fmt.Println(config.ReshHistoryMinSize)
default:
fmt.Println("Error: illegal --key!")
os.Exit(1)
diff --git a/cmd/control/cmd/completion.go b/cmd/control/cmd/completion.go
deleted file mode 100644
index e80fa56d..00000000
--- a/cmd/control/cmd/completion.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package cmd
-
-import (
- "os"
-
- "github.com/curusarn/resh/cmd/control/status"
- "github.com/spf13/cobra"
-)
-
-// completionCmd represents the completion command
-var completionCmd = &cobra.Command{
- Use: "completion",
- Short: "generate bash/zsh completion scripts",
- Long: `To load completion run
-
-. <(reshctl completion bash)
-
-OR
-
-. <(reshctl completion zsh) && compdef _reshctl reshctl
-`,
-}
-
-var completionBashCmd = &cobra.Command{
- Use: "bash",
- Short: "generate bash completion scripts",
- Long: `To load completion run
-
-. <(reshctl completion bash)
-`,
- Run: func(cmd *cobra.Command, args []string) {
- rootCmd.GenBashCompletion(os.Stdout)
- exitCode = status.Success
- },
-}
-
-var completionZshCmd = &cobra.Command{
- Use: "zsh",
- Short: "generate zsh completion scripts",
- Long: `To load completion run
-
-. <(reshctl completion zsh) && compdef _reshctl reshctl
-`,
- Run: func(cmd *cobra.Command, args []string) {
- rootCmd.GenZshCompletion(os.Stdout)
- exitCode = status.Success
- },
-}
diff --git a/cmd/control/cmd/debug.go b/cmd/control/cmd/debug.go
deleted file mode 100644
index f1401c7a..00000000
--- a/cmd/control/cmd/debug.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package cmd
-
-import (
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
-
- "github.com/curusarn/resh/cmd/control/status"
- "github.com/spf13/cobra"
-)
-
-var debugCmd = &cobra.Command{
- Use: "debug",
- 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",
- 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",
- Long: "Shows output from last runs of resh",
- Run: func(cmd *cobra.Command, args []string) {
- files := []string{
- "daemon_last_run_out.txt",
- "collect_last_run_out.txt",
- "postcollect_last_run_out.txt",
- "session_init_last_run_out.txt",
- "cli_last_run_out.txt",
- }
- dir := os.Getenv("__RESH_XDG_CACHE_HOME")
- for _, fpath := range files {
- fpath := filepath.Join(dir, fpath)
- debugReadFile(fpath)
- }
- exitCode = status.Success
- },
-}
-
-func debugReadFile(path string) {
- fmt.Println("============================================================")
- fmt.Println(" filepath:", path)
- fmt.Println("============================================================")
- dat, err := ioutil.ReadFile(path)
- if err != nil {
- fmt.Println("ERROR while reading file:", err)
- }
- fmt.Println(string(dat))
-}
diff --git a/cmd/control/cmd/doctor.go b/cmd/control/cmd/doctor.go
new file mode 100644
index 00000000..f7400c99
--- /dev/null
+++ b/cmd/control/cmd/doctor.go
@@ -0,0 +1,147 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "time"
+
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/check"
+ "github.com/curusarn/resh/internal/msg"
+ "github.com/curusarn/resh/internal/status"
+ "github.com/spf13/cobra"
+ "go.uber.org/zap"
+)
+
+func doctorCmdFunc(config cfg.Config) func(*cobra.Command, []string) {
+ return func(cmd *cobra.Command, args []string) {
+ allOK := true
+ if !checkDaemon(config) {
+ allOK = false
+ printDivider()
+ }
+ if !checkShellSession() {
+ allOK = false
+ printDivider()
+ }
+ if !checkShells() {
+ allOK = false
+ printDivider()
+ }
+
+ if allOK {
+ out.Info("Everything looks good.")
+ }
+ }
+}
+
+func printDivider() {
+ fmt.Printf("\n")
+}
+
+var msgFailedDaemonStart = `Failed to start RESH daemon.
+ -> Start RESH daemon manually - run: resh-daemon-start
+ -> Or restart this terminal window to bring RESH daemon back up
+ -> You can check logs: ~/.local/share/resh/log.json (or ~/$XDG_DATA_HOME/resh/log.json)
+ -> You can create an issue at: https://github.com/curusarn/resh/issues
+`
+
+func checkDaemon(config cfg.Config) bool {
+ ok := true
+ resp, err := status.GetDaemonStatus(config.Port)
+ if err != nil {
+ out.InfoE("RESH Daemon is not running", err)
+ out.Info("Attempting to start RESH daemon ...")
+ resp, err = startDaemon(config.Port, 5, 200*time.Millisecond)
+ if err != nil {
+ out.InfoE(msgFailedDaemonStart, err)
+ return false
+ }
+ ok = false
+ out.Info("Successfully started daemon.")
+ }
+ if version != resp.Version {
+ out.InfoDaemonVersionMismatch(version, resp.Version)
+ return false
+ }
+ return ok
+}
+
+func startDaemon(port int, maxRetries int, backoff time.Duration) (*msg.StatusResponse, error) {
+ err := exec.Command("resh-daemon-start").Run()
+ if err != nil {
+ return nil, err
+ }
+ var resp *msg.StatusResponse
+ retry := 0
+ for {
+ time.Sleep(backoff)
+ resp, err = status.GetDaemonStatus(port)
+ if err == nil {
+ break
+ }
+ if retry == maxRetries {
+ return nil, err
+ }
+ out.Logger.Error("Failed to get daemon status - retrying", zap.Error(err), zap.Int("retry", retry))
+ retry++
+ continue
+ }
+ return resp, nil
+}
+
+var msgShellFilesNotLoaded = `RESH shell files were not properly loaded in this terminal
+ -> Try restarting this terminal to see if the issue persists
+ -> Check your shell rc files (e.g. .zshrc, .bashrc, ...)
+ -> You can create an issue at: https://github.com/curusarn/resh/issues
+`
+
+func checkShellSession() bool {
+ versionEnv, found := os.LookupEnv("__RESH_VERSION")
+ if !found {
+ out.Info(msgShellFilesNotLoaded)
+ return false
+ }
+ if version != versionEnv {
+ out.InfoTerminalVersionMismatch(version, versionEnv)
+ return false
+ }
+ return true
+}
+
+func checkShells() bool {
+ allOK := true
+
+ msg, err := check.LoginShell()
+ if err != nil {
+ out.InfoE("Failed to get login shell", err)
+ allOK = false
+ }
+ if msg != "" {
+ out.Info(msg)
+ allOK = false
+ }
+
+ msg, err = check.ZshVersion()
+ if err != nil {
+ out.InfoE("Failed to check zsh version", err)
+ allOK = false
+ }
+ if msg != "" {
+ out.Info(msg)
+ allOK = false
+ }
+
+ msg, err = check.BashVersion()
+ if err != nil {
+ out.InfoE("Failed to check bash version", err)
+ allOK = false
+ }
+ if msg != "" {
+ out.Info(msg)
+ allOK = false
+ }
+
+ return allOK
+}
diff --git a/cmd/control/cmd/enable.go b/cmd/control/cmd/enable.go
deleted file mode 100644
index 360be5bd..00000000
--- a/cmd/control/cmd/enable.go
+++ /dev/null
@@ -1,80 +0,0 @@
-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"
-)
-
-// Enable commands
-
-var enableCmd = &cobra.Command{
- Use: "enable",
- Short: "enable RESH features (bindings)",
-}
-
-var enableControlRBindingCmd = &cobra.Command{
- Use: "ctrl_r_binding",
- Short: "enable RESH-CLI binding for Ctrl+R",
- Run: func(cmd *cobra.Command, args []string) {
- exitCode = enableDisableControlRBindingGlobally(true)
- if exitCode == status.Success {
- exitCode = status.EnableControlRBinding
- }
- },
-}
-
-// Disable commands
-
-var disableCmd = &cobra.Command{
- Use: "disable",
- Short: "disable RESH features (bindings)",
-}
-
-var disableControlRBindingCmd = &cobra.Command{
- Use: "ctrl_r_binding",
- Short: "disable RESH-CLI binding for Ctrl+R",
- Run: func(cmd *cobra.Command, args []string) {
- exitCode = enableDisableControlRBindingGlobally(false)
- if exitCode == status.Success {
- exitCode = status.DisableControlRBinding
- }
- },
-}
-
-func enableDisableControlRBindingGlobally(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
- }
- if config.BindControlR != value {
- config.BindControlR = value
-
- f, err := os.Create(configPath)
- if err != nil {
- fmt.Println("Error: Failed to create/open file:", configPath, "; error:", err)
- return status.Fail
- }
- 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 status.Fail
- }
- }
- if value {
- fmt.Println("RESH SEARCH app Ctrl+R binding: ENABLED")
- } else {
- fmt.Println("RESH SEARCH app Ctrl+R binding: DISABLED")
- }
- return status.Success
-}
diff --git a/cmd/control/cmd/root.go b/cmd/control/cmd/root.go
index 35c770d2..cf567c81 100644
--- a/cmd/control/cmd/root.go
+++ b/cmd/control/cmd/root.go
@@ -2,70 +2,58 @@ package cmd
import (
"fmt"
- "log"
- "os/user"
- "path/filepath"
- "github.com/BurntSushi/toml"
- "github.com/curusarn/resh/cmd/control/status"
- "github.com/curusarn/resh/pkg/cfg"
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/logger"
+ "github.com/curusarn/resh/internal/output"
"github.com/spf13/cobra"
)
-// globals
-var exitCode status.Code
var version string
var commit string
-var debug = false
-var config cfg.Config
+
+// globals
+var out *output.Output
var rootCmd = &cobra.Command{
Use: "reshctl",
- Short: "Reshctl (RESH control) - check status, update, enable/disable features, sanitize history and more.",
+ Short: "Reshctl (RESH control) - check status, update",
}
// Execute reshctl
-func Execute(ver, com string) status.Code {
+func Execute(ver, com, development string) {
version = ver
commit = com
- usr, _ := user.Current()
- dir := usr.HomeDir
- configPath := filepath.Join(dir, ".config/resh.toml")
- if _, err := toml.DecodeFile(configPath, &config); err != nil {
- log.Println("Error reading config", err)
- return status.Fail
+ config, errCfg := cfg.New()
+ logger, err := logger.New("reshctl", config.LogLevel, development)
+ if err != nil {
+ fmt.Printf("Error while creating logger: %v", err)
}
- if config.Debug {
- debug = true
- // log.SetFlags(log.LstdFlags | log.Lmicroseconds)
+ defer logger.Sync() // flushes buffer, if any
+ out = output.New(logger, "ERROR")
+ if errCfg != nil {
+ out.ErrorE("Error while getting configuration", errCfg)
}
- rootCmd.AddCommand(enableCmd)
- enableCmd.AddCommand(enableControlRBindingCmd)
-
- rootCmd.AddCommand(disableCmd)
- disableCmd.AddCommand(disableControlRBindingCmd)
-
- rootCmd.AddCommand(completionCmd)
- completionCmd.AddCommand(completionBashCmd)
- completionCmd.AddCommand(completionZshCmd)
-
- rootCmd.AddCommand(debugCmd)
- debugCmd.AddCommand(debugReloadCmd)
- debugCmd.AddCommand(debugInspectCmd)
- debugCmd.AddCommand(debugOutputCmd)
+ var versionCmd = cobra.Command{
+ Use: "version",
+ Short: "show RESH version",
+ Run: versionCmdFunc(config),
+ }
+ rootCmd.AddCommand(&versionCmd)
- rootCmd.AddCommand(statusCmd)
+ doctorCmd := cobra.Command{
+ Use: "doctor",
+ Short: "check common problems",
+ Run: doctorCmdFunc(config),
+ }
+ rootCmd.AddCommand(&doctorCmd)
updateCmd.Flags().BoolVar(&betaFlag, "beta", false, "Update to latest version even if it's beta.")
rootCmd.AddCommand(updateCmd)
- rootCmd.AddCommand(sanitizeCmd)
-
if err := rootCmd.Execute(); err != nil {
- fmt.Println(err)
- return status.Fail
+ out.FatalE("Command ended with error", err)
}
- return exitCode
}
diff --git a/cmd/control/cmd/sanitize.go b/cmd/control/cmd/sanitize.go
deleted file mode 100644
index 08f29da3..00000000
--- a/cmd/control/cmd/sanitize.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package cmd
-
-import (
- "fmt"
- "os"
- "os/exec"
- "os/user"
-
- "github.com/curusarn/resh/cmd/control/status"
- "github.com/spf13/cobra"
-)
-
-var sanitizeCmd = &cobra.Command{
- Use: "sanitize",
- Short: "produce a sanitized version of your RESH history",
- Run: func(cmd *cobra.Command, args []string) {
- exitCode = status.Success
- usr, _ := user.Current()
- dir := usr.HomeDir
-
- fmt.Println()
- fmt.Println(" HOW IT WORKS")
- fmt.Println(" In sanitized history, all sensitive information is replaced with its SHA256 hashes.")
- fmt.Println()
- fmt.Println("Creating sanitized history files ...")
- fmt.Println(" * ~/resh_history_sanitized.json (full lengh hashes)")
- execCmd := exec.Command("resh-sanitize", "-trim-hashes", "0", "--output", dir+"/resh_history_sanitized.json")
- execCmd.Stdout = os.Stdout
- execCmd.Stderr = os.Stderr
- err := execCmd.Run()
- if err != nil {
- exitCode = status.Fail
- }
-
- fmt.Println(" * ~/resh_history_sanitized_trim12.json (12 char hashes)")
- execCmd = exec.Command("resh-sanitize", "-trim-hashes", "12", "--output", dir+"/resh_history_sanitized_trim12.json")
- execCmd.Stdout = os.Stdout
- execCmd.Stderr = os.Stderr
- err = execCmd.Run()
- if err != nil {
- exitCode = status.Fail
- }
- fmt.Println()
- fmt.Println("Please direct all questions and/or issues to: https://github.com/curusarn/resh/issues")
- fmt.Println()
- fmt.Println("Please look at the resulting sanitized history using commands below.")
- fmt.Println(" * Pretty print JSON")
- fmt.Println(" cat ~/resh_history_sanitized_trim12.json | jq")
- fmt.Println()
- fmt.Println(" * Only show commands, don't show metadata")
- fmt.Println(" cat ~/resh_history_sanitized_trim12.json | jq '.[\"cmdLine\"]'")
- fmt.Println()
- },
-}
diff --git a/cmd/control/cmd/status.go b/cmd/control/cmd/status.go
deleted file mode 100644
index f12b7fa3..00000000
--- a/cmd/control/cmd/status.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package cmd
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "log"
- "net/http"
- "os"
- "strconv"
-
- "github.com/curusarn/resh/cmd/control/status"
- "github.com/curusarn/resh/pkg/msg"
- "github.com/spf13/cobra"
-)
-
-var statusCmd = &cobra.Command{
- Use: "status",
- Short: "show RESH status",
- Run: func(cmd *cobra.Command, args []string) {
- fmt.Println("resh " + version)
- fmt.Println()
- fmt.Println("Resh versions ...")
- fmt.Println(" * installed: " + version + " (" + commit + ")")
- versionEnv, found := os.LookupEnv("__RESH_VERSION")
- if found == false {
- versionEnv = "UNKNOWN!"
- }
- commitEnv, found := os.LookupEnv("__RESH_REVISION")
- if found == false {
- commitEnv = "unknown"
- }
- fmt.Println(" * this shell session: " + versionEnv + " (" + commitEnv + ")")
-
- resp, err := getDaemonStatus(config.Port)
- if err != nil {
- fmt.Println(" * RESH-DAEMON IS NOT RUNNING")
- fmt.Println(" * Please REPORT this here: https://github.com/curusarn/resh/issues")
- fmt.Println(" * Please RESTART this terminal window")
- exitCode = status.Fail
- return
- }
- fmt.Println(" * daemon: " + resp.Version + " (" + resp.Commit + ")")
-
- if version != resp.Version || version != versionEnv {
- fmt.Println(" * THERE IS A MISMATCH BETWEEN VERSIONS!")
- fmt.Println(" * Please REPORT this here: https://github.com/curusarn/resh/issues")
- fmt.Println(" * Please RESTART this terminal window")
- }
-
- exitCode = status.ReshStatus
- },
-}
-
-func getDaemonStatus(port int) (msg.StatusResponse, error) {
- mess := msg.StatusResponse{}
- url := "http://localhost:" + strconv.Itoa(port) + "/status"
- resp, err := http.Get(url)
- if err != nil {
- return mess, err
- }
- defer resp.Body.Close()
- jsn, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- log.Fatal("Error while reading 'daemon /status' response:", err)
- }
- err = json.Unmarshal(jsn, &mess)
- if err != nil {
- log.Fatal("Error while decoding 'daemon /status' response:", err)
- }
- return mess, nil
-}
diff --git a/cmd/control/cmd/update.go b/cmd/control/cmd/update.go
index dcdb21dd..c74c555b 100644
--- a/cmd/control/cmd/update.go
+++ b/cmd/control/cmd/update.go
@@ -3,10 +3,8 @@ package cmd
import (
"os"
"os/exec"
- "os/user"
"path/filepath"
- "github.com/curusarn/resh/cmd/control/status"
"github.com/spf13/cobra"
)
@@ -15,9 +13,11 @@ var updateCmd = &cobra.Command{
Use: "update",
Short: "check for updates and update RESH",
Run: func(cmd *cobra.Command, args []string) {
- usr, _ := user.Current()
- dir := usr.HomeDir
- rawinstallPath := filepath.Join(dir, ".resh/rawinstall.sh")
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ out.FatalE("Could not get user home dir", err)
+ }
+ rawinstallPath := filepath.Join(homeDir, ".resh/rawinstall.sh")
execArgs := []string{rawinstallPath}
if betaFlag {
execArgs = append(execArgs, "--beta")
@@ -25,9 +25,9 @@ var updateCmd = &cobra.Command{
execCmd := exec.Command("bash", execArgs...)
execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr
- err := execCmd.Run()
- if err == nil {
- exitCode = status.Success
+ err = execCmd.Run()
+ if err != nil {
+ out.FatalE("Update ended with error", err)
}
},
}
diff --git a/cmd/control/cmd/version.go b/cmd/control/cmd/version.go
new file mode 100644
index 00000000..11f884f7
--- /dev/null
+++ b/cmd/control/cmd/version.go
@@ -0,0 +1,45 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/status"
+ "github.com/spf13/cobra"
+)
+
+func versionCmdFunc(config cfg.Config) func(*cobra.Command, []string) {
+ return func(cmd *cobra.Command, args []string) {
+
+ fmt.Printf("Installed: %s\n", version)
+
+ versionEnv := getEnvVarWithDefault("__RESH_VERSION", "")
+ fmt.Printf("This terminal session: %s\n", version)
+
+ resp, err := status.GetDaemonStatus(config.Port)
+ if err != nil {
+ fmt.Printf("Running checks: %s\n", version)
+ out.ErrorDaemonNotRunning(err)
+ return
+ }
+ fmt.Printf("Currently running daemon: %s\n", resp.Version)
+
+ if version != resp.Version {
+ out.ErrorDaemonVersionMismatch(version, resp.Version)
+ return
+ }
+ if version != versionEnv {
+ out.ErrorTerminalVersionMismatch(version, versionEnv)
+ return
+ }
+ }
+}
+
+func getEnvVarWithDefault(varName, defaultValue string) string {
+ val, found := os.LookupEnv(varName)
+ if !found {
+ return defaultValue
+ }
+ return val
+}
diff --git a/cmd/control/main.go b/cmd/control/main.go
index ecae4e0a..b431b670 100644
--- a/cmd/control/main.go
+++ b/cmd/control/main.go
@@ -1,17 +1,13 @@
package main
import (
- "os"
-
"github.com/curusarn/resh/cmd/control/cmd"
)
-// version from git set during build
var version string
-
-// commit from git set during build
var commit string
+var development string
func main() {
- os.Exit(int(cmd.Execute(version, commit)))
+ cmd.Execute(version, commit, development)
}
diff --git a/cmd/control/status/status.go b/cmd/control/status/status.go
deleted file mode 100644
index 0d797d79..00000000
--- a/cmd/control/status/status.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package status
-
-// Code - exit code of the resh-control command
-type Code int
-
-const (
- // Success exit code
- Success Code = 0
- // Fail exit code
- Fail = 1
- // EnableResh exit code - tells reshctl() wrapper to enable resh
- // EnableResh = 30
-
- // EnableControlRBinding exit code - tells reshctl() wrapper to enable control R binding
- EnableControlRBinding = 32
-
- // DisableControlRBinding exit code - tells reshctl() wrapper to disable control R binding
- DisableControlRBinding = 42
- // ReloadRcFiles exit code - tells reshctl() wrapper to reload shellrc resh file
- ReloadRcFiles = 50
- // InspectSessionHistory exit code - tells reshctl() wrapper to take current sessionID and send /inspect request to daemon
- InspectSessionHistory = 51
- // ReshStatus exit code - tells reshctl() wrapper to show RESH status (aka systemctl status)
- ReshStatus = 52
-)
diff --git a/cmd/daemon/dump.go b/cmd/daemon/dump.go
index 375da76a..d3a2e1fa 100644
--- a/cmd/daemon/dump.go
+++ b/cmd/daemon/dump.go
@@ -2,53 +2,50 @@ package main
import (
"encoding/json"
- "io/ioutil"
- "log"
+ "io"
"net/http"
- "github.com/curusarn/resh/pkg/histfile"
- "github.com/curusarn/resh/pkg/msg"
+ "github.com/curusarn/resh/internal/histfile"
+ "github.com/curusarn/resh/internal/msg"
+ "go.uber.org/zap"
)
type dumpHandler struct {
+ sugar *zap.SugaredLogger
histfileBox *histfile.Histfile
}
func (h *dumpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if Debug {
- log.Println("/dump START")
- log.Println("/dump reading body ...")
- }
- jsn, err := ioutil.ReadAll(r.Body)
+ sugar := h.sugar.With(zap.String("endpoint", "/dump"))
+ sugar.Debugw("Handling request, reading body ...")
+ jsn, err := io.ReadAll(r.Body)
if err != nil {
- log.Println("Error reading the body", err)
+ sugar.Errorw("Error reading body", "error", err)
return
}
+ sugar.Debugw("Unmarshaling record ...")
mess := msg.CliMsg{}
- if Debug {
- log.Println("/dump unmarshaling record ...")
- }
err = json.Unmarshal(jsn, &mess)
if err != nil {
- log.Println("Decoding error:", err)
- log.Println("Payload:", jsn)
+ sugar.Errorw("Error during unmarshaling",
+ "error", err,
+ "payload", jsn,
+ )
return
}
- if Debug {
- log.Println("/dump dumping ...")
- }
+ sugar.Debugw("Getting records to send ...")
fullRecords := h.histfileBox.DumpCliRecords()
if err != nil {
- log.Println("Dump error:", err)
+ sugar.Errorw("Error when getting records", "error", err)
}
- resp := msg.CliResponse{CliRecords: fullRecords.List}
+ resp := msg.CliResponse{Records: fullRecords.List}
jsn, err = json.Marshal(&resp)
if err != nil {
- log.Println("Encoding error:", err)
+ sugar.Errorw("Error when marshaling", "error", err)
return
}
w.Write(jsn)
- log.Println("/dump END")
+ sugar.Infow("Request handled")
}
diff --git a/cmd/daemon/kill.go b/cmd/daemon/kill.go
deleted file mode 100644
index 7c403a9a..00000000
--- a/cmd/daemon/kill.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package main
-
-import (
- "io/ioutil"
- "log"
- "os/exec"
- "strconv"
- "strings"
-)
-
-func killDaemon(pidfile string) error {
- dat, err := ioutil.ReadFile(pidfile)
- if err != nil {
- log.Println("Reading pid file failed", err)
- }
- log.Print(string(dat))
- pid, err := strconv.Atoi(strings.TrimSuffix(string(dat), "\n"))
- if err != nil {
- log.Fatal("Pidfile contents are malformed", err)
- }
- cmd := exec.Command("kill", "-s", "sigint", strconv.Itoa(pid))
- err = cmd.Run()
- if err != nil {
- log.Printf("Command finished with error: %v", err)
- return err
- }
- return nil
-}
diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go
index fc7b5bde..01e9105c 100644
--- a/cmd/daemon/main.go
+++ b/cmd/daemon/main.go
@@ -1,87 +1,165 @@
package main
import (
- //"flag"
-
- "io/ioutil"
- "log"
+ "fmt"
"os"
- "os/user"
+ "os/exec"
"path/filepath"
"strconv"
+ "strings"
- "github.com/BurntSushi/toml"
- "github.com/curusarn/resh/pkg/cfg"
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/datadir"
+ "github.com/curusarn/resh/internal/device"
+ "github.com/curusarn/resh/internal/logger"
+ "github.com/curusarn/resh/internal/status"
+ "go.uber.org/zap"
)
-// version from git set during build
+// info passed during build
var version string
-
-// commit from git set during build
var commit string
+var development string
+
+const helpMsg = `ERROR: resh-daemon doesn't accept any arguments
+
+WARNING:
+ You shouldn't typically need to start RESH daemon yourself.
+ Unless its already running, RESH daemon is started when a new terminal is opened.
+ RESH daemon will not start if it's already running even when you run it manually.
+
+USAGE:
+ $ resh-daemon
+ Runs the daemon as foreground process. You can kill it with CTRL+C.
+
+ $ resh-daemon-start
+ Runs the daemon as background process detached from terminal.
+
+LOGS & DEBUGGING:
+ Logs are located in:
+ ${XDG_DATA_HOME}/resh/log.json (if XDG_DATA_HOME is set)
+ ~/.local/share/resh/log.json (otherwise - more common)
+
+ A good way to see the logs as they are being produced is:
+ $ tail -f ~/.local/share/resh/log.json
-// Debug switch
-var Debug = false
+MORE INFO:
+ https://github.com/curusarn/resh/
+`
func main() {
- log.Println("Daemon starting... \n" +
- "version: " + version +
- " commit: " + commit)
- usr, _ := user.Current()
- dir := usr.HomeDir
- pidfilePath := filepath.Join(dir, ".resh/resh.pid")
- configPath := filepath.Join(dir, ".config/resh.toml")
- 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)
+ if len(os.Args) > 1 {
+ fmt.Fprint(os.Stderr, helpMsg)
+ os.Exit(1)
+ }
+ config, errCfg := cfg.New()
+ logger, err := logger.New("daemon", config.LogLevel, development)
if err != nil {
- log.Fatalf("Error opening file: %v\n", err)
+ fmt.Printf("Error while creating logger: %v", err)
}
- defer f.Close()
-
- log.SetOutput(f)
- log.SetPrefix(strconv.Itoa(os.Getpid()) + " | ")
-
- var config cfg.Config
- if _, err := toml.DecodeFile(configPath, &config); err != nil {
- log.Printf("Error reading config: %v\n", err)
- return
+ defer logger.Sync() // flushes buffer, if any
+ if errCfg != nil {
+ logger.Error("Error while getting configuration", zap.Error(errCfg))
+ }
+ sugar := logger.Sugar()
+ d := daemon{sugar: sugar}
+ sugar.Infow("Daemon starting ...",
+ "version", version,
+ "commit", commit,
+ )
+ dataDir, err := datadir.MakePath()
+ if err != nil {
+ sugar.Fatalw("Could not get user data directory", zap.Error(err))
+ }
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ sugar.Fatalw("Could not get user home directory", zap.Error(err))
}
- if config.Debug {
- Debug = true
- log.SetFlags(log.LstdFlags | log.Lmicroseconds)
+ // TODO: These paths should be probably defined in a package
+ pidFile := filepath.Join(dataDir, "daemon.pid")
+ reshHistoryPath := filepath.Join(dataDir, datadir.HistoryFileName)
+ bashHistoryPath := filepath.Join(homeDir, ".bash_history")
+ zshHistoryPath := filepath.Join(homeDir, ".zsh_history")
+ deviceID, err := device.GetID(dataDir)
+ if err != nil {
+ sugar.Fatalw("Could not get resh device ID", zap.Error(err))
+ }
+ deviceName, err := device.GetName(dataDir)
+ if err != nil {
+ sugar.Fatalw("Could not get resh device name", zap.Error(err))
}
- res, err := isDaemonRunning(config.Port)
+ sugar = sugar.With(zap.Int("daemonPID", os.Getpid()))
+
+ res, err := status.IsDaemonRunning(config.Port)
if err != nil {
- log.Printf("Error while checking if the daemon is runnnig"+
- " - it's probably not running: %v\n", err)
+ sugar.Errorw("Error while checking daemon status - it's probably not running",
+ "error", err)
}
if res {
- log.Println("Daemon is already running - exiting!")
+ sugar.Errorw("Daemon is already running - exiting!")
return
}
- _, err = os.Stat(pidfilePath)
+ _, err = os.Stat(pidFile)
if err == nil {
- log.Println("Pidfile exists")
+ sugar.Warnw("PID file exists",
+ "PIDFile", pidFile)
// kill daemon
- err = killDaemon(pidfilePath)
+ err = d.killDaemon(pidFile)
if err != nil {
- log.Printf("Error while killing daemon: %v\n", err)
+ sugar.Errorw("Could not kill daemon",
+ "error", err,
+ )
}
}
- err = ioutil.WriteFile(pidfilePath, []byte(strconv.Itoa(os.Getpid())), 0644)
+ err = os.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0644)
+ if err != nil {
+ sugar.Fatalw("Could not create PID file",
+ "error", err,
+ "PIDFile", pidFile,
+ )
+ }
+ server := Server{
+ sugar: sugar,
+ config: config,
+ reshHistoryPath: reshHistoryPath,
+ bashHistoryPath: bashHistoryPath,
+ zshHistoryPath: zshHistoryPath,
+
+ deviceID: deviceID,
+ deviceName: deviceName,
+ }
+ server.Run()
+ sugar.Infow("Removing PID file ...",
+ "PIDFile", pidFile,
+ )
+ err = os.Remove(pidFile)
+ if err != nil {
+ sugar.Errorw("Could not delete PID file", "error", err)
+ }
+ sugar.Info("Shutting down ...")
+}
+
+type daemon struct {
+ sugar *zap.SugaredLogger
+}
+
+func (d *daemon) killDaemon(pidFile string) error {
+ dat, err := os.ReadFile(pidFile)
+ if err != nil {
+ d.sugar.Errorw("Reading PID file failed",
+ "PIDFile", pidFile,
+ "error", err)
+ }
+ d.sugar.Infow("Successfully read PID file", "contents", string(dat))
+ pid, err := strconv.Atoi(strings.TrimSuffix(string(dat), "\n"))
if err != nil {
- log.Fatalf("Could not create pidfile: %v\n", err)
+ return fmt.Errorf("could not parse PID file contents: %w", err)
}
- runServer(config, reshHistoryPath, bashHistoryPath, zshHistoryPath)
- log.Println("main: Removing pidfile ...")
- err = os.Remove(pidfilePath)
+ d.sugar.Infow("Successfully parsed PID", "PID", pid)
+ err = exec.Command("kill", "-SIGTERM", fmt.Sprintf("%d", pid)).Run()
if err != nil {
- log.Printf("Could not delete pidfile: %v\n", err)
+ return fmt.Errorf("kill command finished with error: %w", err)
}
- log.Println("main: Shutdown - bye")
+ return nil
}
diff --git a/cmd/daemon/recall.go b/cmd/daemon/recall.go
deleted file mode 100644
index 930537ca..00000000
--- a/cmd/daemon/recall.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "io/ioutil"
- "log"
- "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"
-)
-
-type recallHandler struct {
- sesshistDispatch *sesshist.Dispatch
-}
-
-func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if Debug {
- 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.SlimRecord{}
- if Debug {
- log.Println("/recall unmarshaling record ...")
- }
- err = json.Unmarshal(jsn, &rec)
- if err != nil {
- log.Println("Decoding error:", err)
- log.Println("Payload:", jsn)
- return
- }
- if Debug {
- log.Println("/recall recalling ...")
- }
- found := true
- 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)
- found = false
- cmd = ""
- }
- resp := collect.SingleResponse{CmdLine: cmd, Found: found}
- if Debug {
- log.Println("/recall marshaling response ...")
- }
- jsn, err = json.Marshal(&resp)
- if err != nil {
- log.Println("Encoding error:", err)
- log.Println("Response:", resp)
- return
- }
- if Debug {
- log.Println(string(jsn))
- log.Println("/recall writing response ...")
- }
- w.Write(jsn)
- log.Println("/recall END - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ", cmd, " (found:", found, ")")
-}
-
-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/record.go b/cmd/daemon/record.go
index ee403f8f..93a0dd36 100644
--- a/cmd/daemon/record.go
+++ b/cmd/daemon/record.go
@@ -2,46 +2,64 @@ package main
import (
"encoding/json"
- "io/ioutil"
- "log"
+ "io"
"net/http"
- "github.com/curusarn/resh/pkg/records"
+ "github.com/curusarn/resh/internal/recordint"
+ "go.uber.org/zap"
)
+func NewRecordHandler(sugar *zap.SugaredLogger, subscribers []chan recordint.Collect) recordHandler {
+ return recordHandler{
+ sugar: sugar.With(zap.String("endpoint", "/record")),
+ subscribers: subscribers,
+ }
+}
+
type recordHandler struct {
- subscribers []chan records.Record
+ sugar *zap.SugaredLogger
+ subscribers []chan recordint.Collect
+
+ deviceID string
+ deviceName string
}
func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ sugar := h.sugar.With(zap.String("endpoint", "/record"))
+ sugar.Debugw("Handling request, sending response, reading body ...")
w.Write([]byte("OK\n"))
- jsn, err := ioutil.ReadAll(r.Body)
+ jsn, err := io.ReadAll(r.Body)
// run rest of the handler as goroutine to prevent any hangups
go func() {
if err != nil {
- log.Println("Error reading the body", err)
+ sugar.Errorw("Error reading body", "error", err)
return
}
- record := records.Record{}
- err = json.Unmarshal(jsn, &record)
+ sugar.Debugw("Unmarshaling record ...")
+ rec := recordint.Collect{}
+ err = json.Unmarshal(jsn, &rec)
if err != nil {
- log.Println("Decoding error: ", err)
- log.Println("Payload: ", jsn)
+ sugar.Errorw("Error during unmarshaling",
+ "error", err,
+ "payload", jsn,
+ )
return
}
- for _, sub := range h.subscribers {
- sub <- record
- }
part := "2"
- if record.PartOne {
+ if rec.Rec.PartOne {
part = "1"
}
- log.Println("/record - ", record.CmdLine, " - part", part)
+ sugar := sugar.With(
+ "cmdLine", rec.Rec.CmdLine,
+ "part", part,
+ )
+ rec.Rec.DeviceID = h.deviceID
+ rec.Rec.Device = h.deviceName
+ sugar.Debugw("Got record, sending to subscribers ...")
+ for _, sub := range h.subscribers {
+ sub <- rec
+ }
+ sugar.Debugw("Record sent to subscribers")
}()
-
- // fmt.Println("cmd:", r.CmdLine)
- // fmt.Println("pwd:", r.Pwd)
- // fmt.Println("git:", r.GitWorkTree)
- // fmt.Println("exit_code:", r.ExitCode)
}
diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go
index fc708259..e7631c41 100644
--- a/cmd/daemon/run-server.go
+++ b/cmd/daemon/run-server.go
@@ -4,33 +4,40 @@ import (
"net/http"
"os"
"strconv"
+ "time"
- "github.com/curusarn/resh/pkg/cfg"
- "github.com/curusarn/resh/pkg/histfile"
- "github.com/curusarn/resh/pkg/records"
- "github.com/curusarn/resh/pkg/sesshist"
- "github.com/curusarn/resh/pkg/sesswatch"
- "github.com/curusarn/resh/pkg/signalhandler"
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/histfile"
+ "github.com/curusarn/resh/internal/recordint"
+ "github.com/curusarn/resh/internal/sesswatch"
+ "github.com/curusarn/resh/internal/signalhandler"
+ "go.uber.org/zap"
)
-func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPath string) {
- var recordSubscribers []chan records.Record
- var sessionInitSubscribers []chan records.Record
+// TODO: turn server and handlers into package
+
+type Server struct {
+ sugar *zap.SugaredLogger
+ config cfg.Config
+
+ reshHistoryPath string
+ bashHistoryPath string
+ zshHistoryPath string
+
+ deviceID string
+ deviceName string
+}
+
+func (s *Server) Run() {
+ var recordSubscribers []chan recordint.Collect
+ var sessionInitSubscribers []chan recordint.SessionInit
var sessionDropSubscribers []chan string
var signalSubscribers []chan os.Signal
shutdown := make(chan string)
- // sessshist
- sesshistSessionsToInit := make(chan records.Record)
- sessionInitSubscribers = append(sessionInitSubscribers, sesshistSessionsToInit)
- sesshistSessionsToDrop := make(chan string)
- sessionDropSubscribers = append(sessionDropSubscribers, sesshistSessionsToDrop)
- sesshistRecords := make(chan records.Record)
- recordSubscribers = append(recordSubscribers, sesshistRecords)
-
// histfile
- histfileRecords := make(chan records.Record)
+ histfileRecords := make(chan recordint.Collect)
recordSubscribers = append(recordSubscribers, histfileRecords)
histfileSessionsToDrop := make(chan string)
sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop)
@@ -38,38 +45,46 @@ func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPa
signalSubscribers = append(signalSubscribers, histfileSignals)
maxHistSize := 10000 // lines
minHistSizeKB := 2000 // roughly lines
- histfileBox := histfile.New(histfileRecords, histfileSessionsToDrop,
- reshHistoryPath, bashHistoryPath, zshHistoryPath,
+ histfileBox := histfile.New(s.sugar, histfileRecords, histfileSessionsToDrop,
+ s.reshHistoryPath, s.bashHistoryPath, s.zshHistoryPath,
maxHistSize, minHistSizeKB,
histfileSignals, shutdown)
- // sesshist New
- sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop,
- sesshistRecords, histfileBox,
- config.SesshistInitHistorySize)
-
// sesswatch
- sesswatchRecords := make(chan records.Record)
+ sesswatchRecords := make(chan recordint.Collect)
recordSubscribers = append(recordSubscribers, sesswatchRecords)
- sesswatchSessionsToWatch := make(chan records.Record)
- sessionInitSubscribers = append(sessionInitSubscribers, sesswatchRecords, sesswatchSessionsToWatch)
- sesswatch.Go(sesswatchSessionsToWatch, sesswatchRecords, sessionDropSubscribers, config.SesswatchPeriodSeconds)
+ sesswatchSessionsToWatch := make(chan recordint.SessionInit)
+ sessionInitSubscribers = append(sessionInitSubscribers, sesswatchSessionsToWatch)
+ sesswatch.Go(
+ s.sugar,
+ sesswatchSessionsToWatch,
+ sesswatchRecords,
+ sessionDropSubscribers,
+ s.config.SessionWatchPeriodSeconds,
+ )
// handlers
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})
- mux.Handle("/dump", &dumpHandler{histfileBox: histfileBox})
+ mux.Handle("/status", &statusHandler{sugar: s.sugar})
+ mux.Handle("/record", &recordHandler{
+ sugar: s.sugar,
+ subscribers: recordSubscribers,
+ deviceID: s.deviceID,
+ deviceName: s.deviceName,
+ })
+ mux.Handle("/session_init", &sessionInitHandler{sugar: s.sugar, subscribers: sessionInitSubscribers})
+ mux.Handle("/dump", &dumpHandler{sugar: s.sugar, histfileBox: histfileBox})
server := &http.Server{
- Addr: "localhost:" + strconv.Itoa(config.Port),
- Handler: mux,
+ Addr: "localhost:" + strconv.Itoa(s.config.Port),
+ Handler: mux,
+ ReadTimeout: 1 * time.Second,
+ WriteTimeout: 1 * time.Second,
+ ReadHeaderTimeout: 1 * time.Second,
+ IdleTimeout: 30 * time.Second,
}
go server.ListenAndServe()
// signalhandler - takes over the main goroutine so when signal handler exists the whole program exits
- signalhandler.Run(signalSubscribers, shutdown, server)
+ signalhandler.Run(s.sugar, signalSubscribers, shutdown, server)
}
diff --git a/cmd/daemon/session-init.go b/cmd/daemon/session-init.go
index 27a1b275..86fdb921 100644
--- a/cmd/daemon/session-init.go
+++ b/cmd/daemon/session-init.go
@@ -2,37 +2,49 @@ package main
import (
"encoding/json"
- "io/ioutil"
- "log"
+ "io"
"net/http"
- "github.com/curusarn/resh/pkg/records"
+ "github.com/curusarn/resh/internal/recordint"
+ "go.uber.org/zap"
)
type sessionInitHandler struct {
- subscribers []chan records.Record
+ sugar *zap.SugaredLogger
+ subscribers []chan recordint.SessionInit
}
func (h *sessionInitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ sugar := h.sugar.With(zap.String("endpoint", "/session_init"))
+ sugar.Debugw("Handling request, sending response, reading body ...")
w.Write([]byte("OK\n"))
- jsn, err := ioutil.ReadAll(r.Body)
+ // TODO: should we somehow check for errors here?
+ jsn, err := io.ReadAll(r.Body)
// run rest of the handler as goroutine to prevent any hangups
go func() {
if err != nil {
- log.Println("Error reading the body", err)
+ sugar.Errorw("Error reading body", "error", err)
return
}
- record := records.Record{}
- err = json.Unmarshal(jsn, &record)
+ sugar.Debugw("Unmarshaling record ...")
+ rec := recordint.SessionInit{}
+ err = json.Unmarshal(jsn, &rec)
if err != nil {
- log.Println("Decoding error: ", err)
- log.Println("Payload: ", jsn)
+ sugar.Errorw("Error during unmarshaling",
+ "error", err,
+ "payload", jsn,
+ )
return
}
+ sugar := sugar.With(
+ "sessionID", rec.SessionID,
+ "sessionPID", rec.SessionPID,
+ )
+ sugar.Infow("Got session, sending to subscribers ...")
for _, sub := range h.subscribers {
- sub <- record
+ sub <- rec
}
- log.Println("/session_init - id:", record.SessionID, " - pid:", record.SessionPID)
+ sugar.Debugw("Session sent to subscribers")
}()
}
diff --git a/cmd/daemon/status.go b/cmd/daemon/status.go
index 599b8dd2..3c6b416a 100644
--- a/cmd/daemon/status.go
+++ b/cmd/daemon/status.go
@@ -2,16 +2,19 @@ package main
import (
"encoding/json"
- "log"
"net/http"
- "strconv"
- "github.com/curusarn/resh/pkg/httpclient"
- "github.com/curusarn/resh/pkg/msg"
+ "github.com/curusarn/resh/internal/msg"
+ "go.uber.org/zap"
)
-func statusHandler(w http.ResponseWriter, r *http.Request) {
- log.Println("/status START")
+type statusHandler struct {
+ sugar *zap.SugaredLogger
+}
+
+func (h *statusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ sugar := h.sugar.With(zap.String("endpoint", "/status"))
+ sugar.Debugw("Handling request ...")
resp := msg.StatusResponse{
Status: true,
Version: version,
@@ -19,23 +22,12 @@ func statusHandler(w http.ResponseWriter, r *http.Request) {
}
jsn, err := json.Marshal(&resp)
if err != nil {
- log.Println("Encoding error:", err)
- log.Println("Response:", resp)
+ sugar.Errorw("Error when marshaling",
+ "error", err,
+ "response", resp,
+ )
return
}
w.Write(jsn)
- log.Println("/status END")
-}
-
-func isDaemonRunning(port int) (bool, error) {
- url := "http://localhost:" + strconv.Itoa(port) + "/status"
- client := httpclient.New()
- resp, err := client.Get(url)
- if err != nil {
- log.Printf("Error while checking daemon status - "+
- "it's probably not running: %v\n", err)
- return false, err
- }
- defer resp.Body.Close()
- return true, nil
+ sugar.Infow("Request handled")
}
diff --git a/cmd/evaluate/main.go b/cmd/evaluate/main.go
deleted file mode 100644
index 5b4bda86..00000000
--- a/cmd/evaluate/main.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package main
-
-import (
- "flag"
- "fmt"
- "log"
- "os"
- "os/user"
- "path/filepath"
-
- "github.com/curusarn/resh/pkg/histanal"
- "github.com/curusarn/resh/pkg/strat"
-)
-
-// version from git set during build
-var version string
-
-// commit from git set during build
-var commit string
-
-func main() {
- const maxCandidates = 50
-
- usr, _ := user.Current()
- dir := usr.HomeDir
- historyPath := filepath.Join(dir, ".resh_history.json")
- historyPathBatchMode := filepath.Join(dir, "resh_history.json")
- sanitizedHistoryPath := filepath.Join(dir, "resh_history_sanitized.json")
- // tmpPath := "/tmp/resh-evaluate-tmp.json"
-
- showVersion := flag.Bool("version", false, "Show version and exit")
- showRevision := flag.Bool("revision", false, "Show git revision and exit")
- input := flag.String("input", "",
- "Input file (default: "+historyPath+" OR "+sanitizedHistoryPath+
- " depending on --sanitized-input option)")
- // outputDir := flag.String("output", "/tmp/resh-evaluate", "Output directory")
- sanitizedInput := flag.Bool("sanitized-input", false,
- "Handle input as sanitized (also changes default value for input argument)")
- plottingScript := flag.String("plotting-script", "resh-evaluate-plot.py", "Script to use for plotting")
- inputDataRoot := flag.String("input-data-root", "",
- "Input data root, enables batch mode, looks for files matching --input option")
- slow := flag.Bool("slow", false,
- "Enables strategies that takes a long time (e.g. markov chain strategies).")
- skipFailedCmds := flag.Bool("skip-failed-cmds", false,
- "Skips records with non-zero exit status.")
- debugRecords := flag.Float64("debug", 0, "Debug records - percentage of records that should be debugged.")
-
- flag.Parse()
-
- // handle show{Version,Revision} options
- if *showVersion == true {
- fmt.Println(version)
- os.Exit(0)
- }
- if *showRevision == true {
- fmt.Println(commit)
- os.Exit(0)
- }
-
- // handle batch mode
- batchMode := false
- if *inputDataRoot != "" {
- batchMode = true
- }
- // set default input
- if *input == "" {
- if *sanitizedInput {
- *input = sanitizedHistoryPath
- } else if batchMode {
- *input = historyPathBatchMode
- } else {
- *input = historyPath
- }
- }
-
- var evaluator histanal.HistEval
- if batchMode {
- evaluator = histanal.NewHistEvalBatchMode(*input, *inputDataRoot, maxCandidates, *skipFailedCmds, *debugRecords, *sanitizedInput)
- } else {
- evaluator = histanal.NewHistEval(*input, maxCandidates, *skipFailedCmds, *debugRecords, *sanitizedInput)
- }
-
- var simpleStrategies []strat.ISimpleStrategy
- var strategies []strat.IStrategy
-
- // dummy := strategyDummy{}
- // simpleStrategies = append(simpleStrategies, &dummy)
-
- simpleStrategies = append(simpleStrategies, &strat.Recent{})
-
- // frequent := strategyFrequent{}
- // frequent.init()
- // simpleStrategies = append(simpleStrategies, &frequent)
-
- // random := strategyRandom{candidatesSize: maxCandidates}
- // random.init()
- // simpleStrategies = append(simpleStrategies, &random)
-
- directory := strat.DirectorySensitive{}
- directory.Init()
- simpleStrategies = append(simpleStrategies, &directory)
-
- // dynamicDistG := strat.DynamicRecordDistance{
- // MaxDepth: 3000,
- // DistParams: records.DistParams{Pwd: 10, RealPwd: 10, SessionID: 1, Time: 1, Git: 10},
- // Label: "10*pwd,10*realpwd,session,time,10*git",
- // }
- // dynamicDistG.Init()
- // strategies = append(strategies, &dynamicDistG)
-
- // NOTE: this is the decent one !!!
- // distanceStaticBest := strat.RecordDistance{
- // MaxDepth: 3000,
- // DistParams: records.DistParams{Pwd: 10, RealPwd: 10, SessionID: 1, Time: 1},
- // Label: "10*pwd,10*realpwd,session,time",
- // }
- // strategies = append(strategies, &distanceStaticBest)
-
- recentBash := strat.RecentBash{}
- recentBash.Init()
- strategies = append(strategies, &recentBash)
-
- if *slow {
-
- markovCmd := strat.MarkovChainCmd{Order: 1}
- markovCmd.Init()
-
- markovCmd2 := strat.MarkovChainCmd{Order: 2}
- markovCmd2.Init()
-
- markov := strat.MarkovChain{Order: 1}
- markov.Init()
-
- markov2 := strat.MarkovChain{Order: 2}
- markov2.Init()
-
- simpleStrategies = append(simpleStrategies, &markovCmd2, &markovCmd, &markov2, &markov)
- }
-
- for _, strategy := range simpleStrategies {
- strategies = append(strategies, strat.NewSimpleStrategyWrapper(strategy))
- }
-
- for _, strat := range strategies {
- err := evaluator.Evaluate(strat)
- if err != nil {
- log.Println("Evaluator evaluate() error:", err)
- }
- }
-
- evaluator.CalculateStatsAndPlot(*plottingScript)
-}
diff --git a/cmd/event/main.go b/cmd/event/main.go
deleted file mode 100644
index fe3cb721..00000000
--- a/cmd/event/main.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package main
-
-import "fmt"
-
-func main() {
- fmt.Println("Hell world")
-}
diff --git a/cmd/generate-uuid/main.go b/cmd/generate-uuid/main.go
new file mode 100644
index 00000000..8c238a90
--- /dev/null
+++ b/cmd/generate-uuid/main.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/google/uuid"
+)
+
+// Small utility to generate UUID's using google/uuid golang package
+// Doesn't check arguments
+// Exits with status 1 on error
+func main() {
+ rnd, err := uuid.NewRandom()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "ERROR: could not get new random source: %v", err)
+ os.Exit(1)
+ }
+ id := rnd.String()
+ if id == "" {
+ fmt.Fprintf(os.Stderr, "ERROR: got invalid UUID from package")
+ os.Exit(1)
+ }
+ // No newline
+ fmt.Print(id)
+}
diff --git a/cmd/get-epochtime/main.go b/cmd/get-epochtime/main.go
new file mode 100644
index 00000000..2000414c
--- /dev/null
+++ b/cmd/get-epochtime/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/curusarn/resh/internal/epochtime"
+)
+
+// Small utility to get epochtime in millisecond precision
+// Doesn't check arguments
+// Exits with status 1 on error
+func main() {
+ fmt.Printf("%s", epochtime.Now())
+}
diff --git a/cmd/inspect/main.go b/cmd/inspect/main.go
deleted file mode 100644
index 7b76a40c..00000000
--- a/cmd/inspect/main.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package main
-
-import (
- "bytes"
- "encoding/json"
- "flag"
- "fmt"
- "io/ioutil"
- "log"
- "net/http"
- "time"
-
- "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
-
-// commit from git set during build
-var commit 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{
- Timeout: 3 * time.Second,
- }
- resp, err := client.Do(req)
- if err != nil {
- log.Fatal("resh-daemon is not running - try restarting this terminal")
- }
-
- 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/install-utils/device.go b/cmd/install-utils/device.go
new file mode 100644
index 00000000..903e69f8
--- /dev/null
+++ b/cmd/install-utils/device.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/curusarn/resh/internal/datadir"
+ "github.com/curusarn/resh/internal/device"
+ "github.com/curusarn/resh/internal/output"
+)
+
+func setupDevice(out *output.Output) {
+ dataDir, err := datadir.MakePath()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "ERROR: Failed to get/setup data directory: %v\n", err)
+ os.Exit(1)
+ }
+ err = device.SetupName(out, dataDir)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "ERROR: Failed to check/setup device name: %v\n", err)
+ os.Exit(1)
+ }
+ err = device.SetupID(dataDir)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "ERROR: Failed to check/setup device ID: %v\n", err)
+ os.Exit(1)
+ }
+}
diff --git a/cmd/install-utils/main.go b/cmd/install-utils/main.go
new file mode 100644
index 00000000..4b72676f
--- /dev/null
+++ b/cmd/install-utils/main.go
@@ -0,0 +1,67 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/logger"
+ "github.com/curusarn/resh/internal/output"
+ "go.uber.org/zap"
+)
+
+// info passed during build
+var version string
+var commit string
+var development string
+
+func main() {
+ config, errCfg := cfg.New()
+ logger, err := logger.New("install-utils", config.LogLevel, development)
+ if err != nil {
+ fmt.Printf("Error while creating logger: %v", err)
+ }
+ defer logger.Sync() // flushes buffer, if any
+ if errCfg != nil {
+ logger.Error("Error while getting configuration", zap.Error(errCfg))
+ }
+ sugar := logger.Sugar()
+ sugar.Infow("Install-utils invoked ...",
+ "version", version,
+ "commit", commit,
+ )
+ out := output.New(logger, "install-utils ERROR")
+
+ if len(os.Args) < 2 {
+ out.Error("ERROR: Not enough arguments\n")
+ printUsage(os.Stderr)
+ os.Exit(1)
+ }
+ command := os.Args[1]
+ switch command {
+ case "setup-device":
+ setupDevice(out)
+ case "migrate-all":
+ migrateAll(out)
+ case "help":
+ printUsage(os.Stdout)
+ default:
+ out.Error(fmt.Sprintf("ERROR: Unknown command: %s\n", command))
+ printUsage(os.Stderr)
+ os.Exit(1)
+ }
+}
+
+func printUsage(f *os.File) {
+ usage := `
+USAGE: ./install-utils COMMAND
+Utils used during RESH installation.
+
+COMMANDS:
+ setup-device setup device name and device ID
+ migrate-all update config and history to latest format
+ help show this help
+
+`
+ fmt.Fprint(f, usage)
+}
diff --git a/cmd/install-utils/migrate.go b/cmd/install-utils/migrate.go
new file mode 100644
index 00000000..07a06664
--- /dev/null
+++ b/cmd/install-utils/migrate.go
@@ -0,0 +1,195 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path"
+
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/datadir"
+ "github.com/curusarn/resh/internal/futil"
+ "github.com/curusarn/resh/internal/output"
+ "github.com/curusarn/resh/internal/recio"
+)
+
+func printRecoveryInfo(rf *futil.RestorableFile) {
+ fmt.Printf(" -> Backup is '%s'\n"+
+ " -> Original file location is '%s'\n"+
+ " -> Please copy the backup over the file - run: cp -f '%s' '%s'\n\n",
+ rf.PathBackup, rf.Path,
+ rf.PathBackup, rf.Path,
+ )
+}
+
+func migrateAll(out *output.Output) {
+ cfgBackup, err := migrateConfig(out)
+ if err != nil {
+ // out.InfoE("Failed to update config file format", err)
+ out.FatalE("Failed to update config file format", err)
+ }
+ err = migrateHistory(out)
+ if err != nil {
+ errHist := err
+ out.InfoE("Failed to update RESH history", errHist)
+ out.Info("Restoring config from backup ...")
+ err = cfgBackup.Restore()
+ if err != nil {
+ out.InfoE("FAILED TO RESTORE CONFIG FROM BACKUP!", err)
+ printRecoveryInfo(cfgBackup)
+ } else {
+ out.Info("Config file was restored successfully")
+ }
+ out.FatalE("Failed to update history", errHist)
+ }
+}
+
+func migrateConfig(out *output.Output) (*futil.RestorableFile, error) {
+ cfgPath, err := cfg.GetPath()
+ if err != nil {
+ return nil, fmt.Errorf("could not get config file path: %w", err)
+ }
+
+ // Touch config to get rid of edge-cases
+ created, err := futil.TouchFile(cfgPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to touch config file: %w", err)
+ }
+
+ // Backup
+ backup, err := futil.BackupFile(cfgPath)
+ if err != nil {
+ return nil, fmt.Errorf("could not backup config file: %w", err)
+ }
+
+ // Migrate
+ changes, err := cfg.Migrate()
+ if err != nil {
+ // Restore
+ errMigrate := err
+ errMigrateWrap := fmt.Errorf("failed to update config file: %w", errMigrate)
+ out.InfoE("Failed to update config file format", errMigrate)
+ out.Info("Restoring config from backup ...")
+ err = backup.Restore()
+ if err != nil {
+ out.InfoE("FAILED TO RESTORE CONFIG FROM BACKUP!", err)
+ printRecoveryInfo(backup)
+ } else {
+ out.Info("Config file was restored successfully")
+ }
+ // We are returning the root cause - there might be a better solution how to report the errors
+ return nil, errMigrateWrap
+ }
+ if created {
+ out.Info(fmt.Sprintf("RESH config created in '%s'", cfgPath))
+ } else if changes {
+ out.Info("RESH config file format has changed since last update - your config was updated to reflect the changes.")
+ }
+ return backup, nil
+}
+
+func migrateHistory(out *output.Output) error {
+ err := migrateHistoryLocation(out)
+ if err != nil {
+ return fmt.Errorf("failed to move history to new location %w", err)
+ }
+ return migrateHistoryFormat(out)
+}
+
+// Find first existing history and use it
+// Don't bother with merging of history in multiple locations - it could get messy and it shouldn't be necessary
+func migrateHistoryLocation(out *output.Output) error {
+ dataDir, err := datadir.MakePath()
+ if err != nil {
+ return fmt.Errorf("failed to get data directory: %w", err)
+ }
+ historyPath := path.Join(dataDir, datadir.HistoryFileName)
+
+ exists, err := futil.FileExists(historyPath)
+ if err != nil {
+ return fmt.Errorf("failed to check history file: %w", err)
+ }
+ if exists {
+ // TODO: get rid of this output (later)
+ out.Info(fmt.Sprintf("Found history file in '%s' - nothing to move", historyPath))
+ return nil
+ }
+
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return fmt.Errorf("failed to get user home directory: %w", err)
+ }
+
+ legacyHistoryPaths := []string{
+ path.Join(homeDir, ".resh_history.json"),
+ path.Join(homeDir, ".resh/history.json"),
+ }
+ for _, path := range legacyHistoryPaths {
+ exists, err = futil.FileExists(path)
+ if err != nil {
+ return fmt.Errorf("failed to check existence of legacy history file: %w", err)
+ }
+ if exists {
+ // TODO: maybe get rid of this output later
+ out.Info(fmt.Sprintf("Copying history file to new location: '%s' -> '%s' ...", path, historyPath))
+ err = futil.CopyFile(path, historyPath)
+ if err != nil {
+ return fmt.Errorf("failed to copy history file: %w", err)
+ }
+ out.Info("History file copied successfully")
+ return nil
+ }
+ }
+ // out.Info("WARNING: No RESH history file found (this is normal during new installation)")
+ return nil
+}
+
+func migrateHistoryFormat(out *output.Output) error {
+ dataDir, err := datadir.MakePath()
+ if err != nil {
+ return fmt.Errorf("could not get user data directory: %w", err)
+ }
+ historyPath := path.Join(dataDir, datadir.HistoryFileName)
+
+ exists, err := futil.FileExists(historyPath)
+ if err != nil {
+ return fmt.Errorf("failed to check existence of history file: %w", err)
+ }
+ if !exists {
+ out.Error("There is no RESH history file - this is normal if you are installing RESH for the first time on this device")
+ _, err = futil.TouchFile(historyPath)
+ if err != nil {
+ return fmt.Errorf("failed to touch history file: %w", err)
+ }
+ return nil
+ }
+
+ backup, err := futil.BackupFile(historyPath)
+ if err != nil {
+ return fmt.Errorf("could not back up history file: %w", err)
+ }
+
+ rio := recio.New(out.Logger.Sugar())
+
+ recs, err := rio.ReadAndFixFile(historyPath, 3)
+ if err != nil {
+ return fmt.Errorf("could not load history file: %w", err)
+ }
+ err = rio.OverwriteFile(historyPath, recs)
+ if err != nil {
+ // Restore
+ errMigrate := err
+ errMigrateWrap := fmt.Errorf("failed to update format of history file: %w", errMigrate)
+ out.InfoE("Failed to update RESH history file format", errMigrate)
+ out.Info("Restoring RESH history from backup ...")
+ err = backup.Restore()
+ if err != nil {
+ out.InfoE("FAILED TO RESTORE RESH HISTORY FROM BACKUP!", err)
+ printRecoveryInfo(backup)
+ } else {
+ out.Info("RESH history file was restored successfully")
+ }
+ // We are returning the root cause - there might be a better solution how to report the errors
+ return errMigrateWrap
+ }
+ return nil
+}
diff --git a/cmd/postcollect/main.go b/cmd/postcollect/main.go
index b5cb6b04..e67e93ba 100644
--- a/cmd/postcollect/main.go
+++ b/cmd/postcollect/main.go
@@ -1,154 +1,74 @@
package main
import (
- "flag"
"fmt"
- "log"
"os"
- "github.com/BurntSushi/toml"
- "github.com/curusarn/resh/pkg/cfg"
- "github.com/curusarn/resh/pkg/collect"
- "github.com/curusarn/resh/pkg/records"
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/collect"
+ "github.com/curusarn/resh/internal/logger"
+ "github.com/curusarn/resh/internal/opt"
+ "github.com/curusarn/resh/internal/output"
+ "github.com/curusarn/resh/internal/recordint"
+ "github.com/curusarn/resh/record"
+ "github.com/spf13/pflag"
+ "go.uber.org/zap"
- // "os/exec"
- "os/user"
- "path/filepath"
"strconv"
)
-// version from git set during build
+// info passed during build
var version string
-
-// commit from git set during build
var commit string
+var development string
func main() {
- usr, _ := user.Current()
- dir := usr.HomeDir
- configPath := filepath.Join(dir, "/.config/resh.toml")
- reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid")
-
- machineIDPath := "/etc/machine-id"
-
- var config cfg.Config
- if _, err := toml.DecodeFile(configPath, &config); err != nil {
- log.Fatal("Error reading config:", err)
+ config, errCfg := cfg.New()
+ logger, err := logger.New("postcollect", config.LogLevel, development)
+ if err != nil {
+ fmt.Printf("Error while creating logger: %v", err)
}
- 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")
-
- cmdLine := flag.String("cmdLine", "", "command line")
- exitCode := flag.Int("exitCode", -1, "exit code")
- sessionID := flag.String("sessionId", "", "resh generated session id")
- recordID := flag.String("recordId", "", "resh generated record id")
-
- shlvl := flag.Int("shlvl", -1, "$SHLVL")
- shell := flag.String("shell", "", "actual shell")
-
- // posix variables
- pwdAfter := flag.String("pwdAfter", "", "$PWD after command")
-
- // non-posix
- // sessionPid := flag.Int("sessionPid", -1, "$$ at session start")
-
- gitCdupAfter := flag.String("gitCdupAfter", "", "git rev-parse --show-cdup")
- gitRemoteAfter := flag.String("gitRemoteAfter", "", "git remote get-url origin")
-
- gitCdupExitCodeAfter := flag.Int("gitCdupExitCodeAfter", -1, "... $?")
- gitRemoteExitCodeAfter := flag.Int("gitRemoteExitCodeAfter", -1, "... $?")
+ defer logger.Sync() // flushes buffer, if any
+ if errCfg != nil {
+ logger.Error("Error while getting configuration", zap.Error(errCfg))
+ }
+ out := output.New(logger, "resh-postcollect ERROR")
- // before after
- timezoneAfter := flag.String("timezoneAfter", "", "")
+ args := opt.HandleVersionOpts(out, os.Args, version, commit)
- rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME")
- rta := flag.String("realtimeAfter", "-1", "after $EPOCHREALTIME")
- flag.Parse()
+ flags := pflag.NewFlagSet("", pflag.ExitOnError)
+ exitCode := flags.Int("exit-code", -1, "Exit code")
+ sessionID := flags.String("session-id", "", "Resh generated session ID")
+ recordID := flags.String("record-id", "", "Resh generated record ID")
+ shlvl := flags.Int("shlvl", -1, "$SHLVL")
+ rtb := flags.String("time-before", "-1", "Before $EPOCHREALTIME")
+ rta := flags.String("time-after", "-1", "After $EPOCHREALTIME")
+ flags.Parse(args)
- if *showVersion == true {
- fmt.Println(version)
- os.Exit(0)
- }
- if *showRevision == true {
- fmt.Println(commit)
- os.Exit(0)
- }
- if *requireVersion != "" && *requireVersion != version {
- fmt.Println("Please restart/reload this terminal session " +
- "(resh version: " + version +
- "; resh version of this terminal session: " + *requireVersion +
- ")")
- os.Exit(3)
- }
- if *requireRevision != "" && *requireRevision != commit {
- fmt.Println("Please restart/reload this terminal session " +
- "(resh revision: " + commit +
- "; resh revision of this terminal session: " + *requireRevision +
- ")")
- os.Exit(3)
- }
- realtimeAfter, err := strconv.ParseFloat(*rta, 64)
+ timeAfter, err := strconv.ParseFloat(*rta, 64)
if err != nil {
- log.Fatal("Flag Parsing error (rta):", err)
+ out.FatalE("Error while parsing flag --time-after", err)
}
- realtimeBefore, err := strconv.ParseFloat(*rtb, 64)
+ timeBefore, err := strconv.ParseFloat(*rtb, 64)
if err != nil {
- log.Fatal("Flag Parsing error (rtb):", err)
+ out.FatalE("Error while parsing flag --time-before", err)
}
- realtimeDuration := realtimeAfter - realtimeBefore
-
- timezoneAfterOffset := collect.GetTimezoneOffsetInSeconds(*timezoneAfter)
- realtimeAfterLocal := realtimeAfter + timezoneAfterOffset
+ duration := timeAfter - timeBefore
- realPwdAfter, err := filepath.EvalSymlinks(*pwdAfter)
- if err != nil {
- log.Println("err while handling pwdAfter realpath:", err)
- realPwdAfter = ""
- }
+ // FIXME: use recordint.Postcollect
+ rec := recordint.Collect{
+ SessionID: *sessionID,
+ Shlvl: *shlvl,
- gitDirAfter, gitRealDirAfter := collect.GetGitDirs(*gitCdupAfter, *gitCdupExitCodeAfter, *pwdAfter)
- if *gitRemoteExitCodeAfter != 0 {
- *gitRemoteAfter = ""
- }
-
- rec := records.Record{
- // core
- BaseRecord: records.BaseRecord{
- CmdLine: *cmdLine,
- ExitCode: *exitCode,
- SessionID: *sessionID,
+ Rec: record.V1{
RecordID: *recordID,
- Shlvl: *shlvl,
- Shell: *shell,
-
- PwdAfter: *pwdAfter,
-
- // non-posix
- RealPwdAfter: realPwdAfter,
-
- // before after
- TimezoneAfter: *timezoneAfter,
-
- RealtimeBefore: realtimeBefore,
- RealtimeAfter: realtimeAfter,
- RealtimeAfterLocal: realtimeAfterLocal,
-
- RealtimeDuration: realtimeDuration,
-
- GitDirAfter: gitDirAfter,
- GitRealDirAfter: gitRealDirAfter,
- GitOriginRemoteAfter: *gitRemoteAfter,
- MachineID: collect.ReadFileContent(machineIDPath),
+ SessionID: *sessionID,
- PartOne: false,
+ ExitCode: *exitCode,
+ Duration: fmt.Sprintf("%.4f", duration),
- ReshUUID: collect.ReadFileContent(reshUUIDPath),
- ReshVersion: version,
- ReshRevision: commit,
+ PartsNotMerged: true,
},
}
- collect.SendRecord(rec, strconv.Itoa(config.Port), "/record")
+ collect.SendRecord(out, rec, strconv.Itoa(config.Port), "/record")
}
diff --git a/cmd/sanitize/main.go b/cmd/sanitize/main.go
deleted file mode 100644
index c4fd60a7..00000000
--- a/cmd/sanitize/main.go
+++ /dev/null
@@ -1,523 +0,0 @@
-package main
-
-import (
- "bufio"
- "crypto/sha256"
- "encoding/binary"
- "encoding/hex"
- "encoding/json"
- "errors"
- "flag"
- "fmt"
- "log"
- "math"
- "net/url"
- "os"
- "os/user"
- "path"
- "path/filepath"
- "strconv"
- "strings"
- "unicode"
-
- "github.com/coreos/go-semver/semver"
- "github.com/curusarn/resh/pkg/records"
- giturls "github.com/whilp/git-urls"
-)
-
-// version from git set during build
-var version string
-
-// commit from git set during build
-var commit string
-
-func main() {
- usr, _ := user.Current()
- dir := usr.HomeDir
- historyPath := filepath.Join(dir, ".resh_history.json")
- // outputPath := filepath.Join(dir, "resh_history_sanitized.json")
- sanitizerDataPath := filepath.Join(dir, ".resh", "sanitizer_data")
-
- showVersion := flag.Bool("version", false, "Show version and exit")
- showRevision := flag.Bool("revision", false, "Show git revision and exit")
- trimHashes := flag.Int("trim-hashes", 12, "Trim hashes to N characters, '0' turns off trimming")
- inputPath := flag.String("input", historyPath, "Input file")
- outputPath := flag.String("output", "", "Output file (default: use stdout)")
-
- flag.Parse()
-
- if *showVersion == true {
- fmt.Println(version)
- os.Exit(0)
- }
- if *showRevision == true {
- fmt.Println(commit)
- os.Exit(0)
- }
- sanitizer := sanitizer{hashLength: *trimHashes}
- err := sanitizer.init(sanitizerDataPath)
- if err != nil {
- log.Fatal("Sanitizer init() error:", err)
- }
-
- inputFile, err := os.Open(*inputPath)
- if err != nil {
- log.Fatal("Open() resh history file error:", err)
- }
- defer inputFile.Close()
-
- var writer *bufio.Writer
- if *outputPath == "" {
- writer = bufio.NewWriter(os.Stdout)
- } else {
- outputFile, err := os.Create(*outputPath)
- if err != nil {
- log.Fatal("Create() output file error:", err)
- }
- defer outputFile.Close()
- writer = bufio.NewWriter(outputFile)
- }
- defer writer.Flush()
-
- scanner := bufio.NewScanner(inputFile)
- for scanner.Scan() {
- record := records.Record{}
- fallbackRecord := records.FallbackRecord{}
- line := scanner.Text()
- err = json.Unmarshal([]byte(line), &record)
- if err != nil {
- err = json.Unmarshal([]byte(line), &fallbackRecord)
- if err != nil {
- log.Println("Line:", line)
- log.Fatal("Decoding error:", err)
- }
- record = records.Convert(&fallbackRecord)
- }
- err = sanitizer.sanitizeRecord(&record)
- if err != nil {
- log.Println("Line:", line)
- log.Fatal("Sanitization error:", err)
- }
- outLine, err := json.Marshal(&record)
- if err != nil {
- log.Println("Line:", line)
- log.Fatal("Encoding error:", err)
- }
- // fmt.Println(string(outLine))
- n, err := writer.WriteString(string(outLine) + "\n")
- if err != nil {
- log.Fatal(err)
- }
- if n == 0 {
- log.Fatal("Nothing was written", n)
- }
- }
-}
-
-type sanitizer struct {
- hashLength int
- whitelist map[string]bool
-}
-
-func (s *sanitizer) init(dataPath string) error {
- globalData := path.Join(dataPath, "whitelist.txt")
- s.whitelist = loadData(globalData)
- return nil
-}
-
-func loadData(fname string) map[string]bool {
- file, err := os.Open(fname)
- if err != nil {
- log.Fatal("Open() file error:", err)
- }
- defer file.Close()
-
- scanner := bufio.NewScanner(file)
- data := make(map[string]bool)
- for scanner.Scan() {
- line := scanner.Text()
- data[line] = true
- }
- return data
-}
-
-func (s *sanitizer) sanitizeRecord(record *records.Record) error {
- // hash directories of the paths
- record.Pwd = s.sanitizePath(record.Pwd)
- record.RealPwd = s.sanitizePath(record.RealPwd)
- record.PwdAfter = s.sanitizePath(record.PwdAfter)
- record.RealPwdAfter = s.sanitizePath(record.RealPwdAfter)
- record.GitDir = s.sanitizePath(record.GitDir)
- record.GitDirAfter = s.sanitizePath(record.GitDirAfter)
- record.GitRealDir = s.sanitizePath(record.GitRealDir)
- record.GitRealDirAfter = s.sanitizePath(record.GitRealDirAfter)
- record.Home = s.sanitizePath(record.Home)
- record.ShellEnv = s.sanitizePath(record.ShellEnv)
-
- // hash the most sensitive info, do not tokenize
- record.Host = s.hashToken(record.Host)
- record.Login = s.hashToken(record.Login)
- record.MachineID = s.hashToken(record.MachineID)
-
- var err error
- // this changes git url a bit but I'm still happy with the result
- // e.g. "git@github.com:curusarn/resh" becomes "ssh://git@github.com/3385162f14d7/5a7b2909005c"
- // notice the "ssh://" prefix
- record.GitOriginRemote, err = s.sanitizeGitURL(record.GitOriginRemote)
- if err != nil {
- log.Println("Error while snitizing GitOriginRemote url", record.GitOriginRemote, ":", err)
- return err
- }
- record.GitOriginRemoteAfter, err = s.sanitizeGitURL(record.GitOriginRemoteAfter)
- if err != nil {
- log.Println("Error while snitizing GitOriginRemoteAfter url", record.GitOriginRemoteAfter, ":", err)
- return err
- }
-
- // sanitization destroys original CmdLine length -> save it
- record.CmdLength = len(record.CmdLine)
-
- record.CmdLine, err = s.sanitizeCmdLine(record.CmdLine)
- if err != nil {
- log.Fatal("Cmd:", record.CmdLine, "; sanitization error:", err)
- }
- record.RecallLastCmdLine, err = s.sanitizeCmdLine(record.RecallLastCmdLine)
- if err != nil {
- log.Fatal("RecallLastCmdLine:", record.RecallLastCmdLine, "; sanitization error:", err)
- }
-
- if len(record.RecallActionsRaw) > 0 {
- record.RecallActionsRaw, err = s.sanitizeRecallActions(record.RecallActionsRaw, record.ReshVersion)
- if err != nil {
- log.Println("RecallActionsRaw:", record.RecallActionsRaw, "; sanitization error:", err)
- }
- }
- // add a flag to signify that the record has been sanitized
- record.Sanitized = true
- return nil
-}
-
-func fixSeparator(str string) string {
- if len(str) > 0 && str[0] == ';' {
- return "|||" + str[1:]
- }
- return str
-}
-
-func minIndex(str string, substrs []string) (idx, substrIdx int) {
- minMatch := math.MaxInt32
- for i, sep := range substrs {
- match := strings.Index(str, sep)
- if match != -1 && match < minMatch {
- minMatch = match
- substrIdx = i
- }
- }
- idx = minMatch
- return
-}
-
-// sanitizes the recall actions by replacing the recall prefix with it's length
-func (s *sanitizer) sanitizeRecallActions(str string, reshVersion string) (string, error) {
- if len(str) == 0 {
- return "", nil
- }
- var separators []string
- seps := []string{"|||"}
- refVersion, err := semver.NewVersion("2.5.14")
- if err != nil {
- return str, fmt.Errorf("sanitizeRecallActions: semver error: %s", err.Error())
- }
- if len(reshVersion) == 0 {
- return str, errors.New("sanitizeRecallActions: record.ReshVersion is an empty string")
- }
- if reshVersion == "dev" {
- reshVersion = "0.0.0"
- }
- if reshVersion[0] == 'v' {
- reshVersion = reshVersion[1:]
- }
- recordVersion, err := semver.NewVersion(reshVersion)
- if err != nil {
- return str, fmt.Errorf("sanitizeRecallActions: semver error: %s; version string: %s", err.Error(), reshVersion)
- }
- if recordVersion.LessThan(*refVersion) {
- seps = append(seps, ";")
- }
-
- actions := []string{"arrow_up", "arrow_down", "control_R"}
- for _, sep := range seps {
- for _, action := range actions {
- separators = append(separators, sep+action+":")
- }
- }
- /*
- - find any of {|||,;}{arrow_up,arrow_down,control_R}: in the recallActions (on the lowest index)
- - use found substring to parse out the next prefix
- - sanitize prefix
- - add fixed substring and sanitized prefix to output
- */
- doBreak := false
- sanStr := ""
- idx := 0
- var currSeparator string
- tokenLen, sepIdx := minIndex(str, separators)
- if tokenLen != 0 {
- return str, errors.New("sanitizeReacallActions: unexpected string before first action/separator")
- }
- currSeparator = separators[sepIdx]
- idx += len(currSeparator)
- for !doBreak {
- tokenLen, sepIdx := minIndex(str[idx:], separators)
- if tokenLen > len(str[idx:]) {
- tokenLen = len(str[idx:])
- doBreak = true
- }
- // token := str[idx : idx+tokenLen]
- sanStr += fixSeparator(currSeparator) + strconv.Itoa(tokenLen)
- currSeparator = separators[sepIdx]
- idx += tokenLen + len(currSeparator)
- }
- 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
- sanCmdLine := ""
- buff := ""
-
- // simple options shouldn't be sanitized
- // 1) whitespace 2) "-" or "--" 3) letters, digits, "-", "_" 4) ending whitespace or any of "=;)"
- var optionDetected bool
-
- prevR3 := ' '
- prevR2 := ' '
- prevR := ' '
- for _, r := range cmdLine {
- switch optionDetected {
- case true:
- if unicode.IsSpace(r) || strings.ContainsRune(optionEndingChars, r) {
- // whitespace or option ends the option
- // => add option unsanitized
- optionDetected = false
- if len(buff) > 0 {
- sanCmdLine += buff
- buff = ""
- }
- sanCmdLine += string(r)
- } else if unicode.IsLetter(r) == false && unicode.IsDigit(r) == false &&
- strings.ContainsRune(optionAllowedChars, r) == false {
- // r is not any of allowed chars for an option: letter, digit, "-" or "_"
- // => sanitize
- if len(buff) > 0 {
- sanToken, err := s.sanitizeCmdToken(buff)
- if err != nil {
- log.Println("WARN: got error while sanitizing cmdLine:", cmdLine)
- // return cmdLine, err
- }
- sanCmdLine += sanToken
- buff = ""
- }
- sanCmdLine += string(r)
- } else {
- buff += string(r)
- }
- case false:
- // split command on all non-letter and non-digit characters
- if unicode.IsLetter(r) == false && unicode.IsDigit(r) == false {
- // split token
- if len(buff) > 0 {
- sanToken, err := s.sanitizeCmdToken(buff)
- if err != nil {
- log.Println("WARN: got error while sanitizing cmdLine:", cmdLine)
- // return cmdLine, err
- }
- sanCmdLine += sanToken
- buff = ""
- }
- sanCmdLine += string(r)
- } else {
- if (unicode.IsSpace(prevR2) && prevR == '-') ||
- (unicode.IsSpace(prevR3) && prevR2 == '-' && prevR == '-') {
- optionDetected = true
- }
- buff += string(r)
- }
- }
- prevR3 = prevR2
- prevR2 = prevR
- prevR = r
- }
- if len(buff) <= 0 {
- // nothing in the buffer => work is done
- return sanCmdLine, nil
- }
- if optionDetected {
- // option detected => dont sanitize
- sanCmdLine += buff
- return sanCmdLine, nil
- }
- // sanitize
- sanToken, err := s.sanitizeCmdToken(buff)
- if err != nil {
- log.Println("WARN: got error while sanitizing cmdLine:", cmdLine)
- // return cmdLine, err
- }
- sanCmdLine += sanToken
- return sanCmdLine, nil
-}
-
-func (s *sanitizer) sanitizeGitURL(rawURL string) (string, error) {
- if len(rawURL) <= 0 {
- return rawURL, nil
- }
- parsedURL, err := giturls.Parse(rawURL)
- if err != nil {
- return rawURL, err
- }
- return s.sanitizeParsedURL(parsedURL)
-}
-
-func (s *sanitizer) sanitizeURL(rawURL string) (string, error) {
- if len(rawURL) <= 0 {
- return rawURL, nil
- }
- parsedURL, err := url.Parse(rawURL)
- if err != nil {
- return rawURL, err
- }
- return s.sanitizeParsedURL(parsedURL)
-}
-
-func (s *sanitizer) sanitizeParsedURL(parsedURL *url.URL) (string, error) {
- parsedURL.Opaque = s.sanitizeToken(parsedURL.Opaque)
-
- userinfo := parsedURL.User.Username() // only get username => password won't even make it to the sanitized data
- if len(userinfo) > 0 {
- parsedURL.User = url.User(s.sanitizeToken(userinfo))
- } else {
- // we need to do this because `gitUrls.Parse()` sets `User` to `url.User("")` instead of `nil`
- parsedURL.User = nil
- }
- var err error
- parsedURL.Host, err = s.sanitizeTwoPartToken(parsedURL.Host, ":")
- if err != nil {
- return parsedURL.String(), err
- }
- parsedURL.Path = s.sanitizePath(parsedURL.Path)
- // ForceQuery bool
- parsedURL.RawQuery = s.sanitizeToken(parsedURL.RawQuery)
- parsedURL.Fragment = s.sanitizeToken(parsedURL.Fragment)
-
- return parsedURL.String(), nil
-}
-
-func (s *sanitizer) sanitizePath(path string) string {
- var sanPath string
- for _, token := range strings.Split(path, "/") {
- if s.whitelist[token] != true {
- token = s.hashToken(token)
- }
- sanPath += token + "/"
- }
- if len(sanPath) > 0 {
- sanPath = sanPath[:len(sanPath)-1]
- }
- return sanPath
-}
-
-func (s *sanitizer) sanitizeTwoPartToken(token string, delimeter string) (string, error) {
- tokenParts := strings.Split(token, delimeter)
- if len(tokenParts) <= 1 {
- return s.sanitizeToken(token), nil
- }
- if len(tokenParts) == 2 {
- return s.sanitizeToken(tokenParts[0]) + delimeter + s.sanitizeToken(tokenParts[1]), nil
- }
- return token, errors.New("Token has more than two parts")
-}
-
-func (s *sanitizer) sanitizeCmdToken(token string) (string, error) {
- // there shouldn't be tokens with letters or digits mixed together with symbols
- if len(token) <= 1 {
- // NOTE: do not sanitize single letter tokens
- return token, nil
- }
- if s.isInWhitelist(token) == true {
- return token, nil
- }
-
- isLettersOrDigits := true
- // isDigits := true
- isOtherCharacters := true
- for _, r := range token {
- if unicode.IsDigit(r) == false && unicode.IsLetter(r) == false {
- isLettersOrDigits = false
- // isDigits = false
- }
- // if unicode.IsDigit(r) == false {
- // isDigits = false
- // }
- if unicode.IsDigit(r) || unicode.IsLetter(r) {
- isOtherCharacters = false
- }
- }
- // NOTE: I decided that I don't want a special sanitization for numbers
- // if isDigits {
- // return s.hashNumericToken(token), nil
- // }
- if isLettersOrDigits {
- return s.hashToken(token), nil
- }
- if isOtherCharacters {
- return token, nil
- }
- log.Println("WARN: cmd token is made of mix of letters or digits and other characters; token:", token)
- // return token, errors.New("cmd token is made of mix of letters or digits and other characters")
- return s.hashToken(token), errors.New("cmd token is made of mix of letters or digits and other characters")
-}
-
-func (s *sanitizer) sanitizeToken(token string) string {
- if len(token) <= 1 {
- // NOTE: do not sanitize single letter tokens
- return token
- }
- if s.isInWhitelist(token) {
- return token
- }
- return s.hashToken(token)
-}
-
-func (s *sanitizer) hashToken(token string) string {
- if len(token) <= 0 {
- return token
- }
- // hash with sha256
- sum := sha256.Sum256([]byte(token))
- return s.trimHash(hex.EncodeToString(sum[:]))
-}
-
-func (s *sanitizer) hashNumericToken(token string) string {
- if len(token) <= 0 {
- return token
- }
- sum := sha256.Sum256([]byte(token))
- sumInt := int(binary.LittleEndian.Uint64(sum[:]))
- if sumInt < 0 {
- return strconv.Itoa(sumInt * -1)
- }
- return s.trimHash(strconv.Itoa(sumInt))
-}
-
-func (s *sanitizer) trimHash(hash string) string {
- length := s.hashLength
- if length <= 0 || len(hash) < length {
- length = len(hash)
- }
- return hash[:length]
-}
-
-func (s *sanitizer) isInWhitelist(token string) bool {
- return s.whitelist[strings.ToLower(token)] == true
-}
diff --git a/cmd/session-init/main.go b/cmd/session-init/main.go
index 75f5298c..2798fc71 100644
--- a/cmd/session-init/main.go
+++ b/cmd/session-init/main.go
@@ -1,186 +1,48 @@
package main
import (
- "flag"
"fmt"
- "log"
"os"
- "github.com/BurntSushi/toml"
- "github.com/curusarn/resh/pkg/cfg"
- "github.com/curusarn/resh/pkg/collect"
- "github.com/curusarn/resh/pkg/records"
+ "github.com/curusarn/resh/internal/cfg"
+ "github.com/curusarn/resh/internal/collect"
+ "github.com/curusarn/resh/internal/logger"
+ "github.com/curusarn/resh/internal/opt"
+ "github.com/curusarn/resh/internal/output"
+ "github.com/curusarn/resh/internal/recordint"
+ "github.com/spf13/pflag"
+ "go.uber.org/zap"
- "os/user"
- "path/filepath"
"strconv"
)
-// version from git set during build
+// info passed during build
var version string
-
-// commit from git set during build
var commit string
+var development string
func main() {
- usr, _ := user.Current()
- dir := usr.HomeDir
- configPath := filepath.Join(dir, "/.config/resh.toml")
- reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid")
-
- machineIDPath := "/etc/machine-id"
-
- var config cfg.Config
- if _, err := toml.DecodeFile(configPath, &config); err != nil {
- log.Fatal("Error reading config:", err)
- }
- 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")
-
- shell := flag.String("shell", "", "actual shell")
- uname := flag.String("uname", "", "uname")
- sessionID := flag.String("sessionId", "", "resh generated session id")
-
- // posix variables
- cols := flag.String("cols", "-1", "$COLUMNS")
- lines := flag.String("lines", "-1", "$LINES")
- home := flag.String("home", "", "$HOME")
- lang := flag.String("lang", "", "$LANG")
- lcAll := flag.String("lcAll", "", "$LC_ALL")
- login := flag.String("login", "", "$LOGIN")
- shellEnv := flag.String("shellEnv", "", "$SHELL")
- term := flag.String("term", "", "$TERM")
-
- // non-posix
- pid := flag.Int("pid", -1, "$$")
- sessionPid := flag.Int("sessionPid", -1, "$$ at session start")
- shlvl := flag.Int("shlvl", -1, "$SHLVL")
-
- host := flag.String("host", "", "$HOSTNAME")
- hosttype := flag.String("hosttype", "", "$HOSTTYPE")
- ostype := flag.String("ostype", "", "$OSTYPE")
- machtype := flag.String("machtype", "", "$MACHTYPE")
-
- // before after
- timezoneBefore := flag.String("timezoneBefore", "", "")
-
- osReleaseID := flag.String("osReleaseId", "", "/etc/os-release ID")
- osReleaseVersionID := flag.String("osReleaseVersionId", "",
- "/etc/os-release ID")
- osReleaseIDLike := flag.String("osReleaseIdLike", "", "/etc/os-release ID")
- osReleaseName := flag.String("osReleaseName", "", "/etc/os-release ID")
- osReleasePrettyName := flag.String("osReleasePrettyName", "",
- "/etc/os-release ID")
-
- rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME")
- rtsess := flag.String("realtimeSession", "-1",
- "on session start $EPOCHREALTIME")
- rtsessboot := flag.String("realtimeSessSinceBoot", "-1",
- "on session start $EPOCHREALTIME")
- flag.Parse()
-
- if *showVersion == true {
- fmt.Println(version)
- os.Exit(0)
- }
- if *showRevision == true {
- fmt.Println(commit)
- os.Exit(0)
- }
- if *requireVersion != "" && *requireVersion != version {
- fmt.Println("Please restart/reload this terminal session " +
- "(resh version: " + version +
- "; resh version of this terminal session: " + *requireVersion +
- ")")
- os.Exit(3)
- }
- if *requireRevision != "" && *requireRevision != commit {
- fmt.Println("Please restart/reload this terminal session " +
- "(resh revision: " + commit +
- "; resh revision of this terminal session: " + *requireRevision +
- ")")
- os.Exit(3)
- }
- realtimeBefore, err := strconv.ParseFloat(*rtb, 64)
- if err != nil {
- log.Fatal("Flag Parsing error (rtb):", err)
- }
- realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64)
- if err != nil {
- log.Fatal("Flag Parsing error (rt sess):", err)
- }
- realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64)
+ config, errCfg := cfg.New()
+ logger, err := logger.New("session-init", config.LogLevel, development)
if err != nil {
- log.Fatal("Flag Parsing error (rt sess boot):", err)
- }
- realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart
- realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart
-
- timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore)
- realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset
-
- if *osReleaseID == "" {
- *osReleaseID = "linux"
+ fmt.Printf("Error while creating logger: %v", err)
}
- if *osReleaseName == "" {
- *osReleaseName = "Linux"
+ defer logger.Sync() // flushes buffer, if any
+ if errCfg != nil {
+ logger.Error("Error while getting configuration", zap.Error(errCfg))
}
- if *osReleasePrettyName == "" {
- *osReleasePrettyName = "Linux"
- }
-
- rec := records.Record{
- // posix
- Cols: *cols,
- Lines: *lines,
- // core
- BaseRecord: records.BaseRecord{
- Shell: *shell,
- Uname: *uname,
- SessionID: *sessionID,
-
- // posix
- Home: *home,
- Lang: *lang,
- LcAll: *lcAll,
- Login: *login,
- // Path: *path,
- ShellEnv: *shellEnv,
- Term: *term,
-
- // non-posix
- 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,
+ out := output.New(logger, "resh-collect ERROR")
- MachineID: collect.ReadFileContent(machineIDPath),
+ args := opt.HandleVersionOpts(out, os.Args, version, commit)
- OsReleaseID: *osReleaseID,
- OsReleaseVersionID: *osReleaseVersionID,
- OsReleaseIDLike: *osReleaseIDLike,
- OsReleaseName: *osReleaseName,
- OsReleasePrettyName: *osReleasePrettyName,
+ flags := pflag.NewFlagSet("", pflag.ExitOnError)
+ sessionID := flags.String("session-id", "", "Resh generated session ID")
+ sessionPID := flags.Int("session-pid", -1, "$$ - Shell session PID")
+ flags.Parse(args)
- ReshUUID: collect.ReadFileContent(reshUUIDPath),
- ReshVersion: version,
- ReshRevision: commit,
- },
+ rec := recordint.SessionInit{
+ SessionID: *sessionID,
+ SessionPID: *sessionPID,
}
- collect.SendRecord(rec, strconv.Itoa(config.Port), "/session_init")
+ collect.SendSessionInit(out, rec, strconv.Itoa(config.Port))
}
diff --git a/conf/config.toml b/conf/config.toml
deleted file mode 100644
index 9db8b943..00000000
--- a/conf/config.toml
+++ /dev/null
@@ -1,5 +0,0 @@
-port = 2627
-sesswatchPeriodSeconds = 120
-sesshistInitHistorySize = 1000
-debug = false
-bindControlR = true
diff --git a/data/sanitizer/copyright_information.md b/data/sanitizer/copyright_information.md
deleted file mode 100644
index abdbf33e..00000000
--- a/data/sanitizer/copyright_information.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# copyright information
-
-Whitelist contains content from variety of sources.
-
-Part of the whitelist (`./whitelist.txt`) is made of copyrighted content from [FileInfo.com](https://fileinfo.com/filetypes/common).
-
-This content was used with permission from FileInfo.com.
diff --git a/data/sanitizer/whitelist.txt b/data/sanitizer/whitelist.txt
deleted file mode 100644
index 180e9c32..00000000
--- a/data/sanitizer/whitelist.txt
+++ /dev/null
@@ -1,1195 +0,0 @@
-
-!
--
-.
-..
-:
-[
-[[
-]]
-{
-}
-3dm
-3ds
-3g2
-3gp
-7z
-accdb
-add
-addgnupghome
-addgroup
-addpart
-addr2line
-add-shell
-adduser
-agetty
-ai
-aif
-alias
-alternatives
-apk
-app
-applydeltarpm
-applygnupgdefaults
-apt
-apt-cache
-apt-cdrom
-apt-config
-apt-get
-apt-key
-apt-mark
-ar
-arch
-arpd
-arping
-as
-asf
-asm
-asp
-aspx
-au
-autoload
-avi
-awk
-b
-b2sum
-badblocks
-bak
-base32
-base64
-basename
-basenc
-bash
-bashbug
-bashbug-64
-bat
-bg
-bin
-bind
-bindkey
-bisect
-blend
-blkdeactivate
-blkdiscard
-blkid
-blkzone
-blockdev
-bmp
-boot
-bootctl
-br
-branch
-break
-bridge
-brotli
-build-locale-archive
-builtin
-bunzip2
-busctl
-bye
-bz2
-bzcat
-bzcmp
-bzdiff
-bzegrep
-bzexe
-bzfgrep
-bzgrep
-bzip2
-bzip2recover
-bzless
-bzmore
-c
-cab
-cal
-ca-legacy
-caller
-capsh
-captoinfo
-case
-cat
-catchsegv
-cbr
-cc
-cd
-cer
-certutil
-cfdisk
-cfg
-c++filt
-cfm
-cgi
-chacl
-chage
-chardetect
-chattr
-chcon
-chcpu
-chdir
-checkout
-chfn
-chgpasswd
-chgrp
-chkconfig
-chmem
-chmod
-choom
-chown
-chpasswd
-chroot
-chrt
-chsh
-cksum
-class
-clear
-clear_console
-clock
-clockdiff
-clone
-cmp
-cmsutil
-co
-code
-col
-colcrt
-colrm
-column
-com
-combinedeltarpm
-comm
-command
-commit
-compadd
-comparguments
-compcall
-compctl
-compdescribe
-compfiles
-compgen
-compgroups
-complete
-compopt
-compquote
-compset
-comptags
-comptry
-compvalues
-conf
-continue
-convert
-coproc
-coredumpctl
-cp
-cpgr
-cpio
-cpl
-cpp
-cppw
-cracklib-check
-cracklib-format
-cracklib-packer
-cracklib-unpacker
-crdownload
-create-cracklib-dict
-crlutil
-crx
-cs
-csplit
-csr
-css
-csv
-ctrlaltdel
-ctstat
-cue
-cur
-curl
-cut
-cvtsudoers
-cz
-dash
-dat
-date
-db
-db_archive
-db_checkpoint
-db_deadlock
-db_dump
-db_dump185
-dbf
-db_hotbackup
-db_load
-db_log_verify
-db_printlog
-db_recover
-db_replicate
-db_stat
-db_tuner
-db_upgrade
-dbus-binding-tool
-dbus-broker
-dbus-broker-launch
-dbus-cleanup-sockets
-dbus-daemon
-dbus-monitor
-dbus-run-session
-dbus-send
-dbus-test-tool
-dbus-update-activation-environment
-dbus-uuidgen
-db_verify
-dcr
-dd
-dds
-de
-deb
-debconf
-debconf-apt-progress
-debconf-communicate
-debconf-copydb
-debconf-escape
-debconf-set-selections
-debconf-show
-deb-systemd-helper
-deb-systemd-invoke
-debugfs
-debuginfo-install
-declare
-delgroup
-delpart
-deluser
-dem
-depmod
-deskthemepack
-desktop
-dev
-devlink
-df
-dgawk
-diff
-diff3
-dir
-dircolors
-dirmngr
-dirmngr-client
-dirname
-dirs
-disable
-disown
-dll
-dmesg
-dmfilemapd
-dmg
-dmp
-dmsetup
-dmstats
-dnf
-dnf-3
-dnsdomainname
-do
-doc
-docker
-Dockerfile
-docx
-domainname
-done
-dpkg
-dpkg-deb
-dpkg-divert
-dpkg-maintscript-helper
-dpkg-preconfigure
-dpkg-query
-dpkg-reconfigure
-dpkg-split
-dpkg-statoverride
-dpkg-trigger
-dracut
-drv
-dtd
-du
-dumpe2fs
-dwg
-dwp
-dxf
-e2freefrag
-e2fsck
-e2image
-e2label
-e2mmpstatus
-e2undo
-e4crypt
-e4defrag
-easy_install-3.7
-echo
-echotc
-echoti
-egrep
-eject
-elfedit
-elif
-else
-emacs
-emulate
-enable
-end
-env
-eps
-esac
-etc
-eval
-evmctl
-ex
-exe
-exec
-exit
-expand
-expiry
-export
-expr
-factor
-faillock
-faillog
-fallocate
-false
-fc
-fdformat
-fdisk
-fetch
-ffmpeg
-fg
-fgrep
-fi
-filefrag
-fincore
-find
-findfs
-findmnt
-find-repos-of-install
-fips-finish-install
-fips-mode-setup
-fish
-fla
-float
-flock
-flv
-fmt
-fnt
-fold
-fon
-for
-foreach
-free
-fsck
-fsck.cramfs
-fsck.ext2
-fsck.ext3
-fsck.ext4
-fsck.minix
-fsfreeze
-fstab-decode
-fstrim
-function
-functions
-g13
-g13-syshelp
-gadget
-gam
-gapplication
-gawk
-gdbus
-ged
-gencat
-genl
-getcap
-getconf
-getent
-getfacl
-getln
-getopt
-getopts
-getpcaps
-getty
-gif
-gio
-gio-launch-desktop
-gio-querymodules-64
-git
-github.com
-glib-compile-schemas
-glibc_post_upgrade.x86_64
-go
-gpasswd
-gpg
-gpg2
-gpg-agent
-gpgconf
-gpg-connect-agent
-gpg-error
-gpgme-json
-gpgparsemail
-gpgsplit
-gpgv
-gpgv2
-gpg-wks-server
-gpg-zip
-gprof
-gpx
-grep
-groupadd
-groupdel
-groupmems
-groupmod
-groups
-grpck
-grpconv
-grpunconv
-gsettings
-gtar
-gunzip
-gz
-gzexe
-gzip
-h
-halt
-hardlink
-hash
-head
-heic
-help
-hexdump
-history
-home
-hostid
-hostname
-hostnamectl
-hqx
-htm
-html
-http
-https
-hwclock
-i386
-icns
-ico
-iconv
-iconvconfig
-iconvconfig.x86_64
-ics
-id
-idn
-if
-ifenslave
-iff
-igawk
-in
-indd
-info
-infocmp
-infokey
-infotocap
-ini
-init
-initctl
-insmod
-install
-install-info
-installkernel
-integer
-invoke-rc.d
-ionice
-ip
-ipcmk
-ipcrm
-ipcs
-ir
-ischroot
-iso
-isosize
-it
-jar
-java
-jobs
-join
-journalctl
-jpg
-jq
-js
-json
-jsp
-kernel-install
-key
-keychain
-kill
-killall5
-kml
-kmod
-kmz
-kpartx
-ksp
-kss
-kwd
-last
-lastb
-lastlog
-lchage
-lchfn
-lchsh
-ld
-ldattach
-ld.bfd
-ldconfig
-ldconfig.real
-ldd
-ld.gold
-let
-lgroupadd
-lgroupdel
-lgroupmod
-lib
-lib64
-lid
-limit
-link
-linux32
-linux64
-ln
-lnewusers
-lnk
-lnstat
-local
-locale
-locale-check
-localectl
-localedef
-localhost
-log
-logger
-login
-loginctl
-logname
-logout
-logsave
-look
-losetup
-lost+found
-lpasswd
-ls
-lsattr
-lsblk
-lscpu
-lsinitrd
-lsipc
-lslocks
-lslogins
-lsmem
-lsmod
-lsns
-lua
-luac
-luseradd
-luserdel
-lusermod
-lz4
-lz4c
-lz4cat
-m
-m3u
-m4a
-m4p
-m4v
-machinectl
-make
-makedb
-makedeltarpm
-make-dummy-cert
-Makefile
-man
-mapfile
-master
-mawk
-max
-mcookie
-md5
-md5sum
-md5sums
-md5sum.textutils
-mdb
-mdf
-media
-merge
-mesg
-mid
-mim
-mkdict
-mkdir
-mke2fs
-mkfifo
-mkfs
-mkfs.bfs
-mkfs.cramfs
-mkfs.ext2
-mkfs.ext3
-mkfs.ext4
-mkfs.minix
-mkhomedir_helper
-mkinitrd
-mklost+found
-mknod
-mkpasswd
-mkswap
-mktemp
-mnt
-mo
-modinfo
-modprobe
-modulemd-validator-v1
-modutil
-more
-mount
-mountpoint
-mov
-mp3
-mp4
-mpa
-mpg
-msg
-msi
-mv
-namei
-nawk
-needs-restarting
-nes
-net
-networkctl
-newgidmap
-newgrp
-newuidmap
-newusers
-nice
-nisdomainname
-nl
-nm
-no
-nocorrect
-noglob
-nohup
-nologin
-nproc
-nsenter
-nstat
-numfmt
-o
-obj
-objcopy
-objdump
-od
-odt
-ogg
-oldfind
-openssl
-opt
-org
-origin
-otf
-p11-kit
-package-cleanup
-packer
-pager
-pages
-pam-auth-update
-pam_console_apply
-pam_extrausers_chkpwd
-pam_extrausers_update
-pam_getenv
-pam_tally
-pam_tally2
-pam_timestamp_check
-part
-partx
-passwd
-paste
-patch
-pathchk
-pct
-pdb
-pdf
-perl
-perl5.26.1
-perl5.28.1
-pgawk
-pgrep
-php
-phps
-phtml
-pidof
-pinentry
-pinentry-curses
-ping
-ping4
-ping6
-pinky
-pip-3
-pip3
-pip-3.7
-pip3.7
-pivot_root
-pk12util
-pkg
-pkg-config
-pkill
-pkl
-pl
-pldd
-pls
-plugin
-pmap
-png
-policy-rc.d
-popd
-portablectl
-pov
-poweroff
-pps
-ppt
-pptx
-pr
-prf
-print
-printenv
-printf
-private
-prlimit
-proc
-properties
-ps
-psd
-pspimage
-ptx
-pull
-push
-pushd
-pushln
-pwck
-pwconv
-pwd
-pwdx
-pwhistory_helper
-pwmake
-pwscore
-pwunconv
-py
-pyc
-pydoc
-pydoc3
-pydoc3.7
-pyo
-python
-python2
-python2.7
-python3
-python3.7
-python3.7m
-pyvenv
-pyvenv-3.7
-r
-ranlib
-rar
-raw
-rbash
-rc
-rdf
-rdisc
-rdma
-read
-readarray
-readelf
-readlink
-readonly
-readprofile
-realpath
-rebase
-reboot
-rehash
-remove-shell
-rename
-rename.ul
-renew-dummy-cert
-renice
-repeat
-repoclosure
-repodiff
-repo-graph
-repomanage
-repoquery
-repo-rss
-reposync
-repotrack
-reset
-resh
-resize2fs
-resizepart
-resolvconf
-resolvectl
-return
-rev
-rfkill
-rgrep
-rm
-rmdir
-rmmod
-rmt
-rmt-tar
-rom
-root
-routef
-routel
-rpcgen
-rpm
-rpm2archive
-rpm2cpio
-rpmdb
-rpmdumpheader
-rpmkeys
-rpmquery
-rpmverify
-rss
-rtacct
-rtcwake
-rtf
-rtmon
-rtstat
-ru
-run
-runcon
-run-help
-runlevel
-run-parts
-runuser
-rvi
-rview
-s
-sasldblistusers2
-saslpasswd2
-sav
-savelog
-sbin
-sched
-script
-scriptreplay
-sdf
-sdiff
-sed
-sefcontext_compile
-select
-select-editor
-sensible-browser
-sensible-editor
-sensible-pager
-seq
-service
-set
-setarch
-setcap
-setfacl
-setopt
-setpriv
-setsid
-setterm
-setup-nsssysinit
-setup-nsssysinit.sh
-sfdisk
-sg
-sh
-sha1sum
-sha224sum
-sha256sum
-sha384sum
-sha512sum
-shadowconfig
-share
-sh.distrib
-shift
-shopt
-show
-show-changed-rco
-show-installed
-shred
-shuf
-shutdown
-signtool
-signver
-sitx
-size
-skill
-slabtop
-sleep
-sln
-snice
-so
-sort
-sotruss
-source
-split
-sprof
-sql
-sqlite3
-srt
-srv
-ss
-ssh
-ssltap
-start-stop-daemon
-stat
-status
-stdbuf
-strings
-strip
-stty
-su
-sudo
-sudoedit
-sudoreplay
-sulogin
-sum
-suspend
-svg
-swaplabel
-swapoff
-swapon
-swf
-swift
-switch_root
-sync
-sys
-sysctl
-systemctl
-systemd-analyze
-systemd-ask-password
-systemd-cat
-systemd-cgls
-systemd-cgtop
-systemd-coredumpctl
-systemd-delta
-systemd-detect-virt
-systemd-escape
-systemd-firstboot
-systemd-hwdb
-systemd-id128
-systemd-inhibit
-systemd-loginctl
-systemd-machine-id-setup
-systemd-mount
-systemd-notify
-systemd-nspawn
-systemd-path
-systemd-resolve
-systemd-run
-systemd-socket-activate
-systemd-stdio-bridge
-systemd-sysusers
-systemd-tmpfiles
-systemd-tty-ask-password-agent
-systemd-umount
-tabs
-tac
-tag
-tail
-tailf
-tar
-tarcat
-taskset
-tax2016
-tax2018
-tc
-tee
-telinit
-tempfile
-test
-testgdbm
-tex
-tga
-tgz
-then
-thm
-tic
-tif
-tiff
-tig
-time
-timedatectl
-timeout
-times
-tipc
-tload
-tmp
-toast
-toe
-top
-torrent
-touch
-tput
-tr
-tracepath
-tracepath6
-trap
-true
-truncate
-trust
-tset
-tsort
-ttf
-tty
-ttyctl
-tune2fs
-txt
-type
-typeset
-tzconfig
-tzselect
-udevadm
-uk
-ul
-ulimit
-umask
-umount
-unalias
-uname
-uname26
-unbound-anchor
-uncompress
-unexpand
-unfunction
-unhash
-uniq
-unix_chkpwd
-unix_update
-unlimit
-unlink
-unlz4
-unminimize
-unset
-unsetopt
-unshare
-until
-unxz
-update-alternatives
-update-ca-trust
-update-crypto-policies
-update-mime-database
-update-passwd
-update-rc.d
-uptime
-urlgrabber
-useradd
-userdel
-usermod
-users
-usr
-utmpdump
-uue
-uuidgen
-uuidparse
-Vagrantfile
-var
-vared
-vb
-vcd
-vcf
-vcxproj
-vdir
-verifytree
-vi
-view
-vigr
-vim
-vipw
-visudo
-vlc
-vmstat
-vob
-w
-wait
-wall
-watch
-watchgnupg
-wav
-wc
-wdctl
-weak-modules
-whence
-where
-whereis
-which
-which-command
-while
-who
-whoami
-wipefs
-wma
-wmv
-wpd
-w.procps
-wps
-write
-wsf
-x86_64
-xargs
-xbel
-xcodeproj
-xhtml
-xlr
-xls
-xlsx
-xml
-xmlcatalog
-xmllint
-xmlwf
-xpm
-xsd
-xsl
-xz
-xzcat
-xzcmp
-xzdec
-xzdiff
-xzegrep
-xzfgrep
-xzgrep
-xzless
-xzmore
-yaourt
-yes
-ypdomainname
-yum
-yum-builddep
-yum-complete-transaction
-yum-config-manager
-yumdb
-yum-debug-dump
-yum-debug-restore
-yumdownloader
-yum-groups-manager
-yuv
-Z
-zcat
-zcmp
-zcompile
-zdiff
-zdump
-zegrep
-zfgrep
-zforce
-zformat
-zgrep
-zic
-zip
-zipx
-zle
-zless
-zmodload
-zmore
-znew
-zparseopts
-zramctl
-zregexparse
-zsh
-zstyle
diff --git a/go.mod b/go.mod
index 0ca87cda..9ba16a5d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,23 +1,30 @@
module github.com/curusarn/resh
-go 1.16
+go 1.19
require (
- github.com/BurntSushi/toml v0.4.1
- github.com/awesome-gocui/gocui v1.0.0
- github.com/coreos/go-semver v0.3.0
- github.com/gdamore/tcell/v2 v2.4.0 // indirect
- github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629
- github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
- github.com/mattn/go-runewidth v0.0.13 // indirect
- github.com/mattn/go-shellwords v1.0.12
- github.com/mb-14/gomarkov v0.0.0-20210216094942-a5b484cc0243
+ github.com/BurntSushi/toml v1.2.1
+ github.com/awesome-gocui/gocui v1.1.0
+ github.com/google/uuid v1.3.0
+ github.com/mattn/go-isatty v0.0.17
github.com/mitchellh/go-ps v1.0.0
- github.com/schollz/progressbar v1.0.0
- github.com/spf13/cobra v1.2.1
+ github.com/spf13/cobra v1.6.1
+ github.com/spf13/pflag v1.0.5
github.com/whilp/git-urls v1.0.0
- golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6
- golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect
- golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
- golang.org/x/text v0.3.7 // indirect
+ go.uber.org/zap v1.24.0
+ golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2
+)
+
+require (
+ github.com/gdamore/encoding v1.0.0 // indirect
+ github.com/gdamore/tcell/v2 v2.6.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-runewidth v0.0.14 // indirect
+ github.com/rivo/uniseg v0.4.4 // indirect
+ go.uber.org/atomic v1.10.0 // indirect
+ go.uber.org/multierr v1.9.0 // indirect
+ golang.org/x/sys v0.5.0 // indirect
+ golang.org/x/term v0.5.0 // indirect
+ golang.org/x/text v0.7.0 // indirect
)
diff --git a/go.sum b/go.sum
index 60670d55..6f8fb611 100644
--- a/go.sum
+++ b/go.sum
@@ -40,6 +40,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@@ -47,6 +49,10 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/awesome-gocui/gocui v1.0.0 h1:1bf0DAr2JqWNxGFS8Kex4fM/khICjEnCi+a1+NfWy+w=
github.com/awesome-gocui/gocui v1.0.0/go.mod h1:UvP3dP6+UsTGl9IuqP36wzz6Lemo90wn5p3tJvZ2OqY=
+github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII=
+github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -57,11 +63,12 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -77,6 +84,8 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
+github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
+github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -141,7 +150,10 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -170,8 +182,9 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
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=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@@ -187,16 +200,17 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
-github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
-github.com/mb-14/gomarkov v0.0.0-20210216094942-a5b484cc0243 h1:F0IAcxxFNzC8/HOxI5Q2hpsWAoGdy+lGMjoVyrcMeSw=
-github.com/mb-14/gomarkov v0.0.0-20210216094942-a5b484cc0243/go.mod h1:5F3Y03oxWIyMq3Wa4AxU544RYnXNZwHBfqpDpdLibBY=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -211,23 +225,26 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
+github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/schollz/progressbar v1.0.0 h1:gbyFReLHDkZo8mxy/dLWMr+Mpb1MokGJ1FqCiqacjZM=
-github.com/schollz/progressbar v1.0.0/go.mod h1:/l9I7PC3L3erOuz54ghIRKUEFcosiWfLvJv+Eq26UMs=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -236,6 +253,8 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
+github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
+github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -246,7 +265,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU=
github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE=
@@ -255,6 +276,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@@ -265,9 +287,21 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
+go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
+go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
+go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -275,6 +309,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -286,6 +321,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI=
+golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -311,6 +348,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -347,6 +385,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -370,6 +409,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -415,10 +455,18 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -428,6 +476,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -482,6 +532,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -598,7 +650,10 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/installation.md b/installation.md
new file mode 100644
index 00000000..2ddf8165
--- /dev/null
+++ b/installation.md
@@ -0,0 +1,77 @@
+# Installation
+
+## One command installation
+
+Feel free to check the `rawinstall.sh` script before running it.
+
+```sh
+curl -fsSL https://raw.githubusercontent.com/curusarn/resh/master/scripts/rawinstall.sh | sh
+```
+
+ℹ️ You will need to have `curl` and `tar` installed.
+
+## Clone & install
+
+```sh
+git clone https://github.com/curusarn/resh.git
+cd resh
+scripts/rawinstall.sh
+```
+
+## Build from source
+
+:warning: Building from source is intended for development and troubleshooting.
+
+```sh
+git clone https://github.com/curusarn/resh.git
+cd resh
+make install
+```
+
+## Update
+
+Once installed RESH can be updated using:
+```sh
+reshctl update
+```
+
+## Disabling RESH
+
+If you have a persistent issue with RESH you can temporarily disable it and then enable it later.
+
+ℹ️ You won't lose your history nor configuration.
+
+Go to `~/.zshrc` and `~/.bashrc` and comment out following lines:
+```sh
+[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc
+[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # bashrc only
+```
+The second line is bash-specific so you won't find it in `~/.zshrc`
+
+You can re-enable RESH by uncommenting the lines above or by re-installing it.
+
+## Uninstallation
+
+You can uninstall RESH by running: `rm -rf ~/.resh/`.
+
+⚠️ Restart all open terminals after uninstall!
+
+### Installed files
+
+Binaries and shell files are in `~/.resh/`.
+
+Recorded history, device files, and logs are in one of:
+- `~/.local/share/resh/`
+- `$XDG_DATA_HOME/resh/` (if set)
+
+RESH config file is read from one of:
+- `~/.config/resh.toml`
+- `$XDG_CONFIG_HOME/resh.toml` (if set)
+
+RESH also adds a following lines to `~/.zshrc` and `~/.bashrc` to load itself on terminal startup:
+```sh
+[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc
+[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # bashrc only
+```
+
+:information_source: RESH follows [XDG directory specification ⇗](https://maex.me/2019/12/the-power-of-the-xdg-base-directory-specification/)
diff --git a/internal/cfg/cfg.go b/internal/cfg/cfg.go
new file mode 100644
index 00000000..534443d4
--- /dev/null
+++ b/internal/cfg/cfg.go
@@ -0,0 +1,191 @@
+package cfg
+
+import (
+ "fmt"
+ "os"
+ "path"
+
+ "github.com/BurntSushi/toml"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+// configFile used to parse the config file
+type configFile struct {
+ // ConfigVersion - never remove this
+ ConfigVersion *string
+
+ // added in legacy
+ Port *int
+ SesswatchPeriodSeconds *uint
+ SesshistInitHistorySize *int
+ BindControlR *bool
+ Debug *bool
+
+ // added in v1
+ LogLevel *string
+
+ // added in legacy
+ // deprecated in v1
+ BindArrowKeysBash *bool
+ BindArrowKeysZsh *bool
+}
+
+// Config returned by this package to be used in the rest of the project
+type Config struct {
+ // Port used by daemon and rest of the components to communicate
+ // Make sure to restart the daemon when you change it
+ Port int
+
+ // BindControlR causes CTRL+R to launch the search app
+ BindControlR bool
+ // LogLevel used to filter logs
+ LogLevel zapcore.Level
+
+ // Debug mode for search app
+ Debug bool
+ // SessionWatchPeriodSeconds is how often should daemon check if terminal
+ // sessions are still alive
+ // There is not much need to adjust the value because both memory overhead of watched sessions
+ // and the CPU overhead of checking them are quite low
+ SessionWatchPeriodSeconds uint
+ // ReshHistoryMinSize is how large resh history needs to be for
+ // daemon to ignore standard shell history files
+ // Ignoring standard shell history gives us more consistent experience
+ // but you can increase this to something large to see standard shell history in RESH search
+ ReshHistoryMinSize int
+}
+
+// defaults for config
+var defaults = Config{
+ Port: 2627,
+ LogLevel: zap.InfoLevel,
+ BindControlR: true,
+
+ Debug: false,
+ SessionWatchPeriodSeconds: 600,
+ ReshHistoryMinSize: 1000,
+}
+
+const headerComment = `##
+######################
+## RESH config (v1) ##
+######################
+## Here you can find info about RESH configuration options.
+## You can uncomment the options and customize them.
+
+## Required.
+## The config format can change in future versions.
+## ConfigVersion helps us seamlessly upgrade to the new formats.
+# ConfigVersion = "v1"
+
+## Port used by RESH daemon and rest of the components to communicate.
+## Make sure to restart the daemon (pkill resh-daemon) when you change it.
+# Port = 2627
+
+## Controls how much and how detailed logs all RESH components produce.
+## Use "debug" for full logs when you encounter an issue
+## Options: "debug", "info", "warn", "error", "fatal"
+# LogLevel = "info"
+
+## When BindControlR is "true" RESH search app is bound to CTRL+R on terminal startup
+# BindControlR = true
+
+## When Debug is "true" the RESH search app runs in debug mode.
+## This is useful for development.
+# Debug = false
+
+## Daemon keeps track of running terminal sessions.
+## SessionWatchPeriodSeconds controls how often daemon checks if the sessions are still alive.
+## You shouldn't need to adjust this.
+# SessionWatchPeriodSeconds = 600
+
+## When RESH is first installed there is no RESH history so there is nothing to search.
+## As a temporary workaround, RESH daemon parses bash/zsh shell history and searches it.
+## Once RESH history is big enough RESH stops using bash/zsh history.
+## ReshHistoryMinSize controls how big RESH history needs to be before this happens.
+## You can increase this this to e.g. 10000 to get RESH to use bash/zsh history longer.
+# ReshHistoryMinSize = 1000
+
+`
+
+func getConfigPath() (string, error) {
+ fname := "resh.toml"
+ xdgDir, found := os.LookupEnv("XDG_CONFIG_HOME")
+ if found {
+ return path.Join(xdgDir, fname), nil
+ }
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return "", fmt.Errorf("could not get user home dir: %w", err)
+ }
+ return path.Join(homeDir, ".config", fname), nil
+}
+
+func readConfig(path string) (*configFile, error) {
+ var config configFile
+ if _, err := toml.DecodeFile(path, &config); err != nil {
+ return &config, fmt.Errorf("could not decode config: %w", err)
+ }
+ return &config, nil
+}
+
+func getConfig() (*configFile, error) {
+ path, err := getConfigPath()
+ if err != nil {
+ return nil, fmt.Errorf("could not get config file path: %w", err)
+ }
+ return readConfig(path)
+}
+
+// returned config is always usable, returned errors are informative
+func processAndFillDefaults(configF *configFile) (Config, error) {
+ config := defaults
+
+ if configF.Port != nil {
+ config.Port = *configF.Port
+ }
+ if configF.SesswatchPeriodSeconds != nil {
+ config.SessionWatchPeriodSeconds = *configF.SesswatchPeriodSeconds
+ }
+ if configF.SesshistInitHistorySize != nil {
+ config.ReshHistoryMinSize = *configF.SesshistInitHistorySize
+ }
+
+ var err error
+ if configF.LogLevel != nil {
+ logLevel, err := zapcore.ParseLevel(*configF.LogLevel)
+ if err != nil {
+ err = fmt.Errorf("could not parse log level: %w", err)
+ } else {
+ config.LogLevel = logLevel
+ }
+ }
+
+ if configF.BindControlR != nil {
+ config.BindControlR = *configF.BindControlR
+ }
+
+ return config, err
+}
+
+// New returns a config file
+// returned config is always usable, returned errors are informative
+func New() (Config, error) {
+ configF, err := getConfig()
+ if err != nil {
+ return defaults, fmt.Errorf("using default config because of error while getting/reading config: %w", err)
+ }
+
+ config, err := processAndFillDefaults(configF)
+ if err != nil {
+ return config, fmt.Errorf("errors while processing config: %w", err)
+ }
+ return config, nil
+}
+
+// GetPath returns path to config
+// Shouldn't be necessary for basic use
+func GetPath() (string, error) {
+ return getConfigPath()
+}
diff --git a/internal/cfg/migrate.go b/internal/cfg/migrate.go
new file mode 100644
index 00000000..bc92af39
--- /dev/null
+++ b/internal/cfg/migrate.go
@@ -0,0 +1,100 @@
+package cfg
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/BurntSushi/toml"
+)
+
+// Migrate old config versions to current config version
+// returns true if any changes were made to the config
+func Migrate() (bool, error) {
+ fpath, err := getConfigPath()
+ if err != nil {
+ return false, fmt.Errorf("could not get config file path: %w", err)
+ }
+ configF, err := readConfig(fpath)
+ if err != nil {
+ return false, fmt.Errorf("could not read config: %w", err)
+ }
+ const current = "v1"
+ if configF.ConfigVersion != nil && *configF.ConfigVersion == current {
+ return false, nil
+ }
+
+ if configF.ConfigVersion == nil {
+ configF, err = legacyToV1(configF)
+ if err != nil {
+ return true, fmt.Errorf("error converting config from version 'legacy' to 'v1': %w", err)
+ }
+ }
+
+ if *configF.ConfigVersion != current {
+ return false, fmt.Errorf("unrecognized config version: '%s'", *configF.ConfigVersion)
+ }
+ err = writeConfig(configF, fpath)
+ if err != nil {
+ return true, fmt.Errorf("could not write migrated config: %w", err)
+ }
+ return true, nil
+}
+
+// writeConfig should only be used when migrating config to new version
+// writing the config file discards all comments in the config file (limitation of TOML library)
+// to make up for lost comments we add header comment to the start of the file
+func writeConfig(config *configFile, path string) error {
+ file, err := os.OpenFile(path, os.O_RDWR|os.O_TRUNC, 0666)
+ if err != nil {
+ return fmt.Errorf("could not open config for writing: %w", err)
+ }
+ defer file.Close()
+ _, err = file.WriteString(headerComment)
+ if err != nil {
+ return fmt.Errorf("could not write config header: %w", err)
+ }
+ err = toml.NewEncoder(file).Encode(config)
+ if err != nil {
+ return fmt.Errorf("could not encode config: %w", err)
+ }
+ return nil
+}
+
+func legacyToV1(config *configFile) (*configFile, error) {
+ if config.ConfigVersion != nil {
+ return nil, fmt.Errorf("config version is not 'legacy': '%s'", *config.ConfigVersion)
+ }
+ version := "v1"
+ newConf := configFile{
+ ConfigVersion: &version,
+ }
+ // Remove defaults
+ if config.Port != nil && *config.Port != 2627 {
+ newConf.Port = config.Port
+ }
+ if config.SesswatchPeriodSeconds != nil && *config.SesswatchPeriodSeconds != 120 {
+ newConf.SesswatchPeriodSeconds = config.SesswatchPeriodSeconds
+ }
+ if config.SesshistInitHistorySize != nil && *config.SesshistInitHistorySize != 1000 {
+ newConf.SesshistInitHistorySize = config.SesshistInitHistorySize
+ }
+ if config.BindControlR != nil && *config.BindControlR != true {
+ newConf.BindControlR = config.BindControlR
+ }
+ if config.Debug != nil && *config.Debug != false {
+ newConf.Debug = config.Debug
+ }
+ return &newConf, nil
+}
+
+// func v1ToV2(config *configFile) (*configFile, error) {
+// if *config.ConfigVersion != "v1" {
+// return nil, fmt.Errorf("config version is not 'legacy': '%s'", *config.ConfigVersion)
+// }
+// version := "v2"
+// newConf := configFile{
+// ConfigVersion: &version,
+// // Here goes all config fields - no need to prune defaults like we do for legacy
+// }
+// return &newConf, nil
+// }
diff --git a/internal/check/check.go b/internal/check/check.go
new file mode 100644
index 00000000..dab32634
--- /dev/null
+++ b/internal/check/check.go
@@ -0,0 +1,92 @@
+package check
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+)
+
+func LoginShell() (string, error) {
+ shellPath, found := os.LookupEnv("SHELL")
+ if !found {
+ return "", fmt.Errorf("env variable $SHELL is not set")
+ }
+ parts := strings.Split(shellPath, "/")
+ shell := parts[len(parts)-1]
+ if shell != "bash" && shell != "zsh" {
+ return fmt.Sprintf("Current shell (%s) is unsupported\n", shell), nil
+ }
+ return "", nil
+}
+
+func msgShellVersion(shell, expectedVer, actualVer string) string {
+ return fmt.Sprintf(
+ "Minimal supported %s version is %s. You have %s.\n"+
+ " -> Update to %s %s+ if you want to use RESH with it",
+ shell, expectedVer, actualVer,
+ shell, expectedVer,
+ )
+}
+
+func BashVersion() (string, error) {
+ out, err := exec.Command("bash", "-c", "echo $BASH_VERSION").Output()
+ if err != nil {
+ return "", fmt.Errorf("command failed: %w", err)
+ }
+ verStr := strings.TrimSuffix(string(out), "\n")
+ ver, err := parseVersion(verStr)
+ if err != nil {
+ return "", fmt.Errorf("failed to parse version: %w", err)
+ }
+
+ if ver.Major < 4 || (ver.Major == 4 && ver.Minor < 3) {
+ return msgShellVersion("bash", "4.3", verStr), nil
+ }
+ return "", nil
+}
+
+func ZshVersion() (string, error) {
+ out, err := exec.Command("zsh", "-c", "echo $ZSH_VERSION").Output()
+ if err != nil {
+ return "", fmt.Errorf("command failed: %w", err)
+ }
+ verStr := strings.TrimSuffix(string(out), "\n")
+ ver, err := parseVersion(string(out))
+ if err != nil {
+ return "", fmt.Errorf("failed to parse version: %w", err)
+ }
+
+ if ver.Major < 5 {
+ return msgShellVersion("zsh", "5.0", verStr), nil
+ }
+ return "", nil
+}
+
+type version struct {
+ Major int
+ Minor int
+ Rest string
+}
+
+func parseVersion(str string) (version, error) {
+ parts := strings.SplitN(str, ".", 3)
+ if len(parts) < 3 {
+ return version{}, fmt.Errorf("not enough parts")
+ }
+ major, err := strconv.Atoi(parts[0])
+ if err != nil {
+ return version{}, fmt.Errorf("failed to parse major version: %w", err)
+ }
+ minor, err := strconv.Atoi(parts[1])
+ if err != nil {
+ return version{}, fmt.Errorf("failed to parse minor version: %w", err)
+ }
+ ver := version{
+ Major: major,
+ Minor: minor,
+ Rest: parts[2],
+ }
+ return ver, nil
+}
diff --git a/internal/collect/collect.go b/internal/collect/collect.go
new file mode 100644
index 00000000..1d215cf6
--- /dev/null
+++ b/internal/collect/collect.go
@@ -0,0 +1,116 @@
+package collect
+
+import (
+ "bytes"
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/curusarn/resh/internal/output"
+ "github.com/curusarn/resh/internal/recordint"
+ "go.uber.org/zap"
+)
+
+// SendRecord to daemon
+func SendRecord(out *output.Output, r recordint.Collect, port, path string) {
+ out.Logger.Debug("Sending record ...",
+ zap.String("cmdLine", r.Rec.CmdLine),
+ zap.String("sessionID", r.SessionID),
+ )
+ recJSON, err := json.Marshal(r)
+ if err != nil {
+ out.FatalE("Error while encoding record", err)
+ }
+
+ req, err := http.NewRequest("POST", "http://localhost:"+port+path,
+ bytes.NewBuffer(recJSON))
+ if err != nil {
+ out.FatalE("Error while sending record", err)
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ client := http.Client{
+ Timeout: 1 * time.Second,
+ }
+ _, err = client.Do(req)
+ if err != nil {
+ out.FatalDaemonNotRunning(err)
+ }
+}
+
+// SendSessionInit to daemon
+func SendSessionInit(out *output.Output, r recordint.SessionInit, port string) {
+ out.Logger.Debug("Sending session init ...",
+ zap.String("sessionID", r.SessionID),
+ zap.Int("sessionPID", r.SessionPID),
+ )
+ recJSON, err := json.Marshal(r)
+ if err != nil {
+ out.FatalE("Error while encoding record", err)
+ }
+
+ req, err := http.NewRequest("POST", "http://localhost:"+port+"/session_init",
+ bytes.NewBuffer(recJSON))
+ if err != nil {
+ out.FatalE("Error while sending record", err)
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ client := http.Client{
+ Timeout: 1 * time.Second,
+ }
+ _, err = client.Do(req)
+ if err != nil {
+ out.FatalDaemonNotRunning(err)
+ }
+}
+
+// ReadFileContent and return it as a string
+func ReadFileContent(logger *zap.Logger, path string) string {
+ dat, err := ioutil.ReadFile(path)
+ if err != nil {
+ logger.Error("Error reading file",
+ zap.String("filePath", path),
+ zap.Error(err),
+ )
+ return ""
+ }
+ return strings.TrimSuffix(string(dat), "\n")
+}
+
+// GetGitDirs based on result of git "cdup" command
+func GetGitDirs(logger *zap.Logger, cdUp string, exitCode int, pwd string) (string, string) {
+ if exitCode != 0 {
+ return "", ""
+ }
+ absPath := filepath.Clean(filepath.Join(pwd, cdUp))
+ realPath, err := filepath.EvalSymlinks(absPath)
+ if err != nil {
+ logger.Error("Error while handling git dir paths", zap.Error(err))
+ return "", ""
+ }
+ return absPath, realPath
+}
+
+// GetTimezoneOffsetInSeconds based on zone returned by date command
+func GetTimezoneOffsetInSeconds(logger *zap.Logger, zone string) float64 {
+ // date +%z -> "+0200"
+ hoursStr := zone[:3]
+ minsStr := zone[3:]
+ hours, err := strconv.Atoi(hoursStr)
+ if err != nil {
+ logger.Error("Error while parsing hours in timezone offset", zap.Error(err))
+ return -1
+ }
+ mins, err := strconv.Atoi(minsStr)
+ if err != nil {
+ logger.Error("Errot while parsing minutes in timezone offset:", zap.Error(err))
+ return -1
+ }
+ secs := ((hours * 60) + mins) * 60
+ return float64(secs)
+}
diff --git a/internal/datadir/datadir.go b/internal/datadir/datadir.go
new file mode 100644
index 00000000..fea36f86
--- /dev/null
+++ b/internal/datadir/datadir.go
@@ -0,0 +1,36 @@
+package datadir
+
+import (
+ "fmt"
+ "os"
+ "path"
+)
+
+// Maybe there is a better place for this constant
+const HistoryFileName = "history.reshjson"
+
+func GetPath() (string, error) {
+ reshDir := "resh"
+ xdgDir, found := os.LookupEnv("XDG_DATA_HOME")
+ if found {
+ return path.Join(xdgDir, reshDir), nil
+ }
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return "", fmt.Errorf("error while getting home dir: %w", err)
+ }
+ return path.Join(homeDir, ".local/share/", reshDir), nil
+}
+
+func MakePath() (string, error) {
+ path, err := GetPath()
+ if err != nil {
+ return "", err
+ }
+ err = os.MkdirAll(path, 0755)
+ // skip "exists" error
+ if err != nil && !os.IsExist(err) {
+ return "", fmt.Errorf("error while creating directories: %w", err)
+ }
+ return path, nil
+}
diff --git a/internal/device/device.go b/internal/device/device.go
new file mode 100644
index 00000000..1e9fdab9
--- /dev/null
+++ b/internal/device/device.go
@@ -0,0 +1,145 @@
+// device implements helpers that get/set device config files
+package device
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "path"
+ "strings"
+
+ "github.com/curusarn/resh/internal/futil"
+ "github.com/curusarn/resh/internal/output"
+ "github.com/google/uuid"
+ isatty "github.com/mattn/go-isatty"
+)
+
+const fnameID = "device-id"
+const fnameName = "device-name"
+
+const fpathIDLegacy = ".resh/resh-uuid"
+
+const filePerm = 0644
+
+// Getters
+
+func GetID(dataDir string) (string, error) {
+ return readValue(dataDir, fnameID)
+}
+
+func GetName(dataDir string) (string, error) {
+ return readValue(dataDir, fnameName)
+}
+
+// Install helpers
+
+func SetupID(dataDir string) error {
+ return setIDIfUnset(dataDir)
+}
+
+func SetupName(out *output.Output, dataDir string) error {
+ return promptAndWriteNameIfUnset(out, dataDir)
+}
+
+func readValue(dataDir, fname string) (string, error) {
+ fpath := path.Join(dataDir, fname)
+ dat, err := os.ReadFile(fpath)
+ if err != nil {
+ return "", fmt.Errorf("could not read file with %s: %w", fname, err)
+ }
+ val := strings.TrimRight(string(dat), "\n")
+ return val, nil
+}
+
+func setIDIfUnset(dataDir string) error {
+ fpath := path.Join(dataDir, fnameID)
+ exists, err := futil.FileExists(fpath)
+ if err != nil {
+ return err
+ }
+ if exists {
+ return nil
+ }
+
+ // Try copy device ID from legacy location
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return fmt.Errorf("could not get user home: %w", err)
+ }
+ fpathLegacy := path.Join(homeDir, fpathIDLegacy)
+ exists, err = futil.FileExists(fpath)
+ if err != nil {
+ return err
+ }
+ if exists {
+ futil.CopyFile(fpathLegacy, fpath)
+ if err != nil {
+ return fmt.Errorf("could not copy device ID from legacy location: %w", err)
+ }
+ return nil
+ }
+
+ // Generate new device ID
+ rnd, err := uuid.NewRandom()
+ if err != nil {
+ return fmt.Errorf("could not get new random source: %w", err)
+ }
+ id := rnd.String()
+ if id == "" {
+ return fmt.Errorf("got invalid UUID from package")
+ }
+ err = os.WriteFile(fpath, []byte(id), filePerm)
+ if err != nil {
+ return fmt.Errorf("could not write generated ID to file: %w", err)
+ }
+ return nil
+}
+
+func promptAndWriteNameIfUnset(out *output.Output, dataDir string) error {
+ fpath := path.Join(dataDir, fnameName)
+ exists, err := futil.FileExists(fpath)
+ if err != nil {
+ return err
+ }
+ if exists {
+ return nil
+ }
+
+ name, err := promptForName(out, fpath)
+ if err != nil {
+ return fmt.Errorf("error while prompting for input: %w", err)
+ }
+ err = os.WriteFile(fpath, []byte(name), filePerm)
+ if err != nil {
+ return fmt.Errorf("could not write name to file: %w", err)
+ }
+ return nil
+}
+
+func promptForName(out *output.Output, fpath string) (string, error) {
+ // This function should be only ran from install-utils with attached terminal
+ if !isatty.IsTerminal(os.Stdout.Fd()) {
+ return "", fmt.Errorf("output is not a terminal - write name of this device to '%s' to bypass this error", fpath)
+ }
+ host, err := os.Hostname()
+ if err != nil {
+ return "", fmt.Errorf("could not get hostname (prompt default): %w", err)
+ }
+ hostStub := strings.Split(host, ".")[0]
+ fmt.Printf("\nPlease choose a short name for this device (default: '%s'): ", hostStub)
+ var input string
+ scanner := bufio.NewScanner(os.Stdin)
+ if scanner.Scan() {
+ input = scanner.Text()
+ }
+ if err = scanner.Err(); err != nil {
+ return "", fmt.Errorf("scanner error: %w", err)
+ }
+ if input == "" {
+ out.Info("Got no input - using default ...")
+ input = hostStub
+ }
+ out.Info(fmt.Sprintf("Device name set to '%s'", input))
+ fmt.Printf("You can change the device name at any time by editing '%s' file\n", fpath)
+ return input, nil
+}
diff --git a/internal/epochtime/epochtime.go b/internal/epochtime/epochtime.go
new file mode 100644
index 00000000..82d4dbc0
--- /dev/null
+++ b/internal/epochtime/epochtime.go
@@ -0,0 +1,14 @@
+package epochtime
+
+import (
+ "fmt"
+ "time"
+)
+
+func TimeToString(t time.Time) string {
+ return fmt.Sprintf("%.2f", float64(t.UnixMilli())/1000)
+}
+
+func Now() string {
+ return TimeToString(time.Now())
+}
diff --git a/internal/epochtime/epochtime_test.go b/internal/epochtime/epochtime_test.go
new file mode 100644
index 00000000..596ab132
--- /dev/null
+++ b/internal/epochtime/epochtime_test.go
@@ -0,0 +1,18 @@
+package epochtime
+
+import (
+ "strconv"
+ "testing"
+ "time"
+)
+
+func TestConversion(t *testing.T) {
+ epochTime := "1672702332.64"
+ seconds, err := strconv.ParseFloat(epochTime, 64)
+ if err != nil {
+ t.Fatal("Test setup failed: Failed to convert constant")
+ }
+ if TimeToString(time.UnixMilli(int64(seconds*1000))) != epochTime {
+ t.Fatal("EpochTime changed during conversion")
+ }
+}
diff --git a/internal/futil/futil.go b/internal/futil/futil.go
new file mode 100644
index 00000000..4cdbc249
--- /dev/null
+++ b/internal/futil/futil.go
@@ -0,0 +1,113 @@
+// futil implements common file-related utilities
+package futil
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "time"
+)
+
+func CopyFile(source, dest string) error {
+ from, err := os.Open(source)
+ if err != nil {
+ return err
+ }
+ defer from.Close()
+
+ // This is equivalent to: os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0666)
+ to, err := os.Create(dest)
+ if err != nil {
+ return err
+ }
+
+ _, err = io.Copy(to, from)
+ if err != nil {
+ return err
+ }
+ return to.Close()
+}
+
+func FileExists(fpath string) (bool, error) {
+ _, err := os.Stat(fpath)
+ if err == nil {
+ // File exists
+ return true, nil
+ }
+ if os.IsNotExist(err) {
+ // File doesn't exist
+ return false, nil
+ }
+ // Any other error
+ return false, fmt.Errorf("could not stat file: %w", err)
+}
+
+// TouchFile touches file
+// Returns true if file was created false otherwise
+func TouchFile(fpath string) (bool, error) {
+ exists, err := FileExists(fpath)
+ if err != nil {
+ return false, err
+ }
+
+ file, err := os.OpenFile(fpath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
+ if err != nil {
+ return false, fmt.Errorf("could not open/create file: %w", err)
+ }
+ err = file.Close()
+ if err != nil {
+ return false, fmt.Errorf("could not close file: %w", err)
+ }
+ return !exists, nil
+}
+
+func getBackupPath(fpath string) string {
+ ext := fmt.Sprintf(".backup-%d", time.Now().Unix())
+ return fpath + ext
+}
+
+// BackupFile backups file using unique suffix
+// Returns path to backup
+func BackupFile(fpath string) (*RestorableFile, error) {
+ fpathBackup := getBackupPath(fpath)
+ exists, err := FileExists(fpathBackup)
+ if err != nil {
+ return nil, err
+ }
+ if exists {
+ return nil, fmt.Errorf("backup already exists in the determined path")
+ }
+ err = CopyFile(fpath, fpathBackup)
+ if err != nil {
+ return nil, fmt.Errorf("failed to copy file: %w ", err)
+ }
+ rf := RestorableFile{
+ Path: fpath,
+ PathBackup: fpathBackup,
+ }
+ return &rf, nil
+}
+
+type RestorableFile struct {
+ Path string
+ PathBackup string
+}
+
+func (r RestorableFile) Restore() error {
+ return restoreFileFromBackup(r.Path, r.PathBackup)
+}
+
+func restoreFileFromBackup(fpath, fpathBak string) error {
+ exists, err := FileExists(fpathBak)
+ if err != nil {
+ return err
+ }
+ if !exists {
+ return fmt.Errorf("backup not found in given path: no such file or directory: %s", fpathBak)
+ }
+ err = CopyFile(fpathBak, fpath)
+ if err != nil {
+ return fmt.Errorf("failed to copy file: %w ", err)
+ }
+ return nil
+}
diff --git a/pkg/histcli/histcli.go b/internal/histcli/histcli.go
similarity index 50%
rename from pkg/histcli/histcli.go
rename to internal/histcli/histcli.go
index f9b66114..0b75decf 100644
--- a/pkg/histcli/histcli.go
+++ b/internal/histcli/histcli.go
@@ -1,31 +1,34 @@
package histcli
import (
- "github.com/curusarn/resh/pkg/records"
+ "github.com/curusarn/resh/internal/recordint"
+ "github.com/curusarn/resh/record"
+ "go.uber.org/zap"
)
// Histcli is a dump of history preprocessed for resh cli purposes
type Histcli struct {
// list of records
- List []records.CliRecord
+ List []recordint.SearchApp
+
+ sugar *zap.SugaredLogger
}
// New Histcli
-func New() Histcli {
+func New(sugar *zap.SugaredLogger) Histcli {
return Histcli{}
}
// AddRecord to the histcli
-func (h *Histcli) AddRecord(record records.Record) {
- enriched := records.Enriched(record)
- cli := records.NewCliRecord(enriched)
+func (h *Histcli) AddRecord(rec *record.V1) {
+ cli := recordint.NewSearchApp(h.sugar, rec)
h.List = append(h.List, cli)
}
// AddCmdLine to the histcli
func (h *Histcli) AddCmdLine(cmdline string) {
- cli := records.NewCliRecordFromCmdLine(cmdline)
+ cli := recordint.NewSearchAppFromCmdLine(cmdline)
h.List = append(h.List, cli)
}
diff --git a/internal/histfile/histfile.go b/internal/histfile/histfile.go
new file mode 100644
index 00000000..cdf04f93
--- /dev/null
+++ b/internal/histfile/histfile.go
@@ -0,0 +1,283 @@
+package histfile
+
+import (
+ "math"
+ "os"
+ "strconv"
+ "sync"
+
+ "github.com/curusarn/resh/internal/histcli"
+ "github.com/curusarn/resh/internal/histlist"
+ "github.com/curusarn/resh/internal/recio"
+ "github.com/curusarn/resh/internal/recordint"
+ "github.com/curusarn/resh/internal/records"
+ "github.com/curusarn/resh/internal/recutil"
+ "github.com/curusarn/resh/record"
+ "go.uber.org/zap"
+)
+
+// TODO: get rid of histfile - use histio instead
+// Histfile writes records to histfile
+type Histfile struct {
+ sugar *zap.SugaredLogger
+
+ sessionsMutex sync.Mutex
+ sessions map[string]recordint.Collect
+ historyPath string
+
+ // 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
+
+ cliRecords histcli.Histcli
+
+ rio *recio.RecIO
+}
+
+// New creates new histfile and runs its goroutines
+func New(sugar *zap.SugaredLogger, input chan recordint.Collect, sessionsToDrop chan string,
+ reshHistoryPath string, bashHistoryPath string, zshHistoryPath string,
+ maxInitHistSize int, minInitHistSizeKB int,
+ signals chan os.Signal, shutdownDone chan string) *Histfile {
+
+ rio := recio.New(sugar.With("module", "histfile"))
+ hf := Histfile{
+ sugar: sugar.With("module", "histfile"),
+ sessions: map[string]recordint.Collect{},
+ historyPath: reshHistoryPath,
+ bashCmdLines: histlist.New(sugar),
+ zshCmdLines: histlist.New(sugar),
+ cliRecords: histcli.New(sugar),
+ rio: &rio,
+ }
+ go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB)
+ go hf.writer(input, signals, shutdownDone)
+ go hf.sessionGC(sessionsToDrop)
+ return &hf
+}
+
+// load records from resh history, reverse, enrich and save
+func (h *Histfile) loadCliRecords(recs []record.V1) {
+ for _, cmdline := range h.bashCmdLines.List {
+ h.cliRecords.AddCmdLine(cmdline)
+ }
+ for _, cmdline := range h.zshCmdLines.List {
+ h.cliRecords.AddCmdLine(cmdline)
+ }
+ for i := len(recs) - 1; i >= 0; i-- {
+ rec := recs[i]
+ h.cliRecords.AddRecord(&rec)
+ }
+ h.sugar.Infow("Resh history loaded",
+ "historyRecordsCount", len(h.cliRecords.List),
+ )
+}
+
+// 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.sugar.Infow("Checking if resh_history is large enough ...")
+ fi, err := os.Stat(h.historyPath)
+ var size int
+ if err != nil {
+ h.sugar.Errorw("Failed to stat resh_history file", "error", err)
+ } else {
+ size = int(fi.Size())
+ }
+ useNativeHistories := false
+ if size/1024 < minInitHistSizeKB {
+ useNativeHistories = true
+ h.sugar.Warnw("Resh_history is too small - loading native bash and zsh history ...")
+ h.bashCmdLines = records.LoadCmdLinesFromBashFile(h.sugar, bashHistoryPath)
+ h.sugar.Infow("Bash history loaded", "cmdLineCount", len(h.bashCmdLines.List))
+ h.zshCmdLines = records.LoadCmdLinesFromZshFile(h.sugar, zshHistoryPath)
+ h.sugar.Infow("Zsh history loaded", "cmdLineCount", len(h.zshCmdLines.List))
+ // no maxInitHistSize when using native histories
+ maxInitHistSize = math.MaxInt32
+ }
+ h.sugar.Debugw("Loading resh history from file ...",
+ "historyFile", h.historyPath,
+ )
+ history, err := h.rio.ReadAndFixFile(h.historyPath, 3)
+ if err != nil {
+ h.sugar.Fatalf("Failed to read history file: %v", err)
+ }
+ h.sugar.Infow("Resh history loaded from file",
+ "historyFile", h.historyPath,
+ "recordCount", len(history),
+ )
+ go h.loadCliRecords(history)
+ // NOTE: keeping this weird interface for now because we might use it in the future
+ // when we only load bash or zsh history
+ reshCmdLines := loadCmdLines(h.sugar, history)
+ h.sugar.Infow("Resh history loaded and processed",
+ "recordCount", len(reshCmdLines.List),
+ )
+ if !useNativeHistories {
+ h.bashCmdLines = reshCmdLines
+ h.zshCmdLines = histlist.Copy(reshCmdLines)
+ return
+ }
+ h.bashCmdLines.AddHistlist(reshCmdLines)
+ h.sugar.Infow("Processed bash history and resh history together", "cmdLinecount", len(h.bashCmdLines.List))
+ h.zshCmdLines.AddHistlist(reshCmdLines)
+ h.sugar.Infow("Processed zsh history and resh history together", "cmdLineCount", len(h.zshCmdLines.List))
+}
+
+// sessionGC reads sessionIDs from channel and deletes them from histfile struct
+func (h *Histfile) sessionGC(sessionsToDrop chan string) {
+ for {
+ func() {
+ session := <-sessionsToDrop
+ sugar := h.sugar.With("sessionID", session)
+ sugar.Debugw("Got session to drop")
+ h.sessionsMutex.Lock()
+ defer h.sessionsMutex.Unlock()
+ if part1, found := h.sessions[session]; found == true {
+ sugar.Infow("Dropping session")
+ delete(h.sessions, session)
+ go h.rio.AppendToFile(h.historyPath, []record.V1{part1.Rec})
+ } else {
+ sugar.Infow("No hanging parts for session - nothing to drop")
+ }
+ }()
+ }
+}
+
+// writer reads records from channel, merges them and writes them to file
+func (h *Histfile) writer(collect chan recordint.Collect, signals chan os.Signal, shutdownDone chan string) {
+ for {
+ func() {
+ select {
+ case rec := <-collect:
+ part := "2"
+ if rec.Rec.PartOne {
+ part = "1"
+ }
+ sugar := h.sugar.With(
+ "recordCmdLine", rec.Rec.CmdLine,
+ "recordPart", part,
+ "recordShell", rec.Shell,
+ )
+ sugar.Debugw("Got record")
+ h.sessionsMutex.Lock()
+ defer h.sessionsMutex.Unlock()
+
+ // allows nested sessions to merge records properly
+ mergeID := rec.SessionID + "_" + strconv.Itoa(rec.Shlvl)
+ sugar = sugar.With("mergeID", mergeID)
+ if rec.Rec.PartOne {
+ if _, found := h.sessions[mergeID]; found {
+ msg := "Got another first part of the records before merging the previous one - overwriting!"
+ if rec.Shell == "zsh" {
+ sugar.Warnw(msg)
+ } else {
+ sugar.Infow(msg + " Unfortunately this is normal in bash, it can't be prevented.")
+ }
+ }
+ h.sessions[mergeID] = rec
+ } else {
+ if part1, found := h.sessions[mergeID]; found == false {
+ sugar.Warnw("Got second part of record and nothing to merge it with - ignoring!")
+ } else {
+ delete(h.sessions, mergeID)
+ go h.mergeAndWriteRecord(sugar, part1, rec)
+ }
+ }
+ case sig := <-signals:
+ sugar := h.sugar.With(
+ "signal", sig.String(),
+ )
+ sugar.Infow("Got signal")
+ h.sessionsMutex.Lock()
+ defer h.sessionsMutex.Unlock()
+ sugar.Debugw("Unlocked mutex")
+
+ for sessID, rec := range h.sessions {
+ sugar.Warnw("Writing incomplete record for session",
+ "sessionID", sessID,
+ )
+ h.writeRecord(sugar, rec.Rec)
+ }
+ sugar.Debugw("Shutdown successful")
+ shutdownDone <- "histfile"
+ return
+ }
+ }()
+ }
+}
+
+func (h *Histfile) writeRecord(sugar *zap.SugaredLogger, rec record.V1) {
+ h.rio.AppendToFile(h.historyPath, []record.V1{rec})
+}
+
+func (h *Histfile) mergeAndWriteRecord(sugar *zap.SugaredLogger, part1 recordint.Collect, part2 recordint.Collect) {
+ rec, err := recutil.Merge(&part1, &part2)
+ if err != nil {
+ sugar.Errorw("Error while merging records", "error", err)
+ return
+ }
+
+ recV1 := record.V1(rec)
+ func() {
+ cmdLine := rec.CmdLine
+ h.bashCmdLines.AddCmdLine(cmdLine)
+ h.zshCmdLines.AddCmdLine(cmdLine)
+ h.cliRecords.AddRecord(&recV1)
+ }()
+
+ h.rio.AppendToFile(h.historyPath, []record.V1{recV1})
+}
+
+// TODO: use errors in RecIO
+// func writeRecord(sugar *zap.SugaredLogger, rec record.V1, outputPath string) {
+// recJSON, err := json.Marshal(rec)
+// if err != nil {
+// sugar.Errorw("Marshalling error", "error", err)
+// return
+// }
+// f, err := os.OpenFile(outputPath,
+// os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+// if err != nil {
+// sugar.Errorw("Could not open file", "error", err)
+// return
+// }
+// defer f.Close()
+// _, err = f.Write(append(recJSON, []byte("\n")...))
+// if err != nil {
+// sugar.Errorw("Error while writing record",
+// "recordRaw", rec,
+// "error", err,
+// )
+// return
+// }
+// }
+
+// DumpCliRecords returns enriched records
+func (h *Histfile) DumpCliRecords() histcli.Histcli {
+ // don't forget locks in the future
+ return h.cliRecords
+}
+
+func loadCmdLines(sugar *zap.SugaredLogger, recs []record.V1) histlist.Histlist {
+ hl := histlist.New(sugar)
+ // go from bottom and deduplicate
+ var cmdLines []string
+ cmdLinesSet := map[string]bool{}
+ for i := len(recs) - 1; i >= 0; i-- {
+ cmdLine := recs[i].CmdLine
+ if cmdLinesSet[cmdLine] {
+ continue
+ }
+ cmdLinesSet[cmdLine] = true
+ cmdLines = append([]string{cmdLine}, cmdLines...)
+ // if len(cmdLines) > limit {
+ // break
+ // }
+ }
+ // add everything to histlist
+ for _, cmdLine := range cmdLines {
+ hl.AddCmdLine(cmdLine)
+ }
+ return hl
+}
diff --git a/internal/histio/file.go b/internal/histio/file.go
new file mode 100644
index 00000000..40f2544b
--- /dev/null
+++ b/internal/histio/file.go
@@ -0,0 +1,56 @@
+package histio
+
+import (
+ "fmt"
+ "os"
+ "sync"
+
+ "github.com/curusarn/resh/internal/recio"
+ "github.com/curusarn/resh/record"
+ "go.uber.org/zap"
+)
+
+type histfile struct {
+ sugar *zap.SugaredLogger
+ // deviceID string
+ path string
+
+ mu sync.RWMutex
+ data []record.V1
+ fileinfo os.FileInfo
+}
+
+func newHistfile(sugar *zap.SugaredLogger, path string) *histfile {
+ return &histfile{
+ sugar: sugar.With(
+ // FIXME: drop V1 once original histfile is gone
+ "component", "histfileV1",
+ "path", path,
+ ),
+ // deviceID: deviceID,
+ path: path,
+ }
+}
+
+func (h *histfile) updateFromFile() error {
+ rio := recio.New(h.sugar)
+ // TODO: decide and handle errors
+ newData, _, err := rio.ReadFile(h.path)
+ if err != nil {
+ return fmt.Errorf("could not read history file: %w", err)
+ }
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ h.data = newData
+ h.updateFileInfo()
+ return nil
+}
+
+func (h *histfile) updateFileInfo() error {
+ info, err := os.Stat(h.path)
+ if err != nil {
+ return fmt.Errorf("history file not found: %w", err)
+ }
+ h.fileinfo = info
+ return nil
+}
diff --git a/internal/histio/histio.go b/internal/histio/histio.go
new file mode 100644
index 00000000..001623ad
--- /dev/null
+++ b/internal/histio/histio.go
@@ -0,0 +1,43 @@
+package histio
+
+import (
+ "path"
+
+ "github.com/curusarn/resh/record"
+ "go.uber.org/zap"
+)
+
+type Histio struct {
+ sugar *zap.SugaredLogger
+ histDir string
+
+ thisDeviceID string
+ thisHistory *histfile
+ // TODO: remote histories
+ // moreHistories map[string]*histfile
+
+ recordsToAppend chan record.V1
+ // recordsToFlag chan recordint.Flag
+}
+
+func New(sugar *zap.SugaredLogger, dataDir, deviceID string) *Histio {
+ sugarHistio := sugar.With(zap.String("component", "histio"))
+ histDir := path.Join(dataDir, "history")
+ currPath := path.Join(histDir, deviceID)
+ // TODO: file extension for the history, yes or no? (.reshjson vs. )
+
+ // TODO: discover other history files, exclude current
+
+ return &Histio{
+ sugar: sugarHistio,
+ histDir: histDir,
+
+ thisDeviceID: deviceID,
+ thisHistory: newHistfile(sugar, currPath),
+ // moreHistories: ...
+ }
+}
+
+func (h *Histio) Append(r *record.V1) {
+
+}
diff --git a/pkg/histlist/histlist.go b/internal/histlist/histlist.go
similarity index 62%
rename from pkg/histlist/histlist.go
rename to internal/histlist/histlist.go
index a3f4334b..7c80c7f4 100644
--- a/pkg/histlist/histlist.go
+++ b/internal/histlist/histlist.go
@@ -1,9 +1,11 @@
package histlist
-import "log"
+import "go.uber.org/zap"
// Histlist is a deduplicated list of cmdLines
type Histlist struct {
+ // TODO: I'm not excited about logger being passed here
+ sugar *zap.SugaredLogger
// list of commands lines (deduplicated)
List []string
// lookup: cmdLine -> last index
@@ -11,13 +13,16 @@ type Histlist struct {
}
// New Histlist
-func New() Histlist {
- return Histlist{LastIndex: make(map[string]int)}
+func New(sugar *zap.SugaredLogger) Histlist {
+ return Histlist{
+ sugar: sugar.With("component", "histlist"),
+ LastIndex: make(map[string]int),
+ }
}
// Copy Histlist
func Copy(hl Histlist) Histlist {
- newHl := New()
+ newHl := New(hl.sugar)
// copy list
newHl.List = make([]string, len(hl.List))
copy(newHl.List, hl.List)
@@ -36,7 +41,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) {
if found {
// remove duplicate
if cmdLine != h.List[idx] {
- log.Println("histlist ERROR: Adding cmdLine:", cmdLine, " != LastIndex[cmdLine]:", h.List[idx])
+ h.sugar.DPanicw("Index key is different than actual cmd line in the list",
+ "indexKeyCmdLine", cmdLine,
+ "actualCmdLine", h.List[idx],
+ )
}
h.List = append(h.List[:idx], h.List[idx+1:]...)
// idx++
@@ -44,7 +52,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) {
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])
+ h.sugar.DPanicw("Index position is different than actual position of the cmd line",
+ "actualPosition", idx,
+ "indexedPosition", h.LastIndex[cmdLn],
+ )
}
idx++
}
@@ -53,7 +64,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) {
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))
+ h.sugar.Debugw("Added cmdLine",
+ "cmdLine", cmdLine,
+ "historyLength", len(h.List),
+ )
}
// AddHistlist contents of another histlist to this histlist
diff --git a/pkg/httpclient/httpclient.go b/internal/httpclient/httpclient.go
similarity index 100%
rename from pkg/httpclient/httpclient.go
rename to internal/httpclient/httpclient.go
diff --git a/internal/logger/logger.go b/internal/logger/logger.go
new file mode 100644
index 00000000..ef25d72e
--- /dev/null
+++ b/internal/logger/logger.go
@@ -0,0 +1,27 @@
+package logger
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/curusarn/resh/internal/datadir"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+func New(executable string, level zapcore.Level, development string) (*zap.Logger, error) {
+ dataDir, err := datadir.MakePath()
+ if err != nil {
+ return nil, fmt.Errorf("error while getting RESH data dir: %w", err)
+ }
+ logPath := filepath.Join(dataDir, "log.json")
+ loggerConfig := zap.NewProductionConfig()
+ loggerConfig.OutputPaths = []string{logPath}
+ loggerConfig.Level.SetLevel(level)
+ loggerConfig.Development = development == "true" // DPanic panics in development
+ logger, err := loggerConfig.Build()
+ if err != nil {
+ return logger, fmt.Errorf("error while creating logger: %w", err)
+ }
+ return logger.With(zap.String("executable", executable)), err
+}
diff --git a/internal/msg/msg.go b/internal/msg/msg.go
new file mode 100644
index 00000000..7d3b103b
--- /dev/null
+++ b/internal/msg/msg.go
@@ -0,0 +1,21 @@
+package msg
+
+import "github.com/curusarn/resh/internal/recordint"
+
+// CliMsg struct
+type CliMsg struct {
+ SessionID string
+ PWD string
+}
+
+// CliResponse struct
+type CliResponse struct {
+ Records []recordint.SearchApp
+}
+
+// StatusResponse struct
+type StatusResponse struct {
+ Status bool `json:"status"`
+ Version string `json:"version"`
+ Commit string `json:"commit"`
+}
diff --git a/internal/normalize/normailze.go b/internal/normalize/normailze.go
new file mode 100644
index 00000000..6bd72039
--- /dev/null
+++ b/internal/normalize/normailze.go
@@ -0,0 +1,31 @@
+package normalize
+
+import (
+ "net/url"
+ "strings"
+
+ giturls "github.com/whilp/git-urls"
+ "go.uber.org/zap"
+)
+
+// GitRemote helper
+// Returns normalized git remote - valid even on error
+func GitRemote(sugar *zap.SugaredLogger, gitRemote string) string {
+ if len(gitRemote) == 0 {
+ return ""
+ }
+ gitRemote = strings.TrimSuffix(gitRemote, ".git")
+ parsedURL, err := giturls.Parse(gitRemote)
+ if err != nil {
+ sugar.Errorw("Failed to parse git remote", zap.Error(err),
+ "gitRemote", gitRemote,
+ )
+ return gitRemote
+ }
+ if parsedURL.User == nil || parsedURL.User.Username() == "" {
+ parsedURL.User = url.User("git")
+ }
+ // TODO: figure out what scheme we want
+ parsedURL.Scheme = "git+ssh"
+ return parsedURL.String()
+}
diff --git a/internal/normalize/normalize_test.go b/internal/normalize/normalize_test.go
new file mode 100644
index 00000000..a85b5383
--- /dev/null
+++ b/internal/normalize/normalize_test.go
@@ -0,0 +1,51 @@
+package normalize_test
+
+import (
+ "testing"
+
+ "github.com/curusarn/resh/internal/normalize"
+ "go.uber.org/zap"
+)
+
+// TestLeftCutPadString
+func TestGitRemote(t *testing.T) {
+ sugar := zap.NewNop().Sugar()
+
+ data := [][]string{
+ {
+ "git@github.com:curusarn/resh.git", // git
+ "git@github.com:curusarn/resh", // git no ".git"
+ "http://github.com/curusarn/resh.git", // http
+ "https://github.com/curusarn/resh.git", // https
+ "ssh://git@github.com/curusarn/resh.git", // ssh
+ "git+ssh://git@github.com/curusarn/resh.git", // git+ssh
+ },
+ {
+ "git@host.example.com:org/user/repo.git", // git
+ "git@host.example.com:org/user/repo", // git no ".git"
+ "http://host.example.com/org/user/repo.git", // http
+ "https://host.example.com/org/user/repo.git", // https
+ "ssh://git@host.example.com/org/user/repo.git", // ssh
+ "git+ssh://git@host.example.com/org/user/repo.git", // git+ssh
+ },
+ }
+
+ for _, arr := range data {
+ n := len(arr)
+ for i := 0; i < n-1; i++ {
+ for j := i + 1; j < n; j++ {
+ one := normalize.GitRemote(sugar, arr[i])
+ two := normalize.GitRemote(sugar, arr[j])
+ if one != two {
+ t.Fatalf("Normalized git remotes should match for '%s' and '%s'\n -> got '%s' != '%s'",
+ arr[i], arr[j], one, two)
+ }
+ }
+ }
+ }
+
+ empty := normalize.GitRemote(sugar, "")
+ if len(empty) != 0 {
+ t.Fatalf("Normalized git remotes for '' should be ''\n -> got '%s'", empty)
+ }
+}
diff --git a/internal/opt/opt.go b/internal/opt/opt.go
new file mode 100644
index 00000000..efbada2c
--- /dev/null
+++ b/internal/opt/opt.go
@@ -0,0 +1,36 @@
+package opt
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/curusarn/resh/internal/output"
+)
+
+// HandleVersionOpts reads the first option and handles it
+// This is a helper for resh-{collect,postcollect,session-init} commands
+func HandleVersionOpts(out *output.Output, args []string, version, commit string) []string {
+ if len(os.Args) == 0 {
+ return os.Args[1:]
+ }
+ // We use go-like options because of backwards compatibility.
+ // Not ideal but we should support them because they have worked once
+ // and adding "more correct" variants would mean supporting more variants.
+ switch os.Args[1] {
+ case "-version":
+ fmt.Print(version)
+ os.Exit(0)
+ case "-revision":
+ fmt.Print(commit)
+ os.Exit(0)
+ case "-requireVersion":
+ if len(os.Args) < 3 {
+ out.FatalTerminalVersionMismatch(version, "")
+ }
+ if os.Args[2] != version {
+ out.FatalTerminalVersionMismatch(version, os.Args[2])
+ }
+ return os.Args[3:]
+ }
+ return os.Args[1:]
+}
diff --git a/internal/output/output.go b/internal/output/output.go
new file mode 100644
index 00000000..4f7840de
--- /dev/null
+++ b/internal/output/output.go
@@ -0,0 +1,143 @@
+package output
+
+import (
+ "fmt"
+ "os"
+
+ "go.uber.org/zap"
+)
+
+// Output wrapper for writing to logger and stdout/stderr at the same time
+// useful for errors that should be presented to the user
+type Output struct {
+ Logger *zap.Logger
+ ErrPrefix string
+}
+
+func New(logger *zap.Logger, prefix string) *Output {
+ return &Output{
+ Logger: logger,
+ ErrPrefix: prefix,
+ }
+}
+
+// Info outputs string to stdout and to log (as info)
+// This is how we write output to users from interactive commands
+// This way we have full record in logs
+func (f *Output) Info(msg string) {
+ fmt.Printf("%s\n", msg)
+ f.Logger.Info(msg)
+}
+
+// InfoE outputs string to stdout and to log (as error)
+// Passed error is only written to log
+// This is how we output errors to users from interactive commands
+// This way we have errors in logs
+func (f *Output) InfoE(msg string, err error) {
+ fmt.Printf("%s\n", msg)
+ f.Logger.Error(msg, zap.Error(err))
+}
+
+// Error outputs string to stderr and to log (as error)
+// This is how we output errors from non-interactive commands
+func (f *Output) Error(msg string) {
+ fmt.Fprintf(os.Stderr, "%s: %s\n", f.ErrPrefix, msg)
+ f.Logger.Error(msg)
+}
+
+// ErrorE outputs string and error to stderr and to log (as error)
+// This is how we output errors from non-interactive commands
+func (f *Output) ErrorE(msg string, err error) {
+ fmt.Fprintf(os.Stderr, "%s: %s: %v\n", f.ErrPrefix, msg, err)
+ f.Logger.Error(msg, zap.Error(err))
+}
+
+// FatalE outputs string and error to stderr and to log (as fatal)
+// This is how we raise fatal errors from non-interactive commands
+func (f *Output) FatalE(msg string, err error) {
+ fmt.Fprintf(os.Stderr, "%s: %s: %v\n", f.ErrPrefix, msg, err)
+ f.Logger.Fatal(msg, zap.Error(err))
+}
+
+var msgDaemonNotRunning = `RESH daemon didn't respond - it's probably not running.
+ -> Start RESH daemon manually - run: resh-daemon-start
+ -> Or restart this terminal window to bring RESH daemon back up
+ -> You can check logs: ~/.local/share/resh/log.json (or ~/$XDG_DATA_HOME/resh/log.json)
+ -> You can create an issue at: https://github.com/curusarn/resh/issues
+
+`
+var msgTerminalVersionMismatch = `This terminal session was started with different RESH version than is installed now.
+It looks like you updated RESH and didn't restart this terminal.
+ -> Restart this terminal window to fix that
+
+`
+
+var msgDaemonVersionMismatch = `RESH daemon is running in different version than is installed now.
+It looks like something went wrong during RESH update.
+ -> Kill resh-daemon and then launch a new terminal window to fix that: killall resh-daemon
+ -> You can create an issue at: https://github.com/curusarn/resh/issues
+
+`
+
+func (f *Output) InfoDaemonNotRunning(err error) {
+ fmt.Printf("%s", msgDaemonNotRunning)
+ f.Logger.Error("Daemon is not running", zap.Error(err))
+}
+
+func (f *Output) ErrorDaemonNotRunning(err error) {
+ fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgDaemonNotRunning)
+ f.Logger.Error("Daemon is not running", zap.Error(err))
+}
+
+func (f *Output) FatalDaemonNotRunning(err error) {
+ fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgDaemonNotRunning)
+ f.Logger.Fatal("Daemon is not running", zap.Error(err))
+}
+
+func (f *Output) InfoTerminalVersionMismatch(installedVer, terminalVer string) {
+ fmt.Printf("%s(installed version: %s, this terminal version: %s)\n\n",
+ msgTerminalVersionMismatch, installedVer, terminalVer)
+ f.Logger.Fatal("Version mismatch",
+ zap.String("installed", installedVer),
+ zap.String("terminal", terminalVer))
+}
+
+func (f *Output) ErrorTerminalVersionMismatch(installedVer, terminalVer string) {
+ fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, this terminal version: %s)\n\n",
+ f.ErrPrefix, msgTerminalVersionMismatch, installedVer, terminalVer)
+ f.Logger.Fatal("Version mismatch",
+ zap.String("installed", installedVer),
+ zap.String("terminal", terminalVer))
+}
+
+func (f *Output) FatalTerminalVersionMismatch(installedVer, terminalVer string) {
+ fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, this terminal version: %s)\n\n",
+ f.ErrPrefix, msgTerminalVersionMismatch, installedVer, terminalVer)
+ f.Logger.Fatal("Version mismatch",
+ zap.String("installed", installedVer),
+ zap.String("terminal", terminalVer))
+}
+
+func (f *Output) InfoDaemonVersionMismatch(installedVer, daemonVer string) {
+ fmt.Printf("%s(installed version: %s, running daemon version: %s)\n\n",
+ msgDaemonVersionMismatch, installedVer, daemonVer)
+ f.Logger.Error("Version mismatch",
+ zap.String("installed", installedVer),
+ zap.String("daemon", daemonVer))
+}
+
+func (f *Output) ErrorDaemonVersionMismatch(installedVer, daemonVer string) {
+ fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, running daemon version: %s)\n\n",
+ f.ErrPrefix, msgDaemonVersionMismatch, installedVer, daemonVer)
+ f.Logger.Error("Version mismatch",
+ zap.String("installed", installedVer),
+ zap.String("daemon", daemonVer))
+}
+
+func (f *Output) FatalDaemonVersionMismatch(installedVer, daemonVer string) {
+ fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, running daemon version: %s)\n\n",
+ f.ErrPrefix, msgDaemonVersionMismatch, installedVer, daemonVer)
+ f.Logger.Fatal("Version mismatch",
+ zap.String("installed", installedVer),
+ zap.String("daemon", daemonVer))
+}
diff --git a/internal/recconv/recconv.go b/internal/recconv/recconv.go
new file mode 100644
index 00000000..7690ec43
--- /dev/null
+++ b/internal/recconv/recconv.go
@@ -0,0 +1,37 @@
+package recconv
+
+import (
+ "fmt"
+
+ "github.com/curusarn/resh/record"
+)
+
+func LegacyToV1(r *record.Legacy) *record.V1 {
+ return &record.V1{
+ // FIXME: fill in all the fields
+
+ // Flags: 0,
+
+ CmdLine: r.CmdLine,
+ ExitCode: r.ExitCode,
+
+ DeviceID: r.ReshUUID,
+ SessionID: r.SessionID,
+ RecordID: r.RecordID,
+
+ Home: r.Home,
+ Pwd: r.Pwd,
+ RealPwd: r.RealPwd,
+
+ // Logname: r.Login,
+ Device: r.Host,
+
+ GitOriginRemote: r.GitOriginRemote,
+
+ Time: fmt.Sprintf("%.4f", r.RealtimeBefore),
+ Duration: fmt.Sprintf("%.4f", r.RealtimeDuration),
+
+ PartOne: r.PartOne,
+ PartsNotMerged: !r.PartsMerged,
+ }
+}
diff --git a/internal/recio/read.go b/internal/recio/read.go
new file mode 100644
index 00000000..e88d86c8
--- /dev/null
+++ b/internal/recio/read.go
@@ -0,0 +1,144 @@
+package recio
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "github.com/curusarn/resh/internal/futil"
+ "github.com/curusarn/resh/internal/recconv"
+ "github.com/curusarn/resh/record"
+ "go.uber.org/zap"
+)
+
+func (r *RecIO) ReadAndFixFile(fpath string, maxErrors int) ([]record.V1, error) {
+ recs, decodeErrs, err := r.ReadFile(fpath)
+ if err != nil {
+ return nil, err
+ }
+ numErrs := len(decodeErrs)
+ if numErrs > maxErrors {
+ r.sugar.Errorw("Encountered too many decoding errors",
+ "errorsCount", numErrs,
+ "individualErrors", "",
+ )
+ return nil, fmt.Errorf("encountered too many decoding errors, last error: %w", decodeErrs[len(decodeErrs)-1])
+ }
+ if numErrs == 0 {
+ return recs, nil
+ }
+
+ r.sugar.Warnw("Some history records could not be decoded - fixing RESH history file by dropping them",
+ "corruptedRecords", numErrs,
+ "lastError", decodeErrs[len(decodeErrs)-1],
+ "individualErrors", "",
+ )
+
+ fpathBak := fpath + ".bak"
+ r.sugar.Infow("Backing up current corrupted history file",
+ "historyFileBackup", fpathBak,
+ )
+ err = futil.CopyFile(fpath, fpathBak)
+ if err != nil {
+ r.sugar.Errorw("Failed to create a backup history file - aborting fixing history file",
+ "historyFileBackup", fpathBak,
+ zap.Error(err),
+ )
+ return recs, nil
+ }
+ r.sugar.Info("Writing resh history file without errors ...")
+ err = r.OverwriteFile(fpath, recs)
+ if err != nil {
+ r.sugar.Errorw("Failed write fixed history file - restoring history file from backup",
+ "historyFile", fpath,
+ zap.Error(err),
+ )
+
+ err = futil.CopyFile(fpathBak, fpath)
+ if err != nil {
+ r.sugar.Errorw("Failed restore history file from backup",
+ "historyFile", fpath,
+ "HistoryFileBackup", fpathBak,
+ zap.Error(err),
+ )
+ }
+ }
+ return recs, nil
+}
+
+func (r *RecIO) ReadFile(fpath string) ([]record.V1, []error, error) {
+ var recs []record.V1
+ file, err := os.Open(fpath)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to open history file: %w", err)
+ }
+ defer file.Close()
+
+ reader := bufio.NewReader(file)
+ var decodeErrs []error
+ for {
+ var line string
+ line, err = reader.ReadString('\n')
+ if err != nil {
+ break
+ }
+ rec, err := r.decodeLine(line)
+ if err != nil {
+ r.sugar.Errorw("Error while decoding line", zap.Error(err),
+ "filePath", fpath,
+ "line", line,
+ )
+ decodeErrs = append(decodeErrs, err)
+ continue
+ }
+ recs = append(recs, *rec)
+ }
+ if err != io.EOF {
+ r.sugar.Error("Error while reading file", zap.Error(err))
+ return recs, decodeErrs, err
+ }
+ r.sugar.Infow("Loaded resh history records",
+ "recordCount", len(recs),
+ )
+ return recs, decodeErrs, nil
+}
+
+func (r *RecIO) decodeLine(line string) (*record.V1, error) {
+ idx := strings.Index(line, "{")
+ if idx == -1 {
+ return nil, fmt.Errorf("no opening brace found")
+ }
+ schema := line[:idx]
+ jsn := line[idx:]
+ switch schema {
+ case "v1":
+ var rec record.V1
+ err := decodeAnyRecord(jsn, &rec)
+ if err != nil {
+ return nil, err
+ }
+ return &rec, nil
+ case "":
+ var rec record.Legacy
+ err := decodeAnyRecord(jsn, &rec)
+ if err != nil {
+ return nil, err
+ }
+ return recconv.LegacyToV1(&rec), nil
+ default:
+ return nil, fmt.Errorf("unknown record schema/type '%s'", schema)
+ }
+}
+
+// TODO: find out if we are loosing performance because of the use of interface{}
+
+func decodeAnyRecord(jsn string, rec interface{}) error {
+ err := json.Unmarshal([]byte(jsn), &rec)
+ if err != nil {
+ return fmt.Errorf("failed to decode json: %w", err)
+ }
+ return nil
+}
diff --git a/internal/recio/recio.go b/internal/recio/recio.go
new file mode 100644
index 00000000..5ea986ba
--- /dev/null
+++ b/internal/recio/recio.go
@@ -0,0 +1,13 @@
+package recio
+
+import (
+ "go.uber.org/zap"
+)
+
+type RecIO struct {
+ sugar *zap.SugaredLogger
+}
+
+func New(sugar *zap.SugaredLogger) RecIO {
+ return RecIO{sugar: sugar}
+}
diff --git a/internal/recio/write.go b/internal/recio/write.go
new file mode 100644
index 00000000..a772d165
--- /dev/null
+++ b/internal/recio/write.go
@@ -0,0 +1,64 @@
+package recio
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/curusarn/resh/record"
+)
+
+func (r *RecIO) OverwriteFile(fpath string, recs []record.V1) error {
+ file, err := os.Create(fpath)
+ if err != nil {
+ return fmt.Errorf("could not create/truncate file: %w", err)
+ }
+ err = writeRecords(file, recs)
+ if err != nil {
+ return fmt.Errorf("error while writing records: %w", err)
+ }
+ err = file.Close()
+ if err != nil {
+ return fmt.Errorf("could not close file: %w", err)
+ }
+ return nil
+}
+
+func (r *RecIO) AppendToFile(fpath string, recs []record.V1) error {
+ file, err := os.OpenFile(fpath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ return fmt.Errorf("could not open/create file: %w", err)
+ }
+ err = writeRecords(file, recs)
+ if err != nil {
+ return fmt.Errorf("error while writing records: %w", err)
+ }
+ err = file.Close()
+ if err != nil {
+ return fmt.Errorf("could not close file: %w", err)
+ }
+ return nil
+}
+
+func writeRecords(file *os.File, recs []record.V1) error {
+ for _, rec := range recs {
+ jsn, err := encodeV1Record(rec)
+ if err != nil {
+ return fmt.Errorf("could not encode record: %w", err)
+ }
+ _, err = file.Write(jsn)
+ if err != nil {
+ return fmt.Errorf("could not write json: %w", err)
+ }
+ }
+ return nil
+}
+
+func encodeV1Record(rec record.V1) ([]byte, error) {
+ version := []byte("v1")
+ jsn, err := json.Marshal(rec)
+ if err != nil {
+ return nil, fmt.Errorf("failed to encode json: %w", err)
+ }
+ return append(append(version, jsn...), []byte("\n")...), nil
+}
diff --git a/internal/recordint/collect.go b/internal/recordint/collect.go
new file mode 100644
index 00000000..1a7d6a74
--- /dev/null
+++ b/internal/recordint/collect.go
@@ -0,0 +1,34 @@
+package recordint
+
+import "github.com/curusarn/resh/record"
+
+type Collect struct {
+ // record merging
+ SessionID string
+ Shlvl int
+ // session watching
+ SessionPID int
+ Shell string
+
+ Rec record.V1
+}
+
+type Postcollect struct {
+ // record merging
+ SessionID string
+ Shlvl int
+ // session watching
+ SessionPID int
+
+ RecordID string
+ ExitCode int
+ Duration float64
+}
+
+type SessionInit struct {
+ // record merging
+ SessionID string
+ Shlvl int
+ // session watching
+ SessionPID int
+}
diff --git a/internal/recordint/recordint.go b/internal/recordint/recordint.go
new file mode 100644
index 00000000..73457cd7
--- /dev/null
+++ b/internal/recordint/recordint.go
@@ -0,0 +1,2 @@
+// Package recordint provides internal record types that are passed between resh components
+package recordint
diff --git a/internal/recordint/searchapp.go b/internal/recordint/searchapp.go
new file mode 100644
index 00000000..5ccc537f
--- /dev/null
+++ b/internal/recordint/searchapp.go
@@ -0,0 +1,56 @@
+package recordint
+
+import (
+ "strconv"
+
+ "github.com/curusarn/resh/internal/normalize"
+ "github.com/curusarn/resh/record"
+ "go.uber.org/zap"
+)
+
+// SearchApp record used for sending records to RESH-CLI
+type SearchApp struct {
+ IsRaw bool
+ SessionID string
+ DeviceID string
+
+ CmdLine string
+ Host string
+ Pwd string
+ Home string // helps us to collapse /home/user to tilde
+ GitOriginRemote string
+ ExitCode int
+
+ Time float64
+
+ // file index
+ Idx int
+}
+
+func NewSearchAppFromCmdLine(cmdLine string) SearchApp {
+ return SearchApp{
+ IsRaw: true,
+ CmdLine: cmdLine,
+ }
+}
+
+// The error handling here could be better
+func NewSearchApp(sugar *zap.SugaredLogger, r *record.V1) SearchApp {
+ time, err := strconv.ParseFloat(r.Time, 64)
+ if err != nil {
+ sugar.Errorw("Error while parsing time as float", zap.Error(err),
+ "time", time)
+ }
+ return SearchApp{
+ IsRaw: false,
+ SessionID: r.SessionID,
+ CmdLine: r.CmdLine,
+ Host: r.Device,
+ Pwd: r.Pwd,
+ Home: r.Home,
+ // TODO: is this the right place to normalize the git remote?
+ GitOriginRemote: normalize.GitRemote(sugar, r.GitOriginRemote),
+ ExitCode: r.ExitCode,
+ Time: time,
+ }
+}
diff --git a/internal/records/records.go b/internal/records/records.go
new file mode 100644
index 00000000..699a50a5
--- /dev/null
+++ b/internal/records/records.go
@@ -0,0 +1,84 @@
+package records
+
+// DEPRECATION NOTICE: This package should be removed in favor of:
+// - record: public record definitions
+// - recordint: internal record definitions
+// - recutil: record-related utils
+
+import (
+ "bufio"
+ "os"
+ "strings"
+
+ "github.com/curusarn/resh/internal/histlist"
+ "go.uber.org/zap"
+)
+
+// LoadCmdLinesFromZshFile loads cmdlines from zsh history file
+func LoadCmdLinesFromZshFile(sugar *zap.SugaredLogger, fname string) histlist.Histlist {
+ hl := histlist.New(sugar)
+ file, err := os.Open(fname)
+ if err != nil {
+ sugar.Error("Failed to open zsh history file - skipping reading zsh history", zap.Error(err))
+ return hl
+ }
+ defer file.Close()
+
+ 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(sugar *zap.SugaredLogger, fname string) histlist.Histlist {
+ hl := histlist.New(sugar)
+ file, err := os.Open(fname)
+ if err != nil {
+ sugar.Error("Failed to open bash history file - skipping reading bash history", zap.Error(err))
+ return hl
+ }
+ defer file.Close()
+
+ 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/internal/recutil/recutil.go b/internal/recutil/recutil.go
new file mode 100644
index 00000000..1862741a
--- /dev/null
+++ b/internal/recutil/recutil.go
@@ -0,0 +1,51 @@
+package recutil
+
+import (
+ "errors"
+
+ "github.com/curusarn/resh/internal/recordint"
+ "github.com/curusarn/resh/record"
+)
+
+// TODO: reintroduce validation
+// Validate returns error if the record is invalid
+// func Validate(r *record.V1) error {
+// if r.CmdLine == "" {
+// return errors.New("There is no CmdLine")
+// }
+// if r.Time == 0 {
+// return errors.New("There is no Time")
+// }
+// if r.RealPwd == "" {
+// return errors.New("There is no Real Pwd")
+// }
+// if r.Pwd == "" {
+// return errors.New("There is no Pwd")
+// }
+// return nil
+// }
+
+// TODO: maybe more to a more appropriate place
+// TODO: cleanup the interface - stop modifying the part1 and returning a new record at the same time
+// Merge two records (part1 - collect + part2 - postcollect)
+func Merge(r1 *recordint.Collect, r2 *recordint.Collect) (record.V1, error) {
+ if r1.SessionID != r2.SessionID {
+ return record.V1{}, errors.New("Records to merge are not from the same session - r1:" + r1.SessionID + " r2:" + r2.SessionID)
+ }
+ if r1.Rec.RecordID != r2.Rec.RecordID {
+ return record.V1{}, errors.New("Records to merge do not have the same ID - r1:" + r1.Rec.RecordID + " r2:" + r2.Rec.RecordID)
+ }
+
+ r := recordint.Collect{
+ SessionID: r1.SessionID,
+ Shlvl: r1.Shlvl,
+ SessionPID: r1.SessionPID,
+
+ Rec: r1.Rec,
+ }
+ r.Rec.ExitCode = r2.Rec.ExitCode
+ r.Rec.Duration = r2.Rec.Duration
+ r.Rec.PartOne = false
+ r.Rec.PartsNotMerged = false
+ return r.Rec, nil
+}
diff --git a/pkg/searchapp/highlight.go b/internal/searchapp/highlight.go
similarity index 100%
rename from pkg/searchapp/highlight.go
rename to internal/searchapp/highlight.go
diff --git a/pkg/searchapp/item.go b/internal/searchapp/item.go
similarity index 86%
rename from pkg/searchapp/item.go
rename to internal/searchapp/item.go
index 6908c258..96603485 100644
--- a/pkg/searchapp/item.go
+++ b/internal/searchapp/item.go
@@ -2,13 +2,13 @@ package searchapp
import (
"fmt"
- "log"
"math"
"strconv"
"strings"
"time"
+ "unicode"
- "github.com/curusarn/resh/pkg/records"
+ "github.com/curusarn/resh/internal/recordint"
"golang.org/x/exp/utf8string"
)
@@ -19,7 +19,7 @@ const dots = "…"
type Item struct {
isRaw bool
- realtimeBefore float64
+ time float64
// [host:]pwd
differentHost bool
@@ -32,8 +32,11 @@ type Item struct {
sameGitRepo bool
exitCode int
+ // Shown in TUI
CmdLineWithColor string
CmdLine string
+ // Unchanged cmdline to paste to command line
+ CmdLineOut string
Score float64
@@ -106,8 +109,8 @@ func (i Item) DrawStatusLine(compactRendering bool, printedLineLength, realLineL
if i.isRaw {
return splitStatusLineToLines(i.CmdLine, printedLineLength, realLineLength)
}
- secs := int64(i.realtimeBefore)
- nsecs := int64((i.realtimeBefore - float64(secs)) * 1e9)
+ secs := int64(i.time)
+ nsecs := int64((i.time - float64(secs)) * 1e9)
tm := time.Unix(secs, nsecs)
const timeFormat = "2006-01-02 15:04:05"
timeString := tm.Format(timeFormat)
@@ -143,8 +146,8 @@ func (i Item) DrawItemColumns(compactRendering bool, debug bool) ItemColumns {
// DISPLAY
// DISPLAY > date
- secs := int64(i.realtimeBefore)
- nsecs := int64((i.realtimeBefore - float64(secs)) * 1e9)
+ secs := int64(i.time)
+ nsecs := int64((i.time - float64(secs)) * 1e9)
tm := time.Unix(secs, nsecs)
var date string
@@ -228,9 +231,6 @@ func produceLocation(length int, host string, pwdTilde string, differentHost boo
shrinkFactor := float64(length) / float64(totalLen)
shrinkedHostLen := int(math.Ceil(float64(hostLen) * shrinkFactor))
- if debug {
- log.Printf("shrinkFactor: %f\n", shrinkFactor)
- }
halfLocationLen := length/2 - colonLen
newHostLen = minInt(hostLen, shrinkedHostLen, halfLocationLen)
@@ -238,7 +238,14 @@ func produceLocation(length int, host string, pwdTilde string, differentHost boo
// pwd length is the rest of the length
newPwdLen := length - colonLen - newHostLen
- hostWithColor := rightCutPadString(host, newHostLen)
+ // adjust pwd length
+ if newPwdLen > pwdLen {
+ diff := newPwdLen - pwdLen
+ newHostLen += diff
+ newPwdLen -= diff
+ }
+
+ hostWithColor := rightCutLeftPadString(host, newHostLen)
if differentHost {
hostWithColor = highlightHost(hostWithColor)
}
@@ -279,6 +286,20 @@ func (ic ItemColumns) ProduceLine(dateLength int, locationLength int, flagsLengt
return line, length, err
}
+func rightCutLeftPadString(str string, newLen int) string {
+ if newLen <= 0 {
+ return ""
+ }
+ utf8Str := utf8string.NewString(str)
+ strLen := utf8Str.RuneCount()
+ if newLen > strLen {
+ return strings.Repeat(" ", newLen-strLen) + str
+ } else if newLen < strLen {
+ return utf8Str.Slice(0, newLen-1) + dots
+ }
+ return str
+}
+
func leftCutPadString(str string, newLen int) string {
if newLen <= 0 {
return ""
@@ -308,24 +329,22 @@ func rightCutPadString(str string, newLen int) string {
}
// proper match for path is when whole directory is matched
-// proper match for command is when term matches word delimeted by whitespace
+// proper match for command is when term matches word delimited by whitespace
func properMatch(str, term, padChar string) bool {
- if strings.Contains(padChar+str+padChar, padChar+term+padChar) {
- return true
- }
- return false
+ return strings.Contains(padChar+str+padChar, padChar+term+padChar)
}
// NewItemFromRecordForQuery creates new item from record based on given query
-// returns error if the query doesn't match the record
-func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool) (Item, error) {
+//
+// returns error if the query doesn't match the record
+func NewItemFromRecordForQuery(record recordint.SearchApp, query Query, debug bool) (Item, error) {
// Use numbers that won't add up to same score for any number of query words
- // query score weigth 1.51
+ // query score weight 1.51
const hitScore = 1.517 // 1 * 1.51
const properMatchScore = 0.501 // 0.33 * 1.51
const hitScoreConsecutive = 0.00302 // 0.002 * 1.51
- // context score weigth 1
+ // context score weight 1
// Host penalty
var actualPwdScore = 0.9
var sameGitRepoScore = 0.8
@@ -360,22 +379,17 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool
// DISPLAY > cmdline
// cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">"
- cmdLine := strings.ReplaceAll(record.CmdLine, "\n", ";")
- cmdLineWithColor := strings.ReplaceAll(cmd, "\n", ";")
+ cmdLine := strings.ReplaceAll(record.CmdLine, "\n", "\\n ")
+ cmdLineWithColor := strings.ReplaceAll(cmd, "\n", "\\n ")
// KEY for deduplication
+ key := strings.TrimRightFunc(record.CmdLine, unicode.IsSpace)
- key := record.CmdLine
- // NOTE: since we import standard history we need a compatible key without metadata
- /*
- unlikelySeparator := "|||||"
- key := record.CmdLine + unlikelySeparator + record.Pwd + unlikelySeparator +
- record.GitOriginRemote + unlikelySeparator + record.Host
- */
if record.IsRaw {
return Item{
isRaw: true,
+ CmdLineOut: record.CmdLine,
CmdLine: cmdLine,
CmdLineWithColor: cmdLineWithColor,
Score: score,
@@ -387,7 +401,7 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool
// -> N matches against the command
// -> 1 extra match for the actual directory match
sameGitRepo := false
- if query.gitOriginRemote != "" && query.gitOriginRemote == record.GitOriginRemote {
+ if len(query.gitOriginRemote) != 0 && query.gitOriginRemote == record.GitOriginRemote {
sameGitRepo = true
}
@@ -415,10 +429,10 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool
// if score <= 0 && !anyHit {
// return Item{}, errors.New("no match for given record and query")
// }
- score += record.RealtimeBefore * timeScoreCoef
+ score += record.Time * timeScoreCoef
it := Item{
- realtimeBefore: record.RealtimeBefore,
+ time: record.Time,
differentHost: differentHost,
host: record.Host,
@@ -428,6 +442,7 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool
sameGitRepo: sameGitRepo,
exitCode: record.ExitCode,
+ CmdLineOut: record.CmdLine,
CmdLine: cmdLine,
CmdLineWithColor: cmdLineWithColor,
Score: score,
@@ -465,6 +480,7 @@ func GetHeader(compactRendering bool) ItemColumns {
type RawItem struct {
CmdLineWithColor string
CmdLine string
+ CmdLineOut string
Score float64
@@ -473,8 +489,9 @@ type RawItem struct {
}
// NewRawItemFromRecordForQuery creates new item from record based on given query
-// returns error if the query doesn't match the record
-func NewRawItemFromRecordForQuery(record records.CliRecord, terms []string, debug bool) (RawItem, error) {
+//
+// returns error if the query doesn't match the record
+func NewRawItemFromRecordForQuery(record recordint.SearchApp, terms []string, debug bool) (RawItem, error) {
const hitScore = 1.0
const hitScoreConsecutive = 0.01
const properMatchScore = 0.3
@@ -493,7 +510,7 @@ func NewRawItemFromRecordForQuery(record records.CliRecord, terms []string, debu
cmd = strings.ReplaceAll(cmd, term, highlightMatch(term))
}
}
- score += record.RealtimeBefore * timeScoreCoef
+ score += record.Time * timeScoreCoef
// KEY for deduplication
key := record.CmdLine
diff --git a/pkg/searchapp/item_test.go b/internal/searchapp/item_test.go
similarity index 65%
rename from pkg/searchapp/item_test.go
rename to internal/searchapp/item_test.go
index 903157ab..38caad0a 100644
--- a/pkg/searchapp/item_test.go
+++ b/internal/searchapp/item_test.go
@@ -87,3 +87,45 @@ func TestRightCutPadString(t *testing.T) {
t.Fatal("Incorrect right pad from ♥♥♥♥ to '♥♥♥♥ '")
}
}
+
+// TestRightCutLeftPadString
+func TestRightCutLeftPadString(t *testing.T) {
+ if rightCutLeftPadString("abc", -1) != "" {
+ t.Fatal("Incorrect right cut from abc to '' (negative)")
+ }
+ if rightCutLeftPadString("abc", 0) != "" {
+ t.Fatal("Incorrect right cut from abc to ''")
+ }
+ if rightCutLeftPadString("abc", 1) != "…" {
+ t.Fatal("Incorrect right cut from abc to …")
+ }
+ if rightCutLeftPadString("abc", 2) != "a…" {
+ t.Fatal("Incorrect right cut from abc to a…")
+ }
+ if rightCutLeftPadString("abc", 3) != "abc" {
+ t.Fatal("Incorrect right cut from abc to abc")
+ }
+ if rightCutLeftPadString("abc", 5) != " abc" {
+ t.Fatal("Incorrect right pad from abc to ' abc'")
+ }
+
+ // unicode
+ if rightCutLeftPadString("♥♥♥♥", -1) != "" {
+ t.Fatal("Incorrect right cut from ♥♥♥♥ to '' (negative)")
+ }
+ if rightCutLeftPadString("♥♥♥♥", 0) != "" {
+ t.Fatal("Incorrect right cut from ♥♥♥♥ to ''")
+ }
+ if rightCutLeftPadString("♥♥♥♥", 1) != "…" {
+ t.Fatal("Incorrect right cut from ♥♥♥♥ to …")
+ }
+ if rightCutLeftPadString("♥♥♥♥", 2) != "♥…" {
+ t.Fatal("Incorrect right cut from ♥♥♥♥ to ♥…")
+ }
+ if rightCutLeftPadString("♥♥♥♥", 4) != "♥♥♥♥" {
+ t.Fatal("Incorrect right cut from ♥♥♥♥ to ♥♥♥♥")
+ }
+ if rightCutLeftPadString("♥♥♥♥", 6) != " ♥♥♥♥" {
+ t.Fatal("Incorrect right pad from ♥♥♥♥ to ' ♥♥♥♥'")
+ }
+}
diff --git a/pkg/searchapp/query.go b/internal/searchapp/query.go
similarity index 72%
rename from pkg/searchapp/query.go
rename to internal/searchapp/query.go
index 2b8227d9..507af745 100644
--- a/pkg/searchapp/query.go
+++ b/internal/searchapp/query.go
@@ -1,9 +1,11 @@
package searchapp
import (
- "log"
"sort"
"strings"
+
+ "github.com/curusarn/resh/internal/normalize"
+ "go.uber.org/zap"
)
// Query holds information that is used for result scoring
@@ -36,49 +38,33 @@ func filterTerms(terms []string) []string {
}
// NewQueryFromString .
-func NewQueryFromString(queryInput string, host string, pwd string, gitOriginRemote string, debug bool) Query {
- if debug {
- log.Println("QUERY input = <" + queryInput + ">")
- }
+func NewQueryFromString(sugar *zap.SugaredLogger, queryInput string, host string, pwd string, gitOriginRemote string, debug bool) Query {
terms := strings.Fields(queryInput)
var logStr string
for _, term := range terms {
logStr += " <" + term + ">"
}
- if debug {
- log.Println("QUERY raw terms =" + logStr)
- }
terms = filterTerms(terms)
logStr = ""
for _, term := range terms {
logStr += " <" + term + ">"
}
- if debug {
- log.Println("QUERY filtered terms =" + logStr)
- log.Println("QUERY pwd =" + pwd)
- }
sort.SliceStable(terms, func(i, j int) bool { return len(terms[i]) < len(terms[j]) })
return Query{
terms: terms,
host: host,
pwd: pwd,
- gitOriginRemote: gitOriginRemote,
+ gitOriginRemote: normalize.GitRemote(sugar, gitOriginRemote),
}
}
// GetRawTermsFromString .
func GetRawTermsFromString(queryInput string, debug bool) []string {
- if debug {
- log.Println("QUERY input = <" + queryInput + ">")
- }
terms := strings.Fields(queryInput)
var logStr string
for _, term := range terms {
logStr += " <" + term + ">"
}
- if debug {
- log.Println("QUERY raw terms =" + logStr)
- }
terms = filterTerms(terms)
logStr = ""
for _, term := range terms {
diff --git a/pkg/searchapp/time.go b/internal/searchapp/time.go
similarity index 100%
rename from pkg/searchapp/time.go
rename to internal/searchapp/time.go
diff --git a/pkg/sess/sess.go b/internal/sess/sess.go
similarity index 100%
rename from pkg/sess/sess.go
rename to internal/sess/sess.go
diff --git a/internal/sesswatch/sesswatch.go b/internal/sesswatch/sesswatch.go
new file mode 100644
index 00000000..b20bb619
--- /dev/null
+++ b/internal/sesswatch/sesswatch.go
@@ -0,0 +1,96 @@
+package sesswatch
+
+import (
+ "sync"
+ "time"
+
+ "github.com/curusarn/resh/internal/recordint"
+ "github.com/mitchellh/go-ps"
+ "go.uber.org/zap"
+)
+
+type sesswatch struct {
+ sugar *zap.SugaredLogger
+
+ sessionsToDrop []chan string
+ sleepSeconds uint
+
+ watchedSessions map[string]bool
+ mutex sync.Mutex
+}
+
+// Go runs the session watcher - watches sessions and sends
+func Go(sugar *zap.SugaredLogger,
+ sessionsToWatch chan recordint.SessionInit, sessionsToWatchRecords chan recordint.Collect,
+ sessionsToDrop []chan string, sleepSeconds uint) {
+
+ sw := sesswatch{
+ sugar: sugar.With("module", "sesswatch"),
+ sessionsToDrop: sessionsToDrop,
+ sleepSeconds: sleepSeconds,
+ watchedSessions: map[string]bool{},
+ }
+ go sw.waiter(sessionsToWatch, sessionsToWatchRecords)
+}
+
+func (s *sesswatch) waiter(sessionsToWatch chan recordint.SessionInit, sessionsToWatchRecords chan recordint.Collect) {
+ for {
+ func() {
+ select {
+ case rec := <-sessionsToWatch:
+ // normal way to start watching a session
+ id := rec.SessionID
+ pid := rec.SessionPID
+ sugar := s.sugar.With(
+ "sessionID", rec.SessionID,
+ "sessionPID", rec.SessionPID,
+ )
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ if s.watchedSessions[id] == false {
+ sugar.Infow("Starting watching new session")
+ s.watchedSessions[id] = true
+ go s.watcher(sugar, id, pid)
+ }
+ case rec := <-sessionsToWatchRecords:
+ // additional safety - watch sessions that were never properly initialized
+ id := rec.SessionID
+ pid := rec.SessionPID
+ sugar := s.sugar.With(
+ "sessionID", rec.SessionID,
+ "sessionPID", rec.SessionPID,
+ )
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ if s.watchedSessions[id] == false {
+ sugar.Warnw("Starting watching new session based on '/record'")
+ s.watchedSessions[id] = true
+ go s.watcher(sugar, id, pid)
+ }
+ }
+ }()
+ }
+}
+
+func (s *sesswatch) watcher(sugar *zap.SugaredLogger, sessionID string, sessionPID int) {
+ for {
+ time.Sleep(time.Duration(s.sleepSeconds) * time.Second)
+ proc, err := ps.FindProcess(sessionPID)
+ if err != nil {
+ sugar.Errorw("Error while finding process", "error", err)
+ } else if proc == nil {
+ sugar.Infow("Dropping session")
+ func() {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ s.watchedSessions[sessionID] = false
+ }()
+ for _, ch := range s.sessionsToDrop {
+ sugar.Debugw("Sending 'drop session' message ...")
+ ch <- sessionID
+ sugar.Debugw("Sending 'drop session' message DONE")
+ }
+ break
+ }
+ }
+}
diff --git a/internal/signalhandler/signalhander.go b/internal/signalhandler/signalhander.go
new file mode 100644
index 00000000..49c4498d
--- /dev/null
+++ b/internal/signalhandler/signalhander.go
@@ -0,0 +1,74 @@
+package signalhandler
+
+import (
+ "context"
+ "net/http"
+ "os"
+ "os/signal"
+ "strconv"
+ "syscall"
+ "time"
+
+ "go.uber.org/zap"
+)
+
+func sendSignals(sugar *zap.SugaredLogger, sig os.Signal, subscribers []chan os.Signal, done chan string) {
+ for _, sub := range subscribers {
+ sub <- sig
+ }
+ sugar.Warnw("Sent shutdown signals to components")
+ chanCount := len(subscribers)
+ start := time.Now()
+ delay := time.Millisecond * 100
+ timeout := time.Millisecond * 2000
+
+ for {
+ select {
+ case _ = <-done:
+ chanCount--
+ if chanCount == 0 {
+ sugar.Warnw("All components shut down successfully")
+ return
+ }
+ default:
+ time.Sleep(delay)
+ }
+ if time.Since(start) > timeout {
+ sugar.Errorw("Timeouted while waiting for proper shutdown",
+ "componentsStillUp", strconv.Itoa(chanCount),
+ "timeout", timeout.String(),
+ )
+ return
+ }
+ }
+}
+
+// Run catches and handles signals
+func Run(sugar *zap.SugaredLogger, subscribers []chan os.Signal, done chan string, server *http.Server) {
+ sugar = sugar.With("module", "signalhandler")
+ signals := make(chan os.Signal, 1)
+
+ signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP)
+
+ var sig os.Signal
+ for {
+ sig := <-signals
+ sugarSig := sugar.With("signal", sig.String())
+ sugarSig.Infow("Got signal")
+ if sig == syscall.SIGTERM {
+ // Shutdown daemon on SIGTERM
+ break
+ }
+ sugarSig.Warnw("Ignoring signal. Send SIGTERM to trigger shutdown.")
+ }
+
+ sugar.Infow("Sending shutdown signals to components ...")
+ sendSignals(sugar, sig, subscribers, done)
+
+ sugar.Infow("Shutting down the server ...")
+ if err := server.Shutdown(context.Background()); err != nil {
+ sugar.Errorw("Error while shuting down HTTP server",
+ "error", err,
+ )
+ }
+}
diff --git a/internal/status/status.go b/internal/status/status.go
new file mode 100644
index 00000000..52324d49
--- /dev/null
+++ b/internal/status/status.go
@@ -0,0 +1,49 @@
+package status
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+
+ "github.com/curusarn/resh/internal/httpclient"
+ "github.com/curusarn/resh/internal/msg"
+)
+
+func get(port int) (*http.Response, error) {
+ url := "http://localhost:" + strconv.Itoa(port) + "/status"
+ client := httpclient.New()
+ resp, err := client.Get(url)
+ if err != nil {
+ return nil, fmt.Errorf("error while GET'ing daemon /status: %w", err)
+ }
+ return resp, nil
+}
+
+func IsDaemonRunning(port int) (bool, error) {
+ resp, err := get(port)
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+ return true, nil
+}
+
+func GetDaemonStatus(port int) (*msg.StatusResponse, error) {
+ resp, err := get(port)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ jsn, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("error while reading 'daemon /status' response: %w", err)
+ }
+ var msgResp msg.StatusResponse
+ err = json.Unmarshal(jsn, &msgResp)
+ if err != nil {
+ return nil, fmt.Errorf("error while decoding 'daemon /status' response: %w", err)
+ }
+ return &msgResp, nil
+}
diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go
deleted file mode 100644
index 06cc44d6..00000000
--- a/pkg/cfg/cfg.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package cfg
-
-// Config struct
-type Config struct {
- Port int
- SesswatchPeriodSeconds uint
- SesshistInitHistorySize int
- Debug bool
- BindArrowKeysBash bool
- BindArrowKeysZsh bool
- BindControlR bool
-}
diff --git a/pkg/collect/collect.go b/pkg/collect/collect.go
deleted file mode 100644
index 5fd849b1..00000000
--- a/pkg/collect/collect.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package collect
-
-import (
- "bytes"
- "encoding/json"
- "io/ioutil"
- "log"
- "net/http"
- "path/filepath"
- "strconv"
- "strings"
-
- "github.com/curusarn/resh/pkg/httpclient"
- "github.com/curusarn/resh/pkg/records"
-)
-
-// SingleResponse json struct
-type SingleResponse struct {
- Found bool `json:"found"`
- CmdLine string `json:"cmdline"`
-}
-
-// SendRecallRequest to daemon
-func SendRecallRequest(r records.SlimRecord, port string) (string, bool) {
- recJSON, err := json.Marshal(r)
- if err != nil {
- log.Fatal("send err 1", err)
- }
-
- req, err := http.NewRequest("POST", "http://localhost:"+port+"/recall",
- bytes.NewBuffer(recJSON))
- if err != nil {
- log.Fatal("send err 2", err)
- }
- req.Header.Set("Content-Type", "application/json")
-
- client := httpclient.New()
- resp, err := client.Do(req)
- if err != nil {
- log.Fatal("resh-daemon is not running - try restarting this terminal")
- }
-
- defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- log.Fatal("read response error")
- }
- log.Println(string(body))
- response := SingleResponse{}
- err = json.Unmarshal(body, &response)
- if err != nil {
- log.Fatal("unmarshal resp error: ", err)
- }
- log.Println(response)
- return response.CmdLine, response.Found
-}
-
-// SendRecord to daemon
-func SendRecord(r records.Record, port, path string) {
- recJSON, err := json.Marshal(r)
- if err != nil {
- log.Fatal("send err 1", err)
- }
-
- req, err := http.NewRequest("POST", "http://localhost:"+port+path,
- bytes.NewBuffer(recJSON))
- if err != nil {
- log.Fatal("send err 2", err)
- }
- req.Header.Set("Content-Type", "application/json")
-
- client := httpclient.New()
- _, err = client.Do(req)
- if err != nil {
- log.Fatal("resh-daemon is not running - try restarting this terminal")
- }
-}
-
-// ReadFileContent and return it as a string
-func ReadFileContent(path string) string {
- dat, err := ioutil.ReadFile(path)
- if err != nil {
- return ""
- //log.Fatal("failed to open " + path)
- }
- return strings.TrimSuffix(string(dat), "\n")
-}
-
-// GetGitDirs based on result of git "cdup" command
-func GetGitDirs(cdup string, exitCode int, pwd string) (string, string) {
- if exitCode != 0 {
- return "", ""
- }
- abspath := filepath.Clean(filepath.Join(pwd, cdup))
- realpath, err := filepath.EvalSymlinks(abspath)
- if err != nil {
- log.Println("err while handling git dir paths:", err)
- return "", ""
- }
- return abspath, realpath
-}
-
-// GetTimezoneOffsetInSeconds based on zone returned by date command
-func GetTimezoneOffsetInSeconds(zone string) float64 {
- // date +%z -> "+0200"
- hoursStr := zone[:3]
- minsStr := zone[3:]
- hours, err := strconv.Atoi(hoursStr)
- if err != nil {
- log.Println("err while parsing hours in timezone offset:", err)
- return -1
- }
- mins, err := strconv.Atoi(minsStr)
- if err != nil {
- log.Println("err while parsing mins in timezone offset:", err)
- return -1
- }
- secs := ((hours * 60) + mins) * 60
- return float64(secs)
-}
diff --git a/pkg/histanal/histeval.go b/pkg/histanal/histeval.go
deleted file mode 100644
index 4d19779f..00000000
--- a/pkg/histanal/histeval.go
+++ /dev/null
@@ -1,246 +0,0 @@
-package histanal
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "log"
- "math/rand"
- "os"
- "os/exec"
-
- "github.com/curusarn/resh/pkg/records"
- "github.com/curusarn/resh/pkg/strat"
- "github.com/jpillora/longestcommon"
-
- "github.com/schollz/progressbar"
-)
-
-type matchJSON struct {
- Match bool
- Distance int
- CharsRecalled int
-}
-
-type multiMatchItemJSON struct {
- Distance int
- CharsRecalled int
-}
-
-type multiMatchJSON struct {
- Match bool
- Entries []multiMatchItemJSON
-}
-
-type strategyJSON struct {
- Title string
- Description string
- Matches []matchJSON
- PrefixMatches []multiMatchJSON
-}
-
-// HistEval evaluates history
-type HistEval struct {
- HistLoad
- BatchMode bool
- maxCandidates int
- Strategies []strategyJSON
-}
-
-// NewHistEval constructs new HistEval
-func NewHistEval(inputPath string,
- maxCandidates int, skipFailedCmds bool,
- debugRecords float64, sanitizedInput bool) HistEval {
-
- e := HistEval{
- HistLoad: HistLoad{
- skipFailedCmds: skipFailedCmds,
- debugRecords: debugRecords,
- sanitizedInput: sanitizedInput,
- },
- maxCandidates: maxCandidates,
- BatchMode: false,
- }
- records := e.loadHistoryRecords(inputPath)
- device := deviceRecords{Records: records}
- user := userRecords{}
- user.Devices = append(user.Devices, device)
- e.UsersRecords = append(e.UsersRecords, user)
- e.preprocessRecords()
- return e
-}
-
-// NewHistEvalBatchMode constructs new HistEval in batch mode
-func NewHistEvalBatchMode(input string, inputDataRoot string,
- maxCandidates int, skipFailedCmds bool,
- debugRecords float64, sanitizedInput bool) HistEval {
-
- e := HistEval{
- HistLoad: HistLoad{
- skipFailedCmds: skipFailedCmds,
- debugRecords: debugRecords,
- sanitizedInput: sanitizedInput,
- },
- maxCandidates: maxCandidates,
- BatchMode: false,
- }
- e.UsersRecords = e.loadHistoryRecordsBatchMode(input, inputDataRoot)
- e.preprocessRecords()
- return e
-}
-
-func (e *HistEval) preprocessDeviceRecords(device deviceRecords) deviceRecords {
- sessionIDs := map[string]uint64{}
- var nextID uint64
- nextID = 1 // start with 1 because 0 won't get saved to json
- for k, record := range device.Records {
- id, found := sessionIDs[record.SessionID]
- if found == false {
- id = nextID
- sessionIDs[record.SessionID] = id
- nextID++
- }
- device.Records[k].SeqSessionID = id
- // assert
- if record.Sanitized != e.sanitizedInput {
- if e.sanitizedInput {
- log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized")
- }
- log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present")
- }
- device.Records[k].SeqSessionID = id
- if e.debugRecords > 0 && rand.Float64() < e.debugRecords {
- device.Records[k].DebugThisRecord = true
- }
- }
- // sort.SliceStable(device.Records, func(x, y int) bool {
- // if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID {
- // return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal
- // }
- // return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID
- // })
-
- // iterate from back and mark last record of each session
- sessionIDSet := map[string]bool{}
- for i := len(device.Records) - 1; i >= 0; i-- {
- var record *records.EnrichedRecord
- record = &device.Records[i]
- if sessionIDSet[record.SessionID] {
- continue
- }
- sessionIDSet[record.SessionID] = true
- record.LastRecordOfSession = true
- }
- return device
-}
-
-// enrich records and add sequential session ID
-func (e *HistEval) preprocessRecords() {
- for i := range e.UsersRecords {
- for j := range e.UsersRecords[i].Devices {
- e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j])
- }
- }
-}
-
-// Evaluate a given strategy
-func (e *HistEval) Evaluate(strategy strat.IStrategy) error {
- title, description := strategy.GetTitleAndDescription()
- log.Println("Evaluating strategy:", title, "-", description)
- strategyData := strategyJSON{Title: title, Description: description}
- for i := range e.UsersRecords {
- for j := range e.UsersRecords[i].Devices {
- bar := progressbar.New(len(e.UsersRecords[i].Devices[j].Records))
- var prevRecord records.EnrichedRecord
- for _, record := range e.UsersRecords[i].Devices[j].Records {
- if e.skipFailedCmds && record.ExitCode != 0 {
- continue
- }
- candidates := strategy.GetCandidates(records.Stripped(record))
- if record.DebugThisRecord {
- log.Println()
- log.Println("===================================================")
- log.Println("STRATEGY:", title, "-", description)
- log.Println("===================================================")
- log.Println("Previous record:")
- if prevRecord.RealtimeBefore == 0 {
- log.Println("== NIL")
- } else {
- rec, _ := prevRecord.ToString()
- log.Println(rec)
- }
- log.Println("---------------------------------------------------")
- log.Println("Recommendations for:")
- rec, _ := record.ToString()
- log.Println(rec)
- log.Println("---------------------------------------------------")
- for i, candidate := range candidates {
- if i > 10 {
- break
- }
- log.Println(string(candidate))
- }
- log.Println("===================================================")
- }
-
- matchFound := false
- longestPrefixMatchLength := 0
- multiMatch := multiMatchJSON{}
- for i, candidate := range candidates {
- // make an option (--calculate-total) to turn this on/off ?
- // if i >= e.maxCandidates {
- // break
- // }
- commonPrefixLength := len(longestcommon.Prefix([]string{candidate, record.CmdLine}))
- if commonPrefixLength > longestPrefixMatchLength {
- longestPrefixMatchLength = commonPrefixLength
- prefixMatch := multiMatchItemJSON{Distance: i + 1, CharsRecalled: commonPrefixLength}
- multiMatch.Match = true
- multiMatch.Entries = append(multiMatch.Entries, prefixMatch)
- }
- if candidate == record.CmdLine {
- match := matchJSON{Match: true, Distance: i + 1, CharsRecalled: record.CmdLength}
- matchFound = true
- strategyData.Matches = append(strategyData.Matches, match)
- strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch)
- break
- }
- }
- if matchFound == false {
- strategyData.Matches = append(strategyData.Matches, matchJSON{})
- strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch)
- }
- err := strategy.AddHistoryRecord(&record)
- if err != nil {
- log.Println("Error while evauating", err)
- return err
- }
- bar.Add(1)
- prevRecord = record
- }
- strategy.ResetHistory()
- fmt.Println()
- }
- }
- e.Strategies = append(e.Strategies, strategyData)
- return nil
-}
-
-// CalculateStatsAndPlot results
-func (e *HistEval) CalculateStatsAndPlot(scriptName string) {
- evalJSON, err := json.Marshal(e)
- if err != nil {
- log.Fatal("json marshal error", err)
- }
- buffer := bytes.Buffer{}
- buffer.Write(evalJSON)
- // run python script to stat and plot/
- cmd := exec.Command(scriptName)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Stdin = &buffer
- err = cmd.Run()
- if err != nil {
- log.Printf("Command finished with error: %v", err)
- }
-}
diff --git a/pkg/histanal/histload.go b/pkg/histanal/histload.go
deleted file mode 100644
index ec81cc27..00000000
--- a/pkg/histanal/histload.go
+++ /dev/null
@@ -1,180 +0,0 @@
-package histanal
-
-import (
- "bufio"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "log"
- "math/rand"
- "os"
- "path/filepath"
-
- "github.com/curusarn/resh/pkg/records"
-)
-
-type deviceRecords struct {
- Name string
- Records []records.EnrichedRecord
-}
-
-type userRecords struct {
- Name string
- Devices []deviceRecords
-}
-
-// HistLoad loads history
-type HistLoad struct {
- UsersRecords []userRecords
- skipFailedCmds bool
- sanitizedInput bool
- debugRecords float64
-}
-
-func (e *HistLoad) preprocessDeviceRecords(device deviceRecords) deviceRecords {
- sessionIDs := map[string]uint64{}
- var nextID uint64
- nextID = 1 // start with 1 because 0 won't get saved to json
- for k, record := range device.Records {
- id, found := sessionIDs[record.SessionID]
- if found == false {
- id = nextID
- sessionIDs[record.SessionID] = id
- nextID++
- }
- device.Records[k].SeqSessionID = id
- // assert
- if record.Sanitized != e.sanitizedInput {
- if e.sanitizedInput {
- log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized")
- }
- log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present")
- }
- device.Records[k].SeqSessionID = id
- if e.debugRecords > 0 && rand.Float64() < e.debugRecords {
- device.Records[k].DebugThisRecord = true
- }
- }
- // sort.SliceStable(device.Records, func(x, y int) bool {
- // if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID {
- // return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal
- // }
- // return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID
- // })
-
- // iterate from back and mark last record of each session
- sessionIDSet := map[string]bool{}
- for i := len(device.Records) - 1; i >= 0; i-- {
- var record *records.EnrichedRecord
- record = &device.Records[i]
- if sessionIDSet[record.SessionID] {
- continue
- }
- sessionIDSet[record.SessionID] = true
- record.LastRecordOfSession = true
- }
- return device
-}
-
-// enrich records and add sequential session ID
-func (e *HistLoad) preprocessRecords() {
- for i := range e.UsersRecords {
- for j := range e.UsersRecords[i].Devices {
- e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j])
- }
- }
-}
-
-func (e *HistLoad) loadHistoryRecordsBatchMode(fname string, dataRootPath string) []userRecords {
- var records []userRecords
- info, err := os.Stat(dataRootPath)
- if err != nil {
- log.Fatal("Error: Directory", dataRootPath, "does not exist - exiting! (", err, ")")
- }
- if info.IsDir() == false {
- log.Fatal("Error:", dataRootPath, "is not a directory - exiting!")
- }
- users, err := ioutil.ReadDir(dataRootPath)
- if err != nil {
- log.Fatal("Could not read directory:", dataRootPath)
- }
- fmt.Println("Listing users in <", dataRootPath, ">...")
- for _, user := range users {
- userRecords := userRecords{Name: user.Name()}
- userFullPath := filepath.Join(dataRootPath, user.Name())
- if user.IsDir() == false {
- log.Println("Warn: Unexpected file (not a directory) <", userFullPath, "> - skipping.")
- continue
- }
- fmt.Println()
- fmt.Printf("*- %s\n", user.Name())
- devices, err := ioutil.ReadDir(userFullPath)
- if err != nil {
- log.Fatal("Could not read directory:", userFullPath)
- }
- for _, device := range devices {
- deviceRecords := deviceRecords{Name: device.Name()}
- deviceFullPath := filepath.Join(userFullPath, device.Name())
- if device.IsDir() == false {
- log.Println("Warn: Unexpected file (not a directory) <", deviceFullPath, "> - skipping.")
- continue
- }
- fmt.Printf(" \\- %s\n", device.Name())
- files, err := ioutil.ReadDir(deviceFullPath)
- if err != nil {
- log.Fatal("Could not read directory:", deviceFullPath)
- }
- for _, file := range files {
- fileFullPath := filepath.Join(deviceFullPath, file.Name())
- if file.Name() == fname {
- fmt.Printf(" \\- %s - loading ...", file.Name())
- // load the data
- deviceRecords.Records = e.loadHistoryRecords(fileFullPath)
- fmt.Println(" OK ✓")
- } else {
- fmt.Printf(" \\- %s - skipped\n", file.Name())
- }
- }
- userRecords.Devices = append(userRecords.Devices, deviceRecords)
- }
- records = append(records, userRecords)
- }
- return records
-}
-
-func (e *HistLoad) loadHistoryRecords(fname string) []records.EnrichedRecord {
- file, err := os.Open(fname)
- if err != nil {
- log.Fatal("Open() resh history file error:", err)
- }
- defer file.Close()
-
- var recs []records.EnrichedRecord
- scanner := bufio.NewScanner(file)
- for scanner.Scan() {
- record := records.Record{}
- fallbackRecord := records.FallbackRecord{}
- line := scanner.Text()
- err = json.Unmarshal([]byte(line), &record)
- if err != nil {
- err = json.Unmarshal([]byte(line), &fallbackRecord)
- if err != nil {
- log.Println("Line:", line)
- log.Fatal("Decoding error:", err)
- }
- record = records.Convert(&fallbackRecord)
- }
- if e.sanitizedInput == false {
- if record.CmdLength != 0 {
- log.Fatal("Assert failed - 'cmdLength' is set in raw data. Maybe you want to use '--sanitized-input' option?")
- }
- record.CmdLength = len(record.CmdLine)
- } else if record.CmdLength == 0 {
- log.Fatal("Assert failed - 'cmdLength' is unset in the data. This should not happen.")
- }
- if !e.skipFailedCmds || record.ExitCode == 0 {
- recs = append(recs, records.Enriched(record))
- }
- }
- return recs
-}
diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go
deleted file mode 100644
index 73e5309b..00000000
--- a/pkg/histfile/histfile.go
+++ /dev/null
@@ -1,262 +0,0 @@
-package histfile
-
-import (
- "encoding/json"
- "log"
- "math"
- "os"
- "strconv"
- "sync"
-
- "github.com/curusarn/resh/pkg/histcli"
- "github.com/curusarn/resh/pkg/histlist"
- "github.com/curusarn/resh/pkg/records"
-)
-
-// Histfile writes records to histfile
-type Histfile struct {
- sessionsMutex sync.Mutex
- sessions map[string]records.Record
- historyPath string
-
- 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
-
- cliRecords histcli.Histcli
-}
-
-// 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: reshHistoryPath,
- bashCmdLines: histlist.New(),
- zshCmdLines: histlist.New(),
- cliRecords: histcli.New(),
- }
- go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB)
- go hf.writer(input, signals, shutdownDone)
- go hf.sessionGC(sessionsToDrop)
- return &hf
-}
-
-// load records from resh history, reverse, enrich and save
-func (h *Histfile) loadCliRecords(recs []records.Record) {
- for _, cmdline := range h.bashCmdLines.List {
- h.cliRecords.AddCmdLine(cmdline)
- }
- for _, cmdline := range h.zshCmdLines.List {
- h.cliRecords.AddCmdLine(cmdline)
- }
- for i := len(recs) - 1; i >= 0; i-- {
- rec := recs[i]
- h.cliRecords.AddRecord(rec)
- }
- log.Println("histfile: resh history loaded - history records count:", len(h.cliRecords.List))
-}
-
-// 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()
- 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 ...")
- history := records.LoadFromFile(h.historyPath, math.MaxInt32)
- log.Println("histfile: resh history loaded from file - count:", len(history))
- go h.loadCliRecords(history)
- // NOTE: keeping this weird interface for now because we might use it in the future
- // when we only load bash or zsh history
- reshCmdLines := loadCmdLines(history)
- 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
-func (h *Histfile) sessionGC(sessionsToDrop chan string) {
- for {
- func() {
- session := <-sessionsToDrop
- log.Println("histfile: got session to drop", session)
- h.sessionsMutex.Lock()
- defer h.sessionsMutex.Unlock()
- if part1, found := h.sessions[session]; found == true {
- log.Println("histfile: Dropping session:", session)
- delete(h.sessions, session)
- go writeRecord(part1, h.historyPath)
- } else {
- log.Println("histfile: No hanging parts for session:", session)
- }
- }()
- }
-}
-
-// writer reads records from channel, merges them and writes them to file
-func (h *Histfile) writer(input chan records.Record, signals chan os.Signal, shutdownDone chan string) {
- for {
- func() {
- 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, ")")
- } 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.Printf("histfile WARN: Writing incomplete record for session: %v\n", 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 {
- log.Println("Error while merging", err)
- return
- }
-
- func() {
- h.recentMutex.Lock()
- defer h.recentMutex.Unlock()
- h.recentRecords = append(h.recentRecords, part1)
- cmdLine := part1.CmdLine
- h.bashCmdLines.AddCmdLine(cmdLine)
- h.zshCmdLines.AddCmdLine(cmdLine)
- h.cliRecords.AddRecord(part1)
- }()
-
- writeRecord(part1, h.historyPath)
-}
-
-func writeRecord(rec records.Record, outputPath string) {
- recJSON, err := json.Marshal(rec)
- if err != nil {
- log.Println("Marshalling error", err)
- return
- }
- f, err := os.OpenFile(outputPath,
- os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if err != nil {
- log.Println("Could not open file", err)
- return
- }
- defer f.Close()
- _, err = f.Write(append(recJSON, []byte("\n")...))
- if err != nil {
- log.Printf("Error while writing: %v, %s\n", rec, err)
- return
- }
-}
-
-// GetRecentCmdLines returns recent cmdLines
-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
-}
-
-// DumpCliRecords returns enriched records
-func (h *Histfile) DumpCliRecords() histcli.Histcli {
- // don't forget locks in the future
- return h.cliRecords
-}
-
-func loadCmdLines(recs []records.Record) histlist.Histlist {
- hl := histlist.New()
- // go from bottom and deduplicate
- var cmdLines []string
- cmdLinesSet := map[string]bool{}
- for i := len(recs) - 1; i >= 0; i-- {
- cmdLine := recs[i].CmdLine
- if cmdLinesSet[cmdLine] {
- continue
- }
- cmdLinesSet[cmdLine] = true
- cmdLines = append([]string{cmdLine}, cmdLines...)
- // if len(cmdLines) > limit {
- // break
- // }
- }
- // add everything to histlist
- for _, cmdLine := range cmdLines {
- hl.AddCmdLine(cmdLine)
- }
- return hl
-}
diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go
deleted file mode 100644
index 3c859874..00000000
--- a/pkg/msg/msg.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package msg
-
-import "github.com/curusarn/resh/pkg/records"
-
-// CliMsg struct
-type CliMsg struct {
- SessionID string `json:"sessionID"`
- PWD string `json:"pwd"`
-}
-
-// CliResponse struct
-type CliResponse struct {
- CliRecords []records.CliRecord `json:"cliRecords"`
-}
-
-// InspectMsg struct
-type InspectMsg struct {
- SessionID string `json:"sessionId"`
- Count uint `json:"count"`
-}
-
-// MultiResponse struct
-type MultiResponse struct {
- CmdLines []string `json:"cmdlines"`
-}
-
-// StatusResponse struct
-type StatusResponse struct {
- Status bool `json:"status"`
- Version string `json:"version"`
- Commit string `json:"commit"`
-}
diff --git a/pkg/records/records.go b/pkg/records/records.go
deleted file mode 100644
index 6271ba52..00000000
--- a/pkg/records/records.go
+++ /dev/null
@@ -1,689 +0,0 @@
-package records
-
-import (
- "bufio"
- "encoding/json"
- "errors"
- "io"
- "log"
- "math"
- "os"
- "strconv"
- "strings"
-
- "github.com/curusarn/resh/pkg/histlist"
- "github.com/mattn/go-shellwords"
-)
-
-// BaseRecord - common base for Record and FallbackRecord
-type BaseRecord struct {
- // core
- CmdLine string `json:"cmdLine"`
- ExitCode int `json:"exitCode"`
- Shell string `json:"shell"`
- Uname string `json:"uname"`
- SessionID string `json:"sessionId"`
- RecordID string `json:"recordId"`
-
- // posix
- Home string `json:"home"`
- Lang string `json:"lang"`
- LcAll string `json:"lcAll"`
- Login string `json:"login"`
- //Path string `json:"path"`
- Pwd string `json:"pwd"`
- PwdAfter string `json:"pwdAfter"`
- ShellEnv string `json:"shellEnv"`
- Term string `json:"term"`
-
- // non-posix"`
- RealPwd string `json:"realPwd"`
- RealPwdAfter string `json:"realPwdAfter"`
- Pid int `json:"pid"`
- SessionPID int `json:"sessionPid"`
- Host string `json:"host"`
- Hosttype string `json:"hosttype"`
- Ostype string `json:"ostype"`
- Machtype string `json:"machtype"`
- Shlvl int `json:"shlvl"`
-
- // before after
- TimezoneBefore string `json:"timezoneBefore"`
- TimezoneAfter string `json:"timezoneAfter"`
-
- RealtimeBefore float64 `json:"realtimeBefore"`
- RealtimeAfter float64 `json:"realtimeAfter"`
- RealtimeBeforeLocal float64 `json:"realtimeBeforeLocal"`
- RealtimeAfterLocal float64 `json:"realtimeAfterLocal"`
-
- RealtimeDuration float64 `json:"realtimeDuration"`
- RealtimeSinceSessionStart float64 `json:"realtimeSinceSessionStart"`
- RealtimeSinceBoot float64 `json:"realtimeSinceBoot"`
- //Logs []string `json: "logs"`
-
- GitDir string `json:"gitDir"`
- GitRealDir string `json:"gitRealDir"`
- GitOriginRemote string `json:"gitOriginRemote"`
- GitDirAfter string `json:"gitDirAfter"`
- GitRealDirAfter string `json:"gitRealDirAfter"`
- GitOriginRemoteAfter string `json:"gitOriginRemoteAfter"`
- MachineID string `json:"machineId"`
-
- OsReleaseID string `json:"osReleaseId"`
- OsReleaseVersionID string `json:"osReleaseVersionId"`
- OsReleaseIDLike string `json:"osReleaseIdLike"`
- OsReleaseName string `json:"osReleaseName"`
- OsReleasePrettyName string `json:"osReleasePrettyName"`
-
- ReshUUID string `json:"reshUuid"`
- ReshVersion string `json:"reshVersion"`
- ReshRevision string `json:"reshRevision"`
-
- // records come in two parts (collect and postcollect)
- PartOne bool `json:"partOne,omitempty"` // false => part two
- PartsMerged bool `json:"partsMerged"`
- // special flag -> not an actual record but an session end
- SessionExit bool `json:"sessionExit,omitempty"`
-
- // recall metadata
- Recalled bool `json:"recalled"`
- RecallHistno int `json:"recallHistno,omitempty"`
- RecallStrategy string `json:"recallStrategy,omitempty"`
- RecallActionsRaw string `json:"recallActionsRaw,omitempty"`
- RecallActions []string `json:"recallActions,omitempty"`
- RecallLastCmdLine string `json:"recallLastCmdLine"`
-
- // recall command
- RecallPrefix string `json:"recallPrefix,omitempty"`
-
- // added by sanitizatizer
- Sanitized bool `json:"sanitized,omitempty"`
- CmdLength int `json:"cmdLength,omitempty"`
-}
-
-// Record representing single executed command with its metadata
-type Record struct {
- BaseRecord
-
- Cols string `json:"cols"`
- Lines string `json:"lines"`
-}
-
-// EnrichedRecord - record enriched with additional data
-type EnrichedRecord struct {
- Record
-
- // enriching fields - added "later"
- Command string `json:"command"`
- FirstWord string `json:"firstWord"`
- Invalid bool `json:"invalid"`
- SeqSessionID uint64 `json:"seqSessionId"`
- LastRecordOfSession bool `json:"lastRecordOfSession"`
- DebugThisRecord bool `json:"debugThisRecord"`
- Errors []string `json:"errors"`
- // SeqSessionID uint64 `json:"seqSessionId,omitempty"`
-}
-
-// FallbackRecord when record is too old and can't be parsed into regular Record
-type FallbackRecord struct {
- BaseRecord
- // older version of the record where cols and lines are int
-
- Cols int `json:"cols"` // 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"`
-
-}
-
-// CliRecord used for sending records to RESH-CLI
-type CliRecord struct {
- IsRaw bool `json:"isRaw"`
- SessionID string `json:"sessionId"`
-
- CmdLine string `json:"cmdLine"`
- Host string `json:"host"`
- Pwd string `json:"pwd"`
- Home string `json:"home"` // helps us to collapse /home/user to tilde
- GitOriginRemote string `json:"gitOriginRemote"`
- ExitCode int `json:"exitCode"`
-
- RealtimeBefore float64 `json:"realtimeBefore"`
- // RealtimeAfter float64 `json:"realtimeAfter"`
- // RealtimeDuration float64 `json:"realtimeDuration"`
-}
-
-// NewCliRecordFromCmdLine from EnrichedRecord
-func NewCliRecordFromCmdLine(cmdLine string) CliRecord {
- return CliRecord{
- IsRaw: true,
- CmdLine: cmdLine,
- }
-}
-
-// NewCliRecord from EnrichedRecord
-func NewCliRecord(r EnrichedRecord) CliRecord {
- return CliRecord{
- IsRaw: false,
- SessionID: r.SessionID,
- CmdLine: r.CmdLine,
- Host: r.Host,
- Pwd: r.Pwd,
- Home: r.Home,
- GitOriginRemote: r.GitOriginRemote,
- ExitCode: r.ExitCode,
- RealtimeBefore: r.RealtimeBefore,
- }
-}
-
-// Convert from FallbackRecord to Record
-func Convert(r *FallbackRecord) Record {
- return Record{
- BaseRecord: r.BaseRecord,
- // these two lines are the only reason we are doing this
- Cols: strconv.Itoa(r.Cols),
- Lines: strconv.Itoa(r.Lines),
- }
-}
-
-// ToString - returns record the json
-func (r EnrichedRecord) ToString() (string, error) {
- jsonRec, err := json.Marshal(r)
- if err != nil {
- return "marshalling error", err
- }
- return string(jsonRec), nil
-}
-
-// Enriched - returnd enriched record
-func Enriched(r Record) EnrichedRecord {
- record := EnrichedRecord{Record: r}
- // normlize git remote
- record.GitOriginRemote = NormalizeGitRemote(record.GitOriginRemote)
- record.GitOriginRemoteAfter = NormalizeGitRemote(record.GitOriginRemoteAfter)
- // Get command/first word from commandline
- var err error
- err = r.Validate()
- if err != nil {
- record.Errors = append(record.Errors, "Validate error:"+err.Error())
- // rec, _ := record.ToString()
- // log.Println("Invalid command:", rec)
- record.Invalid = true
- }
- record.Command, record.FirstWord, err = GetCommandAndFirstWord(r.CmdLine)
- if err != nil {
- record.Errors = append(record.Errors, "GetCommandAndFirstWord error:"+err.Error())
- // rec, _ := record.ToString()
- // log.Println("Invalid command:", rec)
- record.Invalid = true // should this be really invalid ?
- }
- return record
-}
-
-// Merge two records (part1 - collect + part2 - postcollect)
-func (r *Record) Merge(r2 Record) error {
- if r.PartOne == false || r2.PartOne {
- return errors.New("Expected part1 and part2 of the same record - usage: part1.Merge(part2)")
- }
- if r.SessionID != r2.SessionID {
- return errors.New("Records to merge are not from the same sesion - r1:" + r.SessionID + " r2:" + r2.SessionID)
- }
- if r.CmdLine != r2.CmdLine {
- return errors.New("Records to merge are not parts of the same records - r1:" + r.CmdLine + " r2:" + r2.CmdLine)
- }
- if r.RecordID != r2.RecordID {
- return errors.New("Records to merge do not have the same ID - r1:" + r.RecordID + " r2:" + r2.RecordID)
- }
- // r.RealtimeBefore != r2.RealtimeBefore - can't be used because of bash-preexec runs when it's not supposed to
- r.ExitCode = r2.ExitCode
- r.PwdAfter = r2.PwdAfter
- r.RealPwdAfter = r2.RealPwdAfter
- r.GitDirAfter = r2.GitDirAfter
- r.GitRealDirAfter = r2.GitRealDirAfter
- r.RealtimeAfter = r2.RealtimeAfter
- r.GitOriginRemoteAfter = r2.GitOriginRemoteAfter
- r.TimezoneAfter = r2.TimezoneAfter
- r.RealtimeAfterLocal = r2.RealtimeAfterLocal
- r.RealtimeDuration = r2.RealtimeDuration
-
- r.PartsMerged = true
- r.PartOne = false
- return nil
-}
-
-// Validate - returns error if the record is invalid
-func (r *Record) Validate() error {
- if r.CmdLine == "" {
- return errors.New("There is no CmdLine")
- }
- if r.RealtimeBefore == 0 || r.RealtimeAfter == 0 {
- return errors.New("There is no Time")
- }
- if r.RealtimeBeforeLocal == 0 || r.RealtimeAfterLocal == 0 {
- return errors.New("There is no Local Time")
- }
- if r.RealPwd == "" || r.RealPwdAfter == "" {
- return errors.New("There is no Real Pwd")
- }
- if r.Pwd == "" || r.PwdAfter == "" {
- return errors.New("There is no Pwd")
- }
-
- // TimezoneBefore
- // TimezoneAfter
-
- // RealtimeDuration
- // RealtimeSinceSessionStart - TODO: add later
- // RealtimeSinceBoot - TODO: add later
-
- // device extras
- // Host
- // Hosttype
- // Ostype
- // Machtype
- // OsReleaseID
- // OsReleaseVersionID
- // OsReleaseIDLike
- // OsReleaseName
- // OsReleasePrettyName
-
- // session extras
- // Term
- // Shlvl
-
- // static info
- // Lang
- // LcAll
-
- // meta
- // ReshUUID
- // ReshVersion
- // ReshRevision
-
- // added by sanitizatizer
- // Sanitized
- // CmdLength
- return nil
-}
-
-// SetCmdLine sets cmdLine and related members
-func (r *EnrichedRecord) SetCmdLine(cmdLine string) {
- r.CmdLine = cmdLine
- r.CmdLength = len(cmdLine)
- r.ExitCode = 0
- var err error
- r.Command, r.FirstWord, err = GetCommandAndFirstWord(cmdLine)
- if err != nil {
- r.Errors = append(r.Errors, "GetCommandAndFirstWord error:"+err.Error())
- // log.Println("Invalid command:", r.CmdLine)
- r.Invalid = true
- }
-}
-
-// Stripped returns record stripped of all info that is not available during prediction
-func Stripped(r EnrichedRecord) EnrichedRecord {
- // clear the cmd itself
- r.SetCmdLine("")
- // replace after info with before info
- r.PwdAfter = r.Pwd
- r.RealPwdAfter = r.RealPwd
- r.TimezoneAfter = r.TimezoneBefore
- r.RealtimeAfter = r.RealtimeBefore
- r.RealtimeAfterLocal = r.RealtimeBeforeLocal
- // clear some more stuff
- r.RealtimeDuration = 0
- r.LastRecordOfSession = false
- return r
-}
-
-// GetCommandAndFirstWord func
-func GetCommandAndFirstWord(cmdLine string) (string, string, error) {
- args, err := shellwords.Parse(cmdLine)
- if err != nil {
- // log.Println("shellwords Error:", err, " (cmdLine: <", cmdLine, "> )")
- return "", "", err
- }
- if len(args) == 0 {
- return "", "", nil
- }
- i := 0
- for true {
- // commands in shell sometimes look like this `variable=something command argument otherArgument --option`
- // to get the command we skip over tokens that contain '='
- if strings.ContainsRune(args[i], '=') && len(args) > i+1 {
- i++
- continue
- }
- return args[i], args[0], nil
- }
- log.Fatal("GetCommandAndFirstWord error: this should not happen!")
- return "ERROR", "ERROR", errors.New("this should not happen - contact developer ;)")
-}
-
-// NormalizeGitRemote func
-func NormalizeGitRemote(gitRemote string) string {
- if strings.HasSuffix(gitRemote, ".git") {
- return gitRemote[:len(gitRemote)-4]
- }
- return gitRemote
-}
-
-// DistParams is used to supply params to Enrichedrecords.DistanceTo()
-type DistParams struct {
- ExitCode float64
- MachineID float64
- SessionID float64
- Login float64
- Shell float64
- Pwd float64
- RealPwd float64
- Git float64
- Time float64
-}
-
-// DistanceTo another record
-func (r *EnrichedRecord) DistanceTo(r2 EnrichedRecord, p DistParams) float64 {
- var dist float64
- dist = 0
-
- // lev distance or something? TODO later
- // CmdLine
-
- // exit code
- if r.ExitCode != r2.ExitCode {
- if r.ExitCode == 0 || r2.ExitCode == 0 {
- // one success + one error -> 1
- dist += 1 * p.ExitCode
- } else {
- // two different errors
- dist += 0.5 * p.ExitCode
- }
- }
-
- // machine/device
- if r.MachineID != r2.MachineID {
- dist += 1 * p.MachineID
- }
- // Uname
-
- // session
- if r.SessionID != r2.SessionID {
- dist += 1 * p.SessionID
- }
- // Pid - add because of nested shells?
- // SessionPid
-
- // user
- if r.Login != r2.Login {
- dist += 1 * p.Login
- }
- // Home
-
- // shell
- if r.Shell != r2.Shell {
- dist += 1 * p.Shell
- }
- // ShellEnv
-
- // pwd
- if r.Pwd != r2.Pwd {
- // TODO: compare using hierarchy
- // TODO: make more important
- dist += 1 * p.Pwd
- }
- if r.RealPwd != r2.RealPwd {
- // TODO: -||-
- dist += 1 * p.RealPwd
- }
- // PwdAfter
- // RealPwdAfter
-
- // git
- if r.GitDir != r2.GitDir {
- dist += 1 * p.Git
- }
- if r.GitRealDir != r2.GitRealDir {
- dist += 1 * p.Git
- }
- if r.GitOriginRemote != r2.GitOriginRemote {
- dist += 1 * p.Git
- }
-
- // time
- // this can actually get negative for differences of less than one second which is fine
- // distance grows by 1 with every order
- distTime := math.Log10(math.Abs(r.RealtimeBefore-r2.RealtimeBefore)) * p.Time
- if math.IsNaN(distTime) == false && math.IsInf(distTime, 0) == false {
- dist += distTime
- }
- // RealtimeBeforeLocal
- // RealtimeAfter
- // RealtimeAfterLocal
-
- // TimezoneBefore
- // TimezoneAfter
-
- // RealtimeDuration
- // RealtimeSinceSessionStart - TODO: add later
- // RealtimeSinceBoot - TODO: add later
-
- // device extras
- // Host
- // Hosttype
- // Ostype
- // Machtype
- // OsReleaseID
- // OsReleaseVersionID
- // OsReleaseIDLike
- // OsReleaseName
- // OsReleasePrettyName
-
- // session extras
- // Term
- // Shlvl
-
- // static info
- // Lang
- // LcAll
-
- // meta
- // ReshUUID
- // ReshVersion
- // ReshRevision
-
- // added by sanitizatizer
- // Sanitized
- // CmdLength
-
- return dist
-}
-
-// LoadFromFile loads records from 'fname' file
-func LoadFromFile(fname string, limit int) []Record {
- const allowedErrors = 1
- var encounteredErrors int
- // NOTE: limit does nothing atm
- var recs []Record
- file, err := os.Open(fname)
- if err != nil {
- log.Println("Open() resh history file error:", err)
- log.Println("WARN: Skipping reading resh history!")
- return recs
- }
- defer file.Close()
-
- reader := bufio.NewReader(file)
- var i int
- var firstErrLine int
- for {
- line, err := reader.ReadString('\n')
- if err != nil {
- break
- }
- i++
- record := Record{}
- fallbackRecord := FallbackRecord{}
- err = json.Unmarshal([]byte(line), &record)
- if err != nil {
- err = json.Unmarshal([]byte(line), &fallbackRecord)
- if err != nil {
- if encounteredErrors == 0 {
- firstErrLine = i
- }
- encounteredErrors++
- log.Println("Line:", line)
- log.Println("Decoding error:", err)
- if encounteredErrors > allowedErrors {
- log.Fatalf("Fatal: Encountered more than %d decoding errors (%d)", allowedErrors, encounteredErrors)
- }
- }
- record = Convert(&fallbackRecord)
- }
- recs = append(recs, record)
- }
- // log.Println("records: done loading file:", err)
- if err != io.EOF {
- log.Println("records: error while loading file:", err)
- }
- // log.Println("records: Loaded lines - count:", i)
- if encounteredErrors > 0 {
- // fix errors in the history file
- log.Printf("There were %d decoding errors, the first error happend on line %d/%d", encounteredErrors, firstErrLine, i)
- log.Println("Backing up current history file ...")
- err := copyFile(fname, fname+".bak")
- if err != nil {
- log.Fatalln("Failed to backup history file with decode errors")
- }
- log.Println("Writing out a history file without errors ...")
- err = writeHistory(fname, recs)
- if err != nil {
- log.Fatalln("Fatal: Failed write out new history")
- }
- }
- log.Println("records: Loaded records - count:", len(recs))
- return recs
-}
-
-func copyFile(source, dest string) error {
- from, err := os.Open(source)
- if err != nil {
- // log.Println("Open() resh history file error:", err)
- return err
- }
- defer from.Close()
-
- // to, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0666)
- to, err := os.Create(dest)
- if err != nil {
- // log.Println("Create() resh history backup error:", err)
- return err
- }
- defer to.Close()
-
- _, err = io.Copy(to, from)
- if err != nil {
- // log.Println("Copy() resh history to backup error:", err)
- return err
- }
- return nil
-}
-
-func writeHistory(fname string, history []Record) error {
- file, err := os.Create(fname)
- if err != nil {
- // log.Println("Create() resh history error:", err)
- return err
- }
- defer file.Close()
- for _, rec := range history {
- jsn, err := json.Marshal(rec)
- if err != nil {
- log.Fatalln("Encode error!")
- }
- file.Write(append(jsn, []byte("\n")...))
- }
- return nil
-}
-
-// LoadCmdLinesFromZshFile loads cmdlines from zsh history file
-func LoadCmdLinesFromZshFile(fname string) histlist.Histlist {
- hl := histlist.New()
- file, err := os.Open(fname)
- if err != nil {
- log.Println("Open() zsh history file error:", err)
- log.Println("WARN: Skipping reading zsh history!")
- return hl
- }
- defer file.Close()
-
- 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 {
- hl := histlist.New()
- file, err := os.Open(fname)
- if err != nil {
- log.Println("Open() bash history file error:", err)
- log.Println("WARN: Skipping reading bash history!")
- return hl
- }
- defer file.Close()
-
- 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/records/records_test.go b/pkg/records/records_test.go
deleted file mode 100644
index 5ef3c55a..00000000
--- a/pkg/records/records_test.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package records
-
-import (
- "bufio"
- "encoding/json"
- "log"
- "os"
- "testing"
-)
-
-func GetTestRecords() []Record {
- file, err := os.Open("testdata/resh_history.json")
- if err != nil {
- log.Fatal("Open() resh history file error:", err)
- }
- defer file.Close()
-
- var recs []Record
- scanner := bufio.NewScanner(file)
- for scanner.Scan() {
- record := Record{}
- line := scanner.Text()
- err = json.Unmarshal([]byte(line), &record)
- if err != nil {
- log.Println("Line:", line)
- log.Fatal("Decoding error:", err)
- }
- recs = append(recs, record)
- }
- return recs
-}
-
-func GetTestEnrichedRecords() []EnrichedRecord {
- var recs []EnrichedRecord
- for _, rec := range GetTestRecords() {
- recs = append(recs, Enriched(rec))
- }
- return recs
-}
-
-func TestToString(t *testing.T) {
- for _, rec := range GetTestEnrichedRecords() {
- _, err := rec.ToString()
- if err != nil {
- t.Error("ToString() failed")
- }
- }
-}
-
-func TestEnriched(t *testing.T) {
- record := Record{BaseRecord: BaseRecord{CmdLine: "cmd arg1 arg2"}}
- enriched := Enriched(record)
- if enriched.FirstWord != "cmd" || enriched.Command != "cmd" {
- t.Error("Enriched() returned reocord w/ wrong Command OR FirstWord")
- }
-}
-
-func TestValidate(t *testing.T) {
- record := EnrichedRecord{}
- if record.Validate() == nil {
- t.Error("Validate() didn't return an error for invalid record")
- }
- record.CmdLine = "cmd arg"
- record.FirstWord = "cmd"
- record.Command = "cmd"
- time := 1234.5678
- record.RealtimeBefore = time
- record.RealtimeAfter = time
- record.RealtimeBeforeLocal = time
- record.RealtimeAfterLocal = time
- pwd := "/pwd"
- record.Pwd = pwd
- record.PwdAfter = pwd
- record.RealPwd = pwd
- record.RealPwdAfter = pwd
- if record.Validate() != nil {
- t.Error("Validate() returned an error for a valid record")
- }
-}
-
-func TestSetCmdLine(t *testing.T) {
- record := EnrichedRecord{}
- cmdline := "cmd arg1 arg2"
- record.SetCmdLine(cmdline)
- if record.CmdLine != cmdline || record.Command != "cmd" || record.FirstWord != "cmd" {
- t.Error()
- }
-}
-
-func TestStripped(t *testing.T) {
- for _, rec := range GetTestEnrichedRecords() {
- stripped := Stripped(rec)
-
- // there should be no cmdline
- if stripped.CmdLine != "" ||
- stripped.FirstWord != "" ||
- stripped.Command != "" {
- t.Error("Stripped() returned record w/ info about CmdLine, Command OR FirstWord")
- }
- // *after* fields should be overwritten by *before* fields
- if stripped.PwdAfter != stripped.Pwd ||
- stripped.RealPwdAfter != stripped.RealPwd ||
- stripped.TimezoneAfter != stripped.TimezoneBefore ||
- stripped.RealtimeAfter != stripped.RealtimeBefore ||
- stripped.RealtimeAfterLocal != stripped.RealtimeBeforeLocal {
- t.Error("Stripped() returned record w/ different *after* and *before* values - *after* fields should be overwritten by *before* fields")
- }
- // there should be no information about duration and session end
- if stripped.RealtimeDuration != 0 ||
- stripped.LastRecordOfSession != false {
- t.Error("Stripped() returned record with too much information")
- }
- }
-}
-
-func TestGetCommandAndFirstWord(t *testing.T) {
- cmd, stWord, err := GetCommandAndFirstWord("cmd arg1 arg2")
- if err != nil || cmd != "cmd" || stWord != "cmd" {
- t.Error("GetCommandAndFirstWord() returned wrong Command OR FirstWord")
- }
-}
-
-func TestDistanceTo(t *testing.T) {
- paramsFull := DistParams{
- ExitCode: 1,
- MachineID: 1,
- SessionID: 1,
- Login: 1,
- Shell: 1,
- Pwd: 1,
- RealPwd: 1,
- Git: 1,
- Time: 1,
- }
- paramsZero := DistParams{}
- var prevRec EnrichedRecord
- for _, rec := range GetTestEnrichedRecords() {
- dist := rec.DistanceTo(rec, paramsFull)
- if dist != 0 {
- t.Error("DistanceTo() itself should be always 0")
- }
- dist = rec.DistanceTo(prevRec, paramsFull)
- if dist == 0 {
- t.Error("DistanceTo() between two test records shouldn't be 0")
- }
- dist = rec.DistanceTo(prevRec, paramsZero)
- if dist != 0 {
- t.Error("DistanceTo() should be 0 when DistParams is all zeros")
- }
- prevRec = rec
- }
-}
diff --git a/pkg/records/testdata/resh_history.json b/pkg/records/testdata/resh_history.json
deleted file mode 100644
index 40f43ab6..00000000
--- a/pkg/records/testdata/resh_history.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{"cmdLine":"ls","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"d5c0fe70-c80b-4715-87cb-f8d8d5b4c673","cols":"80","lines":"24","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":14560,"sessionPid":14560,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1566762905.173595,"realtimeAfter":1566762905.1894295,"realtimeBeforeLocal":1566770105.173595,"realtimeAfterLocal":1566770105.1894295,"realtimeDuration":0.015834569931030273,"realtimeSinceSessionStart":1.7122540473937988,"realtimeSinceBoot":20766.542254047396,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"}
-{"cmdLine":"find . -name applications","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"c5251955-3a64-4353-952e-08d62a898694","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":3109,"sessionPid":3109,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567420001.2531302,"realtimeAfter":1567420002.4311218,"realtimeBeforeLocal":1567427201.2531302,"realtimeAfterLocal":1567427202.4311218,"realtimeDuration":1.1779916286468506,"realtimeSinceSessionStart":957.4848053455353,"realtimeSinceBoot":2336.594805345535,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"}
-{"cmdLine":"desktop-file-validate curusarn.sync-clipboards.desktop ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"c5251955-3a64-4353-952e-08d62a898694","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/.local/share/applications","pwdAfter":"/home/simon/.local/share/applications","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/.local/share/applications","realPwdAfter":"/home/simon/.local/share/applications","pid":3109,"sessionPid":3109,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567421748.2965438,"realtimeAfter":1567421748.3068867,"realtimeBeforeLocal":1567428948.2965438,"realtimeAfterLocal":1567428948.3068867,"realtimeDuration":0.010342836380004883,"realtimeSinceSessionStart":2704.528218984604,"realtimeSinceBoot":4083.6382189846036,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"}
-{"cmdLine":"cat /tmp/extensions | grep '.'","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461416.6871984,"realtimeAfter":1567461416.7336714,"realtimeBeforeLocal":1567468616.6871984,"realtimeAfterLocal":1567468616.7336714,"realtimeDuration":0.046473026275634766,"realtimeSinceSessionStart":21.45597553253174,"realtimeSinceBoot":43752.03597553253,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"}
-{"cmdLine":"cd git/resh/","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461667.8806899,"realtimeAfter":1567461667.8949044,"realtimeBeforeLocal":1567468867.8806899,"realtimeAfterLocal":1567468867.8949044,"realtimeDuration":0.014214515686035156,"realtimeSinceSessionStart":272.64946699142456,"realtimeSinceBoot":44003.229466991426,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"}
-{"cmdLine":"git s","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461707.6467602,"realtimeAfter":1567461707.7177293,"realtimeBeforeLocal":1567468907.6467602,"realtimeAfterLocal":1567468907.7177293,"realtimeDuration":0.0709691047668457,"realtimeSinceSessionStart":312.4155373573303,"realtimeSinceBoot":44042.99553735733,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"}
-{"cmdLine":"cat /tmp/extensions | grep '^\\.' | cut -f1 |tr '[:upper:]' '[:lower:]' ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461722.813049,"realtimeAfter":1567461722.8280325,"realtimeBeforeLocal":1567468922.813049,"realtimeAfterLocal":1567468922.8280325,"realtimeDuration":0.014983415603637695,"realtimeSinceSessionStart":327.581826210022,"realtimeSinceBoot":44058.161826210024,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"}
-{"cmdLine":"tig","exitCode":127,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461906.3896828,"realtimeAfter":1567461906.4084594,"realtimeBeforeLocal":1567469106.3896828,"realtimeAfterLocal":1567469106.4084594,"realtimeDuration":0.018776655197143555,"realtimeSinceSessionStart":511.1584599018097,"realtimeSinceBoot":44241.73845990181,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"}
-{"cmdLine":"resh-sanitize-history | jq","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"a3318c80-3521-4b22-aa64-ea0f6c641410","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":14601,"sessionPid":14601,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567547116.2430356,"realtimeAfter":1567547116.7547352,"realtimeBeforeLocal":1567554316.2430356,"realtimeAfterLocal":1567554316.7547352,"realtimeDuration":0.5116996765136719,"realtimeSinceSessionStart":15.841878414154053,"realtimeSinceBoot":30527.201878414155,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"sudo pacman -S ansible","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"64154f2d-a4bc-4463-a690-520080b61ead","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/kristin","pwdAfter":"/home/simon/git/kristin","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/kristin","realPwdAfter":"/home/simon/git/kristin","pid":5663,"sessionPid":5663,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567609042.0166302,"realtimeAfter":1567609076.9726007,"realtimeBeforeLocal":1567616242.0166302,"realtimeAfterLocal":1567616276.9726007,"realtimeDuration":34.95597052574158,"realtimeSinceSessionStart":1617.0794131755829,"realtimeSinceBoot":6120.029413175583,"gitDir":"/home/simon/git/kristin","gitRealDir":"/home/simon/git/kristin","gitOriginRemote":"git@gitlab.com:sucvut/kristin.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"vagrant up","exitCode":1,"shell":"bash","uname":"Linux","sessionId":"64154f2d-a4bc-4463-a690-520080b61ead","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/kristin","pwdAfter":"/home/simon/git/kristin","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/kristin","realPwdAfter":"/home/simon/git/kristin","pid":5663,"sessionPid":5663,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567609090.7359188,"realtimeAfter":1567609098.3125577,"realtimeBeforeLocal":1567616290.7359188,"realtimeAfterLocal":1567616298.3125577,"realtimeDuration":7.57663893699646,"realtimeSinceSessionStart":1665.798701763153,"realtimeSinceBoot":6168.748701763153,"gitDir":"/home/simon/git/kristin","gitRealDir":"/home/simon/git/kristin","gitOriginRemote":"git@gitlab.com:sucvut/kristin.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"sudo modprobe vboxnetflt","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"64154f2d-a4bc-4463-a690-520080b61ead","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/kristin","pwdAfter":"/home/simon/git/kristin","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/kristin","realPwdAfter":"/home/simon/git/kristin","pid":5663,"sessionPid":5663,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567609143.2847652,"realtimeAfter":1567609143.3116078,"realtimeBeforeLocal":1567616343.2847652,"realtimeAfterLocal":1567616343.3116078,"realtimeDuration":0.026842594146728516,"realtimeSinceSessionStart":1718.3475482463837,"realtimeSinceBoot":6221.2975482463835,"gitDir":"/home/simon/git/kristin","gitRealDir":"/home/simon/git/kristin","gitOriginRemote":"git@gitlab.com:sucvut/kristin.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"echo $RANDOM","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"8ddacadc-6e73-483c-b347-4e18df204466","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":31387,"sessionPid":31387,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567727039.6540458,"realtimeAfter":1567727039.6629689,"realtimeBeforeLocal":1567734239.6540458,"realtimeAfterLocal":1567734239.6629689,"realtimeDuration":0.008923053741455078,"realtimeSinceSessionStart":1470.7667458057404,"realtimeSinceBoot":18495.01674580574,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"make resh-evaluate ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567977478.9672194,"realtimeAfter":1567977479.5449634,"realtimeBeforeLocal":1567984678.9672194,"realtimeAfterLocal":1567984679.5449634,"realtimeDuration":0.5777440071105957,"realtimeSinceSessionStart":5738.577540636063,"realtimeSinceBoot":20980.42754063606,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"cat ~/.resh_history.json | grep \"./resh-eval\" | jq","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"105","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567986105.3988302,"realtimeAfter":1567986105.4809113,"realtimeBeforeLocal":1567993305.3988302,"realtimeAfterLocal":1567993305.4809113,"realtimeDuration":0.08208107948303223,"realtimeSinceSessionStart":14365.00915145874,"realtimeSinceBoot":29606.85915145874,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"git c \"add sanitized flag to record, add Enrich() to record\"","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568063976.9103937,"realtimeAfter":1568063976.9326868,"realtimeBeforeLocal":1568071176.9103937,"realtimeAfterLocal":1568071176.9326868,"realtimeDuration":0.0222930908203125,"realtimeSinceSessionStart":92236.52071499825,"realtimeSinceBoot":107478.37071499825,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"git s","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568063978.2340608,"realtimeAfter":1568063978.252463,"realtimeBeforeLocal":1568071178.2340608,"realtimeAfterLocal":1568071178.252463,"realtimeDuration":0.0184023380279541,"realtimeSinceSessionStart":92237.84438204765,"realtimeSinceBoot":107479.69438204766,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"git a evaluate/results.go ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568063989.0446353,"realtimeAfter":1568063989.2452207,"realtimeBeforeLocal":1568071189.0446353,"realtimeAfterLocal":1568071189.2452207,"realtimeDuration":0.20058536529541016,"realtimeSinceSessionStart":92248.65495657921,"realtimeSinceBoot":107490.50495657921,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"sudo pacman -S python-pip","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568072068.3557143,"realtimeAfter":1568072070.7509863,"realtimeBeforeLocal":1568079268.3557143,"realtimeAfterLocal":1568079270.7509863,"realtimeDuration":2.3952720165252686,"realtimeSinceSessionStart":100327.96603560448,"realtimeSinceBoot":115569.81603560448,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"pip3 install matplotlib","exitCode":1,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568072088.5575967,"realtimeAfter":1568072094.372314,"realtimeBeforeLocal":1568079288.5575967,"realtimeAfterLocal":1568079294.372314,"realtimeDuration":5.8147172927856445,"realtimeSinceSessionStart":100348.16791796684,"realtimeSinceBoot":115590.01791796685,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"sudo pip3 install matplotlib","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568072106.138616,"realtimeAfter":1568072115.1124601,"realtimeBeforeLocal":1568079306.138616,"realtimeAfterLocal":1568079315.1124601,"realtimeDuration":8.973844051361084,"realtimeSinceSessionStart":100365.7489373684,"realtimeSinceBoot":115607.5989373684,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"./resh-evaluate --plotting-script evaluate/resh-evaluate-plot.py --input ~/git/resh_private/history_data/simon/dell/resh_history.json ","exitCode":130,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568076266.9364285,"realtimeAfter":1568076288.1131275,"realtimeBeforeLocal":1568083466.9364285,"realtimeAfterLocal":1568083488.1131275,"realtimeDuration":21.176698923110962,"realtimeSinceSessionStart":104526.54674983025,"realtimeSinceBoot":119768.39674983025,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0}
-{"cmdLine":"git c \"Add a bunch of useless comments to make linter happy\"","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"04050353-a97d-4435-9248-f47dd08b2f2a","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":14702,"sessionPid":14702,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569456045.8763022,"realtimeAfter":1569456045.9030173,"realtimeBeforeLocal":1569463245.8763022,"realtimeAfterLocal":1569463245.9030173,"realtimeDuration":0.02671504020690918,"realtimeSinceSessionStart":2289.789242744446,"realtimeSinceBoot":143217.91924274445,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false}
-{"cmdLine":"fuck","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"a4aadf03-610d-4731-ba94-5b7ce21e7bb9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":3413,"sessionPid":3413,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569687682.4250975,"realtimeAfter":1569687682.5877323,"realtimeBeforeLocal":1569694882.4250975,"realtimeAfterLocal":1569694882.5877323,"realtimeDuration":0.16263484954833984,"realtimeSinceSessionStart":264603.49496507645,"realtimeSinceBoot":374854.48496507644,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false}
-{"cmdLine":"code .","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"87c7ab14-ae51-408d-adbc-fc4f9d28de6e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":31947,"sessionPid":31947,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569709366.523767,"realtimeAfter":1569709367.516908,"realtimeBeforeLocal":1569716566.523767,"realtimeAfterLocal":1569716567.516908,"realtimeDuration":0.9931409358978271,"realtimeSinceSessionStart":23846.908839941025,"realtimeSinceBoot":396539.888839941,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false}
-{"cmdLine":"make test","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"87c7ab14-ae51-408d-adbc-fc4f9d28de6e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":31947,"sessionPid":31947,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569709371.89966,"realtimeAfter":1569709377.430194,"realtimeBeforeLocal":1569716571.89966,"realtimeAfterLocal":1569716577.430194,"realtimeDuration":5.530533790588379,"realtimeSinceSessionStart":23852.284733057022,"realtimeSinceBoot":396545.264733057,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false}
-{"cmdLine":"mkdir ~/git/resh/testdata","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"71529b60-2e7b-4d5b-8dc1-6d0740b58e9e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":21224,"sessionPid":21224,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569709838.4642656,"realtimeAfter":1569709838.4718792,"realtimeBeforeLocal":1569717038.4642656,"realtimeAfterLocal":1569717038.4718792,"realtimeDuration":0.007613658905029297,"realtimeSinceSessionStart":9.437154054641724,"realtimeSinceBoot":397011.02715405467,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false}
diff --git a/pkg/searchapp/test.go b/pkg/searchapp/test.go
deleted file mode 100644
index e33e2f77..00000000
--- a/pkg/searchapp/test.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package searchapp
-
-import (
- "math"
-
- "github.com/curusarn/resh/pkg/histcli"
- "github.com/curusarn/resh/pkg/msg"
- "github.com/curusarn/resh/pkg/records"
-)
-
-// LoadHistoryFromFile ...
-func LoadHistoryFromFile(historyPath string, numLines int) msg.CliResponse {
- recs := records.LoadFromFile(historyPath, math.MaxInt32)
- if numLines != 0 && numLines < len(recs) {
- recs = recs[:numLines]
- }
- cliRecords := histcli.New()
- for i := len(recs) - 1; i >= 0; i-- {
- rec := recs[i]
- cliRecords.AddRecord(rec)
- }
- return msg.CliResponse{CliRecords: cliRecords.List}
-}
diff --git a/pkg/sesshist/sesshist.go b/pkg/sesshist/sesshist.go
deleted file mode 100644
index f7227642..00000000
--- a/pkg/sesshist/sesshist.go
+++ /dev/null
@@ -1,243 +0,0 @@
-package sesshist
-
-import (
- "errors"
- "log"
- "strconv"
- "strings"
- "sync"
-
- "github.com/curusarn/resh/pkg/histfile"
- "github.com/curusarn/resh/pkg/histlist"
- "github.com/curusarn/resh/pkg/records"
-)
-
-// Dispatch Recall() calls to an apropriate session history (sesshist)
-type Dispatch struct {
- sessions map[string]*sesshist
- mutex sync.RWMutex
-
- history *histfile.Histfile
- historyInitSize int
-}
-
-// NewDispatch creates a new sesshist.Dispatch and starts necessary gorutines
-func NewDispatch(sessionsToInit chan records.Record, sessionsToDrop chan string,
- recordsToAdd chan records.Record, history *histfile.Histfile, historyInitSize int) *Dispatch {
-
- s := Dispatch{
- sessions: map[string]*sesshist{},
- history: history,
- historyInitSize: historyInitSize,
- }
- go s.sessionInitializer(sessionsToInit)
- go s.sessionDropper(sessionsToDrop)
- go s.recordAdder(recordsToAdd)
- return &s
-}
-
-func (s *Dispatch) sessionInitializer(sessionsToInit chan records.Record) {
- for {
- record := <-sessionsToInit
- log.Println("sesshist: got session to init - " + record.SessionID)
- s.initSession(record.SessionID, record.Shell)
- }
-}
-
-func (s *Dispatch) sessionDropper(sessionsToDrop chan string) {
- for {
- sessionID := <-sessionsToDrop
- log.Println("sesshist: got session to drop - " + sessionID)
- s.dropSession(sessionID)
- }
-}
-
-func (s *Dispatch) recordAdder(recordsToAdd chan records.Record) {
- for {
- record := <-recordsToAdd
- 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, shell string) error {
- log.Println("sesshist: initializing session - " + sessionID)
- s.mutex.RLock()
- _, found := s.sessions[sessionID]
- s.mutex.RUnlock()
-
- if found == true {
- return errors.New("sesshist ERROR: Can't INIT already existing session " + sessionID)
- }
-
- log.Println("sesshist: loading history to populate session - " + sessionID)
- 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,
- }
- log.Println("sesshist: session init done - " + sessionID)
- return nil
-}
-
-// DropSession struct
-func (s *Dispatch) dropSession(sessionID string) error {
- s.mutex.RLock()
- _, found := s.sessions[sessionID]
- s.mutex.RUnlock()
-
- if found == false {
- return errors.New("sesshist ERROR: Can't DROP not existing session " + sessionID)
- }
-
- s.mutex.Lock()
- defer s.mutex.Unlock()
- delete(s.sessions, sessionID)
- return nil
-}
-
-// 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: 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)
- session.recentCmdLines.AddCmdLine(record.CmdLine)
- log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID,
- "; 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()
-
- if found == false {
- // TODO: propagate actual shell here so we can use it
- go s.initSession(sessionID, "bash")
- return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - creating one ...")
- }
- log.Println("sesshist - recall: Locking session lock ...")
- session.mutex.Lock()
- defer session.mutex.Unlock()
- if prefix == "" {
- 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()
- 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 {
- mutex sync.Mutex
- recentRecords []records.Record
- recentCmdLines histlist.Histlist
-}
-
-func (s *sesshist) getRecordByHistno(histno int) (string, error) {
- // 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")
- }
- if histno < 0 {
- return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)")
- }
- 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.List)) + ")")
- }
- return s.recentCmdLines.List[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.List) - histno
- if index < 0 {
- 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.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
- }
- }
- }
- if len(cmdLines) < histno {
- return "", errors.New("sesshist ERROR: 'histno > number of commands matching with given prefix' (" + strconv.Itoa(len(cmdLines)) + ")")
- }
- return cmdLines[histno-1], nil
-}
diff --git a/pkg/sesswatch/sesswatch.go b/pkg/sesswatch/sesswatch.go
deleted file mode 100644
index ae32bc49..00000000
--- a/pkg/sesswatch/sesswatch.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package sesswatch
-
-import (
- "log"
- "sync"
- "time"
-
- "github.com/curusarn/resh/pkg/records"
- "github.com/mitchellh/go-ps"
-)
-
-type sesswatch struct {
- sessionsToDrop []chan string
- sleepSeconds uint
-
- watchedSessions map[string]bool
- mutex sync.Mutex
-}
-
-// Go runs the session watcher - watches sessions and sends
-func Go(sessionsToWatch chan records.Record, sessionsToWatchRecords chan records.Record, sessionsToDrop []chan string, sleepSeconds uint) {
- sw := sesswatch{sessionsToDrop: sessionsToDrop, sleepSeconds: sleepSeconds, watchedSessions: map[string]bool{}}
- go sw.waiter(sessionsToWatch, sessionsToWatchRecords)
-}
-
-func (s *sesswatch) waiter(sessionsToWatch chan records.Record, sessionsToWatchRecords chan records.Record) {
- for {
- func() {
- select {
- case record := <-sessionsToWatch:
- // normal way to start watching a session
- id := record.SessionID
- pid := record.SessionPID
- s.mutex.Lock()
- defer s.mutex.Unlock()
- if s.watchedSessions[id] == false {
- log.Println("sesswatch: start watching NEW session ~ pid:", id, "~", pid)
- s.watchedSessions[id] = true
- go s.watcher(id, pid)
- }
- case record := <-sessionsToWatchRecords:
- // additional safety - watch sessions that were never properly initialized
- id := record.SessionID
- pid := record.SessionPID
- s.mutex.Lock()
- defer s.mutex.Unlock()
- if s.watchedSessions[id] == false {
- log.Println("sesswatch WARN: start watching NEW session (based on /record) ~ pid:", id, "~", pid)
- s.watchedSessions[id] = true
- go s.watcher(id, pid)
- }
- }
- }()
- }
-}
-
-func (s *sesswatch) watcher(sessionID string, sessionPID int) {
- for {
- time.Sleep(time.Duration(s.sleepSeconds) * time.Second)
- proc, err := ps.FindProcess(sessionPID)
- if err != nil {
- log.Println("sesswatch ERROR: error while finding process:", sessionPID)
- } else if proc == nil {
- log.Println("sesswatch: Dropping session ~ pid:", sessionID, "~", sessionPID)
- func() {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.watchedSessions[sessionID] = false
- }()
- for _, ch := range s.sessionsToDrop {
- log.Println("sesswatch: sending 'drop session' message ...")
- ch <- sessionID
- log.Println("sesswatch: sending 'drop session' message DONE")
- }
- break
- }
- }
-}
diff --git a/pkg/signalhandler/signalhander.go b/pkg/signalhandler/signalhander.go
deleted file mode 100644
index 5d2233e4..00000000
--- a/pkg/signalhandler/signalhander.go
+++ /dev/null
@@ -1,65 +0,0 @@
-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 components 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, syscall.SIGQUIT)
-
- var sig os.Signal
- for {
- sig := <-signals
- log.Printf("signalhandler: Got signal '%s'\n", sig.String())
- if sig == syscall.SIGTERM {
- // Shutdown daemon on SIGTERM
- break
- }
- log.Printf("signalhandler: Ignoring signal '%s'. Send SIGTERM to trigger shutdown.\n", sig.String())
- }
-
- log.Println("signalhandler: Sending shutdown signals to components")
- 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/pkg/strat/directory-sensitive.go b/pkg/strat/directory-sensitive.go
deleted file mode 100644
index 89d030e7..00000000
--- a/pkg/strat/directory-sensitive.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package strat
-
-import "github.com/curusarn/resh/pkg/records"
-
-// DirectorySensitive prediction/recommendation strategy
-type DirectorySensitive struct {
- history map[string][]string
- lastPwd string
-}
-
-// Init see name
-func (s *DirectorySensitive) Init() {
- s.history = map[string][]string{}
-}
-
-// GetTitleAndDescription see name
-func (s *DirectorySensitive) GetTitleAndDescription() (string, string) {
- return "directory sensitive (recent)", "Use recent commands executed is the same directory"
-}
-
-// GetCandidates see name
-func (s *DirectorySensitive) GetCandidates() []string {
- return s.history[s.lastPwd]
-}
-
-// AddHistoryRecord see name
-func (s *DirectorySensitive) AddHistoryRecord(record *records.EnrichedRecord) error {
- // work on history for PWD
- pwd := record.Pwd
- // remove previous occurance of record
- for i, cmd := range s.history[pwd] {
- if cmd == record.CmdLine {
- s.history[pwd] = append(s.history[pwd][:i], s.history[pwd][i+1:]...)
- }
- }
- // append new record
- s.history[pwd] = append([]string{record.CmdLine}, s.history[pwd]...)
- s.lastPwd = record.PwdAfter
- return nil
-}
-
-// ResetHistory see name
-func (s *DirectorySensitive) ResetHistory() error {
- s.Init()
- s.history = map[string][]string{}
- return nil
-}
diff --git a/pkg/strat/dummy.go b/pkg/strat/dummy.go
deleted file mode 100644
index fc813f2a..00000000
--- a/pkg/strat/dummy.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package strat
-
-import "github.com/curusarn/resh/pkg/records"
-
-// Dummy prediction/recommendation strategy
-type Dummy struct {
- history []string
-}
-
-// GetTitleAndDescription see name
-func (s *Dummy) GetTitleAndDescription() (string, string) {
- return "dummy", "Return empty candidate list"
-}
-
-// GetCandidates see name
-func (s *Dummy) GetCandidates() []string {
- return nil
-}
-
-// AddHistoryRecord see name
-func (s *Dummy) AddHistoryRecord(record *records.EnrichedRecord) error {
- s.history = append(s.history, record.CmdLine)
- return nil
-}
-
-// ResetHistory see name
-func (s *Dummy) ResetHistory() error {
- return nil
-}
diff --git a/pkg/strat/dynamic-record-distance.go b/pkg/strat/dynamic-record-distance.go
deleted file mode 100644
index 1f779c27..00000000
--- a/pkg/strat/dynamic-record-distance.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package strat
-
-import (
- "math"
- "sort"
- "strconv"
-
- "github.com/curusarn/resh/pkg/records"
-)
-
-// DynamicRecordDistance prediction/recommendation strategy
-type DynamicRecordDistance struct {
- history []records.EnrichedRecord
- DistParams records.DistParams
- pwdHistogram map[string]int
- realPwdHistogram map[string]int
- gitOriginHistogram map[string]int
- MaxDepth int
- Label string
-}
-
-type strDynDistEntry struct {
- cmdLine string
- distance float64
-}
-
-// Init see name
-func (s *DynamicRecordDistance) Init() {
- s.history = nil
- s.pwdHistogram = map[string]int{}
- s.realPwdHistogram = map[string]int{}
- s.gitOriginHistogram = map[string]int{}
-}
-
-// GetTitleAndDescription see name
-func (s *DynamicRecordDistance) GetTitleAndDescription() (string, string) {
- return "dynamic record distance (depth:" + strconv.Itoa(s.MaxDepth) + ";" + s.Label + ")", "Use TF-IDF record distance to recommend commands"
-}
-
-func (s *DynamicRecordDistance) idf(count int) float64 {
- return math.Log(float64(len(s.history)) / float64(count))
-}
-
-// GetCandidates see name
-func (s *DynamicRecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string {
- if len(s.history) == 0 {
- return nil
- }
- var mapItems []strDynDistEntry
- for i, record := range s.history {
- if s.MaxDepth != 0 && i > s.MaxDepth {
- break
- }
- distParams := records.DistParams{
- Pwd: s.DistParams.Pwd * s.idf(s.pwdHistogram[strippedRecord.PwdAfter]),
- RealPwd: s.DistParams.RealPwd * s.idf(s.realPwdHistogram[strippedRecord.RealPwdAfter]),
- Git: s.DistParams.Git * s.idf(s.gitOriginHistogram[strippedRecord.GitOriginRemote]),
- Time: s.DistParams.Time,
- SessionID: s.DistParams.SessionID,
- }
- distance := record.DistanceTo(strippedRecord, distParams)
- mapItems = append(mapItems, strDynDistEntry{record.CmdLine, distance})
- }
- sort.SliceStable(mapItems, func(i int, j int) bool { return mapItems[i].distance < mapItems[j].distance })
- var hist []string
- histSet := map[string]bool{}
- for _, item := range mapItems {
- if histSet[item.cmdLine] {
- continue
- }
- histSet[item.cmdLine] = true
- hist = append(hist, item.cmdLine)
- }
- return hist
-}
-
-// AddHistoryRecord see name
-func (s *DynamicRecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error {
- // append record to front
- s.history = append([]records.EnrichedRecord{*record}, s.history...)
- s.pwdHistogram[record.Pwd]++
- s.realPwdHistogram[record.RealPwd]++
- s.gitOriginHistogram[record.GitOriginRemote]++
- return nil
-}
-
-// ResetHistory see name
-func (s *DynamicRecordDistance) ResetHistory() error {
- s.Init()
- return nil
-}
diff --git a/pkg/strat/frequent.go b/pkg/strat/frequent.go
deleted file mode 100644
index ff3b9125..00000000
--- a/pkg/strat/frequent.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package strat
-
-import (
- "sort"
-
- "github.com/curusarn/resh/pkg/records"
-)
-
-// Frequent prediction/recommendation strategy
-type Frequent struct {
- history map[string]int
-}
-
-type strFrqEntry struct {
- cmdLine string
- count int
-}
-
-// Init see name
-func (s *Frequent) Init() {
- s.history = map[string]int{}
-}
-
-// GetTitleAndDescription see name
-func (s *Frequent) GetTitleAndDescription() (string, string) {
- return "frequent", "Use frequent commands"
-}
-
-// GetCandidates see name
-func (s *Frequent) GetCandidates() []string {
- var mapItems []strFrqEntry
- for cmdLine, count := range s.history {
- mapItems = append(mapItems, strFrqEntry{cmdLine, count})
- }
- sort.Slice(mapItems, func(i int, j int) bool { return mapItems[i].count > mapItems[j].count })
- var hist []string
- for _, item := range mapItems {
- hist = append(hist, item.cmdLine)
- }
- return hist
-}
-
-// AddHistoryRecord see name
-func (s *Frequent) AddHistoryRecord(record *records.EnrichedRecord) error {
- s.history[record.CmdLine]++
- return nil
-}
-
-// ResetHistory see name
-func (s *Frequent) ResetHistory() error {
- s.Init()
- return nil
-}
diff --git a/pkg/strat/markov-chain-cmd.go b/pkg/strat/markov-chain-cmd.go
deleted file mode 100644
index b1fa2f5b..00000000
--- a/pkg/strat/markov-chain-cmd.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package strat
-
-import (
- "sort"
- "strconv"
-
- "github.com/curusarn/resh/pkg/records"
- "github.com/mb-14/gomarkov"
-)
-
-// MarkovChainCmd prediction/recommendation strategy
-type MarkovChainCmd struct {
- Order int
- history []strMarkCmdHistoryEntry
- historyCmds []string
-}
-
-type strMarkCmdHistoryEntry struct {
- cmd string
- cmdLine string
-}
-
-type strMarkCmdEntry struct {
- cmd string
- transProb float64
-}
-
-// Init see name
-func (s *MarkovChainCmd) Init() {
- s.history = nil
- s.historyCmds = nil
-}
-
-// GetTitleAndDescription see name
-func (s *MarkovChainCmd) GetTitleAndDescription() (string, string) {
- return "command-based markov chain (order " + strconv.Itoa(s.Order) + ")", "Use command-based markov chain to recommend commands"
-}
-
-// GetCandidates see name
-func (s *MarkovChainCmd) GetCandidates() []string {
- if len(s.history) < s.Order {
- var hist []string
- for _, item := range s.history {
- hist = append(hist, item.cmdLine)
- }
- return hist
- }
- chain := gomarkov.NewChain(s.Order)
-
- chain.Add(s.historyCmds)
-
- cmdsSet := map[string]bool{}
- var entries []strMarkCmdEntry
- for _, cmd := range s.historyCmds {
- if cmdsSet[cmd] {
- continue
- }
- cmdsSet[cmd] = true
- prob, _ := chain.TransitionProbability(cmd, s.historyCmds[len(s.historyCmds)-s.Order:])
- entries = append(entries, strMarkCmdEntry{cmd: cmd, transProb: prob})
- }
- sort.Slice(entries, func(i int, j int) bool { return entries[i].transProb > entries[j].transProb })
- var hist []string
- histSet := map[string]bool{}
- for i := len(s.history) - 1; i >= 0; i-- {
- if histSet[s.history[i].cmdLine] {
- continue
- }
- histSet[s.history[i].cmdLine] = true
- if s.history[i].cmd == entries[0].cmd {
- hist = append(hist, s.history[i].cmdLine)
- }
- }
- // log.Println("################")
- // log.Println(s.history[len(s.history)-s.order:])
- // log.Println(" -> ")
- // x := math.Min(float64(len(hist)), 3)
- // log.Println(entries[:int(x)])
- // x = math.Min(float64(len(hist)), 5)
- // log.Println(hist[:int(x)])
- // log.Println("################")
- return hist
-}
-
-// AddHistoryRecord see name
-func (s *MarkovChainCmd) AddHistoryRecord(record *records.EnrichedRecord) error {
- s.history = append(s.history, strMarkCmdHistoryEntry{cmdLine: record.CmdLine, cmd: record.Command})
- s.historyCmds = append(s.historyCmds, record.Command)
- // s.historySet[record.CmdLine] = true
- return nil
-}
-
-// ResetHistory see name
-func (s *MarkovChainCmd) ResetHistory() error {
- s.Init()
- return nil
-}
diff --git a/pkg/strat/markov-chain.go b/pkg/strat/markov-chain.go
deleted file mode 100644
index 50c7fdce..00000000
--- a/pkg/strat/markov-chain.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package strat
-
-import (
- "sort"
- "strconv"
-
- "github.com/curusarn/resh/pkg/records"
- "github.com/mb-14/gomarkov"
-)
-
-// MarkovChain prediction/recommendation strategy
-type MarkovChain struct {
- Order int
- history []string
-}
-
-type strMarkEntry struct {
- cmdLine string
- transProb float64
-}
-
-// Init see name
-func (s *MarkovChain) Init() {
- s.history = nil
-}
-
-// GetTitleAndDescription see name
-func (s *MarkovChain) GetTitleAndDescription() (string, string) {
- return "markov chain (order " + strconv.Itoa(s.Order) + ")", "Use markov chain to recommend commands"
-}
-
-// GetCandidates see name
-func (s *MarkovChain) GetCandidates() []string {
- if len(s.history) < s.Order {
- return s.history
- }
- chain := gomarkov.NewChain(s.Order)
-
- chain.Add(s.history)
-
- cmdLinesSet := map[string]bool{}
- var entries []strMarkEntry
- for _, cmdLine := range s.history {
- if cmdLinesSet[cmdLine] {
- continue
- }
- cmdLinesSet[cmdLine] = true
- prob, _ := chain.TransitionProbability(cmdLine, s.history[len(s.history)-s.Order:])
- entries = append(entries, strMarkEntry{cmdLine: cmdLine, transProb: prob})
- }
- sort.Slice(entries, func(i int, j int) bool { return entries[i].transProb > entries[j].transProb })
- var hist []string
- for _, item := range entries {
- hist = append(hist, item.cmdLine)
- }
- // log.Println("################")
- // log.Println(s.history[len(s.history)-s.order:])
- // log.Println(" -> ")
- // x := math.Min(float64(len(hist)), 5)
- // log.Println(hist[:int(x)])
- // log.Println("################")
- return hist
-}
-
-// AddHistoryRecord see name
-func (s *MarkovChain) AddHistoryRecord(record *records.EnrichedRecord) error {
- s.history = append(s.history, record.CmdLine)
- // s.historySet[record.CmdLine] = true
- return nil
-}
-
-// ResetHistory see name
-func (s *MarkovChain) ResetHistory() error {
- s.Init()
- return nil
-}
diff --git a/pkg/strat/random.go b/pkg/strat/random.go
deleted file mode 100644
index 0ff52f1a..00000000
--- a/pkg/strat/random.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package strat
-
-import (
- "math/rand"
- "time"
-
- "github.com/curusarn/resh/pkg/records"
-)
-
-// Random prediction/recommendation strategy
-type Random struct {
- CandidatesSize int
- history []string
- historySet map[string]bool
-}
-
-// Init see name
-func (s *Random) Init() {
- s.history = nil
- s.historySet = map[string]bool{}
-}
-
-// GetTitleAndDescription see name
-func (s *Random) GetTitleAndDescription() (string, string) {
- return "random", "Use random commands"
-}
-
-// GetCandidates see name
-func (s *Random) GetCandidates() []string {
- seed := time.Now().UnixNano()
- rand.Seed(seed)
- var candidates []string
- candidateSet := map[string]bool{}
- for len(candidates) < s.CandidatesSize && len(candidates)*2 < len(s.historySet) {
- x := rand.Intn(len(s.history))
- candidate := s.history[x]
- if candidateSet[candidate] == false {
- candidateSet[candidate] = true
- candidates = append(candidates, candidate)
- continue
- }
- }
- return candidates
-}
-
-// AddHistoryRecord see name
-func (s *Random) AddHistoryRecord(record *records.EnrichedRecord) error {
- s.history = append([]string{record.CmdLine}, s.history...)
- s.historySet[record.CmdLine] = true
- return nil
-}
-
-// ResetHistory see name
-func (s *Random) ResetHistory() error {
- s.Init()
- return nil
-}
diff --git a/pkg/strat/recent-bash.go b/pkg/strat/recent-bash.go
deleted file mode 100644
index ace35711..00000000
--- a/pkg/strat/recent-bash.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package strat
-
-import "github.com/curusarn/resh/pkg/records"
-
-// RecentBash prediction/recommendation strategy
-type RecentBash struct {
- histfile []string
- histfileSnapshot map[string][]string
- history map[string][]string
-}
-
-// Init see name
-func (s *RecentBash) Init() {
- s.histfileSnapshot = map[string][]string{}
- s.history = map[string][]string{}
-}
-
-// GetTitleAndDescription see name
-func (s *RecentBash) GetTitleAndDescription() (string, string) {
- return "recent (bash-like)", "Behave like bash"
-}
-
-// GetCandidates see name
-func (s *RecentBash) GetCandidates(strippedRecord records.EnrichedRecord) []string {
- // populate the local history from histfile
- if s.histfileSnapshot[strippedRecord.SessionID] == nil {
- s.histfileSnapshot[strippedRecord.SessionID] = s.histfile
- }
- return append(s.history[strippedRecord.SessionID], s.histfileSnapshot[strippedRecord.SessionID]...)
-}
-
-// AddHistoryRecord see name
-func (s *RecentBash) AddHistoryRecord(record *records.EnrichedRecord) error {
- // remove previous occurance of record
- for i, cmd := range s.history[record.SessionID] {
- if cmd == record.CmdLine {
- s.history[record.SessionID] = append(s.history[record.SessionID][:i], s.history[record.SessionID][i+1:]...)
- }
- }
- // append new record
- s.history[record.SessionID] = append([]string{record.CmdLine}, s.history[record.SessionID]...)
-
- if record.LastRecordOfSession {
- // append history of the session to histfile and clear session history
- s.histfile = append(s.history[record.SessionID], s.histfile...)
- s.histfileSnapshot[record.SessionID] = nil
- s.history[record.SessionID] = nil
- }
- return nil
-}
-
-// ResetHistory see name
-func (s *RecentBash) ResetHistory() error {
- s.Init()
- return nil
-}
diff --git a/pkg/strat/recent.go b/pkg/strat/recent.go
deleted file mode 100644
index 157b52c8..00000000
--- a/pkg/strat/recent.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package strat
-
-import "github.com/curusarn/resh/pkg/records"
-
-// Recent prediction/recommendation strategy
-type Recent struct {
- history []string
-}
-
-// GetTitleAndDescription see name
-func (s *Recent) GetTitleAndDescription() (string, string) {
- return "recent", "Use recent commands"
-}
-
-// GetCandidates see name
-func (s *Recent) GetCandidates() []string {
- return s.history
-}
-
-// AddHistoryRecord see name
-func (s *Recent) AddHistoryRecord(record *records.EnrichedRecord) error {
- // remove previous occurance of record
- for i, cmd := range s.history {
- if cmd == record.CmdLine {
- s.history = append(s.history[:i], s.history[i+1:]...)
- }
- }
- // append new record
- s.history = append([]string{record.CmdLine}, s.history...)
- return nil
-}
-
-// ResetHistory see name
-func (s *Recent) ResetHistory() error {
- s.history = nil
- return nil
-}
diff --git a/pkg/strat/record-distance.go b/pkg/strat/record-distance.go
deleted file mode 100644
index e582584c..00000000
--- a/pkg/strat/record-distance.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package strat
-
-import (
- "sort"
- "strconv"
-
- "github.com/curusarn/resh/pkg/records"
-)
-
-// RecordDistance prediction/recommendation strategy
-type RecordDistance struct {
- history []records.EnrichedRecord
- DistParams records.DistParams
- MaxDepth int
- Label string
-}
-
-type strDistEntry struct {
- cmdLine string
- distance float64
-}
-
-// Init see name
-func (s *RecordDistance) Init() {
- s.history = nil
-}
-
-// GetTitleAndDescription see name
-func (s *RecordDistance) GetTitleAndDescription() (string, string) {
- return "record distance (depth:" + strconv.Itoa(s.MaxDepth) + ";" + s.Label + ")", "Use record distance to recommend commands"
-}
-
-// GetCandidates see name
-func (s *RecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string {
- if len(s.history) == 0 {
- return nil
- }
- var mapItems []strDistEntry
- for i, record := range s.history {
- if s.MaxDepth != 0 && i > s.MaxDepth {
- break
- }
- distance := record.DistanceTo(strippedRecord, s.DistParams)
- mapItems = append(mapItems, strDistEntry{record.CmdLine, distance})
- }
- sort.SliceStable(mapItems, func(i int, j int) bool { return mapItems[i].distance < mapItems[j].distance })
- var hist []string
- histSet := map[string]bool{}
- for _, item := range mapItems {
- if histSet[item.cmdLine] {
- continue
- }
- histSet[item.cmdLine] = true
- hist = append(hist, item.cmdLine)
- }
- return hist
-}
-
-// AddHistoryRecord see name
-func (s *RecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error {
- // append record to front
- s.history = append([]records.EnrichedRecord{*record}, s.history...)
- return nil
-}
-
-// ResetHistory see name
-func (s *RecordDistance) ResetHistory() error {
- s.Init()
- return nil
-}
diff --git a/pkg/strat/strat.go b/pkg/strat/strat.go
deleted file mode 100644
index 28ac0151..00000000
--- a/pkg/strat/strat.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package strat
-
-import (
- "github.com/curusarn/resh/pkg/records"
-)
-
-// ISimpleStrategy interface
-type ISimpleStrategy interface {
- GetTitleAndDescription() (string, string)
- GetCandidates() []string
- AddHistoryRecord(record *records.EnrichedRecord) error
- ResetHistory() error
-}
-
-// IStrategy interface
-type IStrategy interface {
- GetTitleAndDescription() (string, string)
- GetCandidates(r records.EnrichedRecord) []string
- AddHistoryRecord(record *records.EnrichedRecord) error
- ResetHistory() error
-}
-
-type simpleStrategyWrapper struct {
- strategy ISimpleStrategy
-}
-
-// NewSimpleStrategyWrapper returns IStrategy created by wrapping given ISimpleStrategy
-func NewSimpleStrategyWrapper(strategy ISimpleStrategy) *simpleStrategyWrapper {
- return &simpleStrategyWrapper{strategy: strategy}
-}
-
-func (s *simpleStrategyWrapper) GetTitleAndDescription() (string, string) {
- return s.strategy.GetTitleAndDescription()
-}
-
-func (s *simpleStrategyWrapper) GetCandidates(r records.EnrichedRecord) []string {
- return s.strategy.GetCandidates()
-}
-
-func (s *simpleStrategyWrapper) AddHistoryRecord(r *records.EnrichedRecord) error {
- return s.strategy.AddHistoryRecord(r)
-}
-
-func (s *simpleStrategyWrapper) ResetHistory() error {
- return s.strategy.ResetHistory()
-}
diff --git a/record/legacy.go b/record/legacy.go
new file mode 100644
index 00000000..3b913fb2
--- /dev/null
+++ b/record/legacy.go
@@ -0,0 +1,88 @@
+package record
+
+type Legacy struct {
+ // core
+ CmdLine string `json:"cmdLine"`
+ ExitCode int `json:"exitCode"`
+ Shell string `json:"shell"`
+ Uname string `json:"uname"`
+ SessionID string `json:"sessionId"`
+ RecordID string `json:"recordId"`
+
+ // posix
+ Home string `json:"home"`
+ Lang string `json:"lang"`
+ LcAll string `json:"lcAll"`
+ Login string `json:"login"`
+ Pwd string `json:"pwd"`
+ PwdAfter string `json:"pwdAfter"`
+ ShellEnv string `json:"shellEnv"`
+ Term string `json:"term"`
+
+ // non-posix"`
+ RealPwd string `json:"realPwd"`
+ RealPwdAfter string `json:"realPwdAfter"`
+ Pid int `json:"pid"`
+ SessionPID int `json:"sessionPid"`
+ Host string `json:"host"`
+ Hosttype string `json:"hosttype"`
+ Ostype string `json:"ostype"`
+ Machtype string `json:"machtype"`
+ Shlvl int `json:"shlvl"`
+
+ // before after
+ TimezoneBefore string `json:"timezoneBefore"`
+ TimezoneAfter string `json:"timezoneAfter"`
+
+ RealtimeBefore float64 `json:"realtimeBefore"`
+ RealtimeAfter float64 `json:"realtimeAfter"`
+ RealtimeBeforeLocal float64 `json:"realtimeBeforeLocal"`
+ RealtimeAfterLocal float64 `json:"realtimeAfterLocal"`
+
+ RealtimeDuration float64 `json:"realtimeDuration"`
+ RealtimeSinceSessionStart float64 `json:"realtimeSinceSessionStart"`
+ RealtimeSinceBoot float64 `json:"realtimeSinceBoot"`
+
+ GitDir string `json:"gitDir"`
+ GitRealDir string `json:"gitRealDir"`
+ GitOriginRemote string `json:"gitOriginRemote"`
+ GitDirAfter string `json:"gitDirAfter"`
+ GitRealDirAfter string `json:"gitRealDirAfter"`
+ GitOriginRemoteAfter string `json:"gitOriginRemoteAfter"`
+ MachineID string `json:"machineId"`
+
+ OsReleaseID string `json:"osReleaseId"`
+ OsReleaseVersionID string `json:"osReleaseVersionId"`
+ OsReleaseIDLike string `json:"osReleaseIdLike"`
+ OsReleaseName string `json:"osReleaseName"`
+ OsReleasePrettyName string `json:"osReleasePrettyName"`
+
+ ReshUUID string `json:"reshUuid"`
+ ReshVersion string `json:"reshVersion"`
+ ReshRevision string `json:"reshRevision"`
+
+ // records come in two parts (collect and postcollect)
+ PartOne bool `json:"partOne,omitempty"` // false => part two
+ PartsMerged bool `json:"partsMerged"`
+ // special flag -> not an actual record but an session end
+ SessionExit bool `json:"sessionExit,omitempty"`
+
+ // recall metadata
+ Recalled bool `json:"recalled"`
+ RecallHistno int `json:"recallHistno,omitempty"`
+ RecallStrategy string `json:"recallStrategy,omitempty"`
+ RecallActionsRaw string `json:"recallActionsRaw,omitempty"`
+ RecallActions []string `json:"recallActions,omitempty"`
+ RecallLastCmdLine string `json:"recallLastCmdLine"`
+
+ // recall command
+ RecallPrefix string `json:"recallPrefix,omitempty"`
+
+ // added by sanitizatizer
+ Sanitized bool `json:"sanitized,omitempty"`
+ CmdLength int `json:"cmdLength,omitempty"`
+
+ // fields that are string here and int in older resh verisons
+ Cols interface{} `json:"cols"`
+ Lines interface{} `json:"lines"`
+}
diff --git a/record/record.go b/record/record.go
new file mode 100644
index 00000000..07ad798d
--- /dev/null
+++ b/record/record.go
@@ -0,0 +1,2 @@
+// Package record provides record types that are used in resh history files
+package record
diff --git a/record/v1.go b/record/v1.go
new file mode 100644
index 00000000..5b512985
--- /dev/null
+++ b/record/v1.go
@@ -0,0 +1,65 @@
+package record
+
+type V1 struct {
+ // future-proofing so that we can add this later without version bump
+ // deleted, favorite
+ Deleted bool `json:"deleted,omitempty"`
+ Favorite bool `json:"favorite,omitempty"`
+
+ // cmdline, exitcode
+ CmdLine string `json:"cmdLine"`
+ ExitCode int `json:"exitCode"`
+
+ DeviceID string `json:"deviceID"`
+ SessionID string `json:"sessionID"`
+ // can we have a shorter uuid for record
+ RecordID string `json:"recordID"`
+
+ // paths
+ // TODO: Do we need both pwd and real pwd?
+ Home string `json:"home"`
+ Pwd string `json:"pwd"`
+ RealPwd string `json:"realPwd"`
+
+ // Device is set during installation/setup
+ // It is stored in RESH configuration
+ Device string `json:"device"`
+
+ // git info
+ // origin is the most important
+ GitOriginRemote string `json:"gitOriginRemote"`
+ // TODO: add GitBranch (v2 ?)
+ // maybe branch could be useful - e.g. in monorepo ??
+ // GitBranch string `json:"gitBranch"`
+
+ // what is this for ??
+ // session watching needs this
+ // but I'm not sure if we need to save it
+ // records belong to sessions
+ // PID int `json:"pid"`
+ // needed for tracking of sessions but I think it shouldn't be part of V1
+ // SessionPID int `json:"sessionPID"`
+
+ // needed to because records are merged with parts with same "SessionID + Shlvl"
+ // I don't think we need to save it
+ // Shlvl int `json:"shlvl"`
+
+ // time (before), duration of command
+ // time and duration are strings because we don't want unnecessary precision when they get serialized into json
+ // we could implement custom (un)marshalling but I don't see downsides of directly representing the values as strings
+ Time string `json:"time"`
+ Duration string `json:"duration"`
+
+ // these look like internal stuff
+
+ // TODO: rethink
+ // I don't really like this :/
+ // Maybe just one field 'NotMerged' with 'partOne' and 'partTwo' as values and empty string for merged
+ // records come in two parts (collect and postcollect)
+ PartOne bool `json:"partOne,omitempty"` // false => part two
+ PartsNotMerged bool `json:"partsNotMerged,omitempty"`
+
+ // special flag -> not an actual record but an session end
+ // TODO: this shouldn't be part of serializable V1 record
+ SessionExit bool `json:"sessionExit,omitempty"`
+}
diff --git a/roadmap.md b/roadmap.md
deleted file mode 100644
index efcc2d51..00000000
--- a/roadmap.md
+++ /dev/null
@@ -1,45 +0,0 @@
-
-# RESH Roadmap
-
-| | Legend |
-| --- | --- |
-| :heavy_check_mark: | Implemented |
-| :white_check_mark: | Implemented but I'm not happy with it |
-| :x: | Not implemented |
-
-*NOTE: Features can change in the future*
-
-- :heavy_check_mark: Record shell history with metadata
- - :heavy_check_mark: save it as JSON to `~/.resh_history.json`
-
-- :white_check_mark: Provide an app to search the history
- - :heavy_check_mark: launch with CTRL+R (enable it using `reshctl enable ctrl_r_binding_global`)
- - :heavy_check_mark: search by keywords
- - :heavy_check_mark: relevant results show up first based on context (host, directory, git, exit status)
- - :heavy_check_mark: allow searching completely without context ("raw" mode)
- - :heavy_check_mark: import and search history from before RESH was installed
- - :white_check_mark: include a help with keybindings
- - :x: allow listing details for individual commands
- - :x: allow explicitly searching by metadata
-
-- :heavy_check_mark: Provide a `reshctl` utility to control and interact with the project
- - :heavy_check_mark: turn on/off resh key bindings
- - :heavy_check_mark: zsh completion
- - :heavy_check_mark: bash completion
-
-- :x: Multi-device history
- - :x: Synchronize recorded history between devices
- - :x: Allow proxying history when ssh'ing into remote servers
-
-- :x: Provide a stable API to make resh extensible
-
-- :heavy_check_mark: Support zsh and bash
-
-- :heavy_check_mark: Support Linux and macOS
-
-- :white_check_mark: Require only essential prerequisite software
- - :heavy_check_mark: Linux
- - :white_check_mark: MacOS *(requires coreutils - `brew install coreutils`)*
-
-- :heavy_check_mark: Provide a tool to sanitize the recorded history
-
diff --git a/scripts/hooks.sh b/scripts/hooks.sh
index 02e74c34..c71468ff 100644
--- a/scripts/hooks.sh
+++ b/scripts/hooks.sh
@@ -1,179 +1,218 @@
+#!/hint/sh
-__resh_reset_variables() {
- __RESH_HISTNO=0
- __RESH_HISTNO_MAX=""
- __RESH_HISTNO_ZERO_LINE=""
- __RESH_HIST_PREV_LINE=""
- __RESH_HIST_PREV_CURSOR="" # deprecated
- __RESH_HIST_PREV_PREFIX=""
- __RESH_HIST_RECALL_ACTIONS=""
- __RESH_HIST_NO_PREFIX_MODE=0
- __RESH_HIST_RECALL_STRATEGY=""
- __RESH_RECORD_ID=$(__resh_get_uuid)
+# BACKWARDS COMPATIBILITY NOTES:
+#
+# Stable names and options:
+# * `resh-collect -version` / `resh-postcollect -version` is used to detect version mismatch.
+# => The go-like/short `-version` option needs to exist for new resh-(post)collect commands in all future version.
+# => Prefer using go-like/short `-version` option so that we don't have more options to support indefinitely.
+# * `__resh_preexec ` with `__RESH_NO_RELOAD=1` is called on version mismatch.
+# => The `__resh_preexec` function needs to exist in all future versions.
+# => Make sure that `__RESH_NO_RELOAD` behavior is not broken in any future version.
+# => Prefer only testing `__RESH_NO_RELOAD` for emptyness instead of specific value
+# * `__resh_reload_msg` is called *after* shell files reload
+# => The function shows a message from the already updated shell files
+# => We can drop this function at any time - the old version will be used
+
+# Backwards compatibilty: Please see notes above before making any changes here.
+__resh_reload_msg() {
+ printf '\n'
+ printf '+--------------------------------------------------------------+\n'
+ printf '| New version of RESH shell files was loaded in this terminal. |\n'
+ printf '| This is an informative message - no action is necessary. |\n'
+ printf '| Please restart this terminal if you encounter any issues. |\n'
+ printf '+--------------------------------------------------------------+\n'
+ printf '\n'
}
+# (pre)collect
+# Backwards compatibilty: Please see notes above before making any changes here.
__resh_preexec() {
- # core
+ if [ "$(resh-collect -version)" != "$__RESH_VERSION" ] && [ -z "${__RESH_NO_RELOAD-}" ]; then
+ # Reload shell files and restart __resh_preexec - i.e. the full command will be recorded only with a slight delay.
+ # This should happens in every already open terminal after resh update.
+ # __RESH_NO_RELOAD prevents recursive reloads
+ # We never repeatadly reload the shell files to prevent potentially infinite recursion.
+ # If the version is still wrong then error will be raised by `resh-collect -requiredVersion`.
+
+ source ~/.resh/shellrc
+ # Show reload message from the updated shell files
+ __resh_reload_msg
+ # Rerun self but prevent another reload. Extra protection against infinite recursion.
+ __RESH_NO_RELOAD=1 __resh_preexec "$@"
+ return $?
+ fi
__RESH_COLLECT=1
- __RESH_CMDLINE="$1" # not local to preserve it for postcollect (useful as sanity check)
- local fpath_last_run="$__RESH_XDG_CACHE_HOME/collect_last_run_out.txt"
- __resh_collect --cmdLine "$__RESH_CMDLINE" \
- --recall-actions "$__RESH_HIST_RECALL_ACTIONS" \
- --recall-strategy "$__RESH_HIST_RECALL_STRATEGY" \
- --recall-last-cmdline "$__RESH_HIST_PREV_LINE" \
- >| "$fpath_last_run" 2>&1 || echo "resh-collect ERROR: $(head -n 1 $fpath_last_run)"
+ __RESH_RECORD_ID=$(resh-generate-uuid)
+ # TODO: do this in resh-collect
+ # shellcheck disable=2155
+ local git_remote="$(git remote get-url origin 2>/dev/null)"
+ # TODO: do this in resh-collect
+ __RESH_RT_BEFORE=$(resh-get-epochtime)
+ resh-collect -requireVersion "$__RESH_VERSION" \
+ --git-remote "$git_remote" \
+ --home "$HOME" \
+ --pwd "$PWD" \
+ --record-id "$__RESH_RECORD_ID" \
+ --session-id "$__RESH_SESSION_ID" \
+ --session-pid "$$" \
+ --shell "$__RESH_SHELL" \
+ --shlvl "$SHLVL" \
+ --time "$__RESH_RT_BEFORE" \
+ --cmd-line "$1"
+ return $?
}
-# used for collect and collect --recall
-__resh_collect() {
- # posix
- local __RESH_COLS="$COLUMNS"
- local __RESH_LANG="$LANG"
- local __RESH_LC_ALL="$LC_ALL"
- # other LC ?
- local __RESH_LINES="$LINES"
- # __RESH_PATH="$PATH"
- local __RESH_PWD="$PWD"
-
- # non-posix
- local __RESH_SHLVL="$SHLVL"
- local __RESH_GIT_CDUP; __RESH_GIT_CDUP="$(git rev-parse --show-cdup 2>/dev/null)"
- local __RESH_GIT_CDUP_EXIT_CODE=$?
- local __RESH_GIT_REMOTE; __RESH_GIT_REMOTE="$(git remote get-url origin 2>/dev/null)"
- local __RESH_GIT_REMOTE_EXIT_CODE=$?
- #__RESH_GIT_TOPLEVEL="$(git rev-parse --show-toplevel)"
- #__RESH_GIT_TOPLEVEL_EXIT_CODE=$?
-
- if [ -n "${ZSH_VERSION-}" ]; then
- # assume Zsh
- local __RESH_PID="$$" # current pid
- elif [ -n "${BASH_VERSION-}" ]; then
- # assume Bash
- if [ "${BASH_VERSINFO[0]}" -ge "4" ]; then
- # $BASHPID is only available in bash4+
- # $$ is fairly similar so it should not be an issue
- local __RESH_PID="$BASHPID" # current pid
- else
- local __RESH_PID="$$" # current pid
- fi
+# postcollect
+# Backwards compatibilty: Please see notes above before making any changes here.
+__resh_precmd() {
+ # Get status first before it gets overriden by another command.
+ local exit_code=$?
+ # Don't do anything if __resh_preexec was not called.
+ # There are situations (in bash) where no command was submitted but __resh_precmd gets called anyway.
+ [ -n "${__RESH_COLLECT-}" ] || return
+ if [ "$(resh-postcollect -version)" != "$__RESH_VERSION" ]; then
+ # Reload shell files and return - i.e. skip recording part2 for this command.
+ # We don't call __resh_precmd because the new __resh_preexec might not be backwards compatible with variables set by old __resh_preexec.
+ # This should happen only in the one terminal where resh update was executed.
+ # And the resh-daemon was likely restarted so we likely don't even have the matching part1 of the comand in the resh-daemon memory.
+ source ~/.resh/shellrc
+ # Show reload message from the updated shell files
+ __resh_reload_msg
+ return
fi
- # time
- local __RESH_TZ_BEFORE; __RESH_TZ_BEFORE=$(date +%z)
- # __RESH_RT_BEFORE="$EPOCHREALTIME"
- __RESH_RT_BEFORE=$(__resh_get_epochrealtime)
-
- if [ "$__RESH_VERSION" != "$(resh-collect -version)" ]; then
- # shellcheck source=shellrc.sh
- source ~/.resh/shellrc
- if [ "$__RESH_VERSION" != "$(resh-collect -version)" ]; then
- echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh version: $(resh-collect -version); resh version of this terminal session: ${__RESH_VERSION})"
- else
- echo "RESH INFO: New RESH shellrc script was loaded - if you encounter any issues please restart this terminal session."
+ unset __RESH_COLLECT
+
+ # do this in resh-postcollect
+ # shellcheck disable=2155
+ local rt_after=$(resh-get-epochtime)
+ resh-postcollect -requireVersion "$__RESH_VERSION" \
+ --exit-code "$exit_code" \
+ --record-id "$__RESH_RECORD_ID" \
+ --session-id "$__RESH_SESSION_ID" \
+ --shlvl "$SHLVL" \
+ --time-after "$rt_after" \
+ --time-before "$__RESH_RT_BEFORE"
+ return $?
+}
+
+# Backwards compatibilty: No restrictions. This is only used at the start of the session.
+__resh_session_init() {
+ resh-session-init -requireVersion "$__RESH_VERSION" \
+ --session-id "$__RESH_SESSION_ID" \
+ --session-pid "$$"
+ return $?
+}
+
+# Backwards compatibilty: Please see notes above before making any changes here.
+__resh_widget_control_R() {
+ # This is a very bad workaround.
+ # Force bash-preexec to run repeatedly because otherwise premature run of bash-preexec overshadows the next proper run.
+ # I honestly think that it's impossible to make widgets work in bash without hacks like this.
+ # shellcheck disable=2034
+ __bp_preexec_interactive_mode="on"
+
+ local PREVBUFFER=$BUFFER
+
+ local status_code
+ local git_remote; git_remote="$(git remote get-url origin 2>/dev/null)"
+ if [ "$(resh-cli -version)" != "$__RESH_VERSION" ] && [ -z "${__RESH_NO_RELOAD-}" ]; then
+ source ~/.resh/shellrc
+ # Show reload message from the updated shell files
+ __resh_reload_msg
+ # Rerun self but prevent another reload. Extra protection against infinite recursion.
+ __RESH_NO_RELOAD=1 __resh_widget_control_R "$@"
+ return $?
+ fi
+ BUFFER=$(resh-cli -requireVersion "$__RESH_VERSION" \
+ --git-remote "$git_remote" \
+ --pwd "$PWD" \
+ --query "$BUFFER" \
+ --session-id "$__RESH_SESSION_ID" \
+ )
+ status_code=$?
+ if [ $status_code = 111 ]; then
+ # execute
+ if [ -n "${ZSH_VERSION-}" ]; then
+ # zsh
+ zle accept-line
+ elif [ -n "${BASH_VERSION-}" ]; then
+ # bash
+ # set chained keyseq to accept-line
+ bind '"\u[32~": accept-line'
fi
- elif [ "$__RESH_REVISION" != "$(resh-collect -revision)" ]; then
- # shellcheck source=shellrc.sh
- source ~/.resh/shellrc
- if [ "$__RESH_REVISION" != "$(resh-collect -revision)" ]; then
- echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh revision: $(resh-collect -revision); resh revision of this terminal session: ${__RESH_REVISION})"
+ elif [ $status_code = 0 ]; then
+ if [ -n "${BASH_VERSION-}" ]; then
+ # bash
+ # set chained keyseq to nothing
+ bind -x '"\u[32~": __resh_nop'
fi
+ else
+ echo "RESH SEARCH APP failed"
+ printf "%s" "$buffer" >&2
+ BUFFER="$PREVBUFFER"
fi
- if [ "$__RESH_VERSION" = "$(resh-collect -version)" ] && [ "$__RESH_REVISION" = "$(resh-collect -revision)" ]; then
- resh-collect -requireVersion "$__RESH_VERSION" \
- -requireRevision "$__RESH_REVISION" \
- -shell "$__RESH_SHELL" \
- -uname "$__RESH_UNAME" \
- -sessionId "$__RESH_SESSION_ID" \
- -recordId "$__RESH_RECORD_ID" \
- -cols "$__RESH_COLS" \
- -home "$__RESH_HOME" \
- -lang "$__RESH_LANG" \
- -lcAll "$__RESH_LC_ALL" \
- -lines "$__RESH_LINES" \
- -login "$__RESH_LOGIN" \
- -pwd "$__RESH_PWD" \
- -shellEnv "$__RESH_SHELL_ENV" \
- -term "$__RESH_TERM" \
- -pid "$__RESH_PID" \
- -sessionPid "$__RESH_SESSION_PID" \
- -host "$__RESH_HOST" \
- -hosttype "$__RESH_HOSTTYPE" \
- -ostype "$__RESH_OSTYPE" \
- -machtype "$__RESH_MACHTYPE" \
- -shlvl "$__RESH_SHLVL" \
- -gitCdup "$__RESH_GIT_CDUP" \
- -gitCdupExitCode "$__RESH_GIT_CDUP_EXIT_CODE" \
- -gitRemote "$__RESH_GIT_REMOTE" \
- -gitRemoteExitCode "$__RESH_GIT_REMOTE_EXIT_CODE" \
- -realtimeBefore "$__RESH_RT_BEFORE" \
- -realtimeSession "$__RESH_RT_SESSION" \
- -realtimeSessSinceBoot "$__RESH_RT_SESS_SINCE_BOOT" \
- -timezoneBefore "$__RESH_TZ_BEFORE" \
- -osReleaseId "$__RESH_OS_RELEASE_ID" \
- -osReleaseVersionId "$__RESH_OS_RELEASE_VERSION_ID" \
- -osReleaseIdLike "$__RESH_OS_RELEASE_ID_LIKE" \
- -osReleaseName "$__RESH_OS_RELEASE_NAME" \
- -osReleasePrettyName "$__RESH_OS_RELEASE_PRETTY_NAME" \
- -histno "$__RESH_HISTNO" \
- "$@"
- return $?
- fi
- return 1
+ CURSOR=${#BUFFER}
}
-__resh_precmd() {
- local __RESH_EXIT_CODE=$?
- local __RESH_RT_AFTER
- local __RESH_TZ_AFTER
- local __RESH_PWD_AFTER
- local __RESH_GIT_CDUP_AFTER
- local __RESH_GIT_CDUP_EXIT_CODE_AFTER
- local __RESH_GIT_REMOTE_AFTER
- local __RESH_GIT_REMOTE_EXIT_CODE_AFTER
- local __RESH_SHLVL="$SHLVL"
- __RESH_RT_AFTER=$(__resh_get_epochrealtime)
- __RESH_TZ_AFTER=$(date +%z)
- __RESH_PWD_AFTER="$PWD"
- __RESH_GIT_CDUP_AFTER="$(git rev-parse --show-cdup 2>/dev/null)"
- __RESH_GIT_CDUP_EXIT_CODE_AFTER=$?
- __RESH_GIT_REMOTE_AFTER="$(git remote get-url origin 2>/dev/null)"
- __RESH_GIT_REMOTE_EXIT_CODE_AFTER=$?
- if [ -n "${__RESH_COLLECT}" ]; then
- if [ "$__RESH_VERSION" != "$(resh-postcollect -version)" ]; then
- # shellcheck source=shellrc.sh
- source ~/.resh/shellrc
- if [ "$__RESH_VERSION" != "$(resh-postcollect -version)" ]; then
- echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh version: $(resh-collect -version); resh version of this terminal session: ${__RESH_VERSION})"
- else
- echo "RESH INFO: New RESH shellrc script was loaded - if you encounter any issues please restart this terminal session."
- fi
- elif [ "$__RESH_REVISION" != "$(resh-postcollect -revision)" ]; then
- # shellcheck source=shellrc.sh
- source ~/.resh/shellrc
- if [ "$__RESH_REVISION" != "$(resh-postcollect -revision)" ]; then
- echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh revision: $(resh-collect -revision); resh revision of this terminal session: ${__RESH_REVISION})"
- fi
- fi
- if [ "$__RESH_VERSION" = "$(resh-postcollect -version)" ] && [ "$__RESH_REVISION" = "$(resh-postcollect -revision)" ]; then
- local fpath_last_run="$__RESH_XDG_CACHE_HOME/postcollect_last_run_out.txt"
- resh-postcollect -requireVersion "$__RESH_VERSION" \
- -requireRevision "$__RESH_REVISION" \
- -cmdLine "$__RESH_CMDLINE" \
- -realtimeBefore "$__RESH_RT_BEFORE" \
- -exitCode "$__RESH_EXIT_CODE" \
- -sessionId "$__RESH_SESSION_ID" \
- -recordId "$__RESH_RECORD_ID" \
- -shell "$__RESH_SHELL" \
- -shlvl "$__RESH_SHLVL" \
- -pwdAfter "$__RESH_PWD_AFTER" \
- -gitCdupAfter "$__RESH_GIT_CDUP_AFTER" \
- -gitCdupExitCodeAfter "$__RESH_GIT_CDUP_EXIT_CODE_AFTER" \
- -gitRemoteAfter "$__RESH_GIT_REMOTE_AFTER" \
- -gitRemoteExitCodeAfter "$__RESH_GIT_REMOTE_EXIT_CODE_AFTER" \
- -realtimeAfter "$__RESH_RT_AFTER" \
- -timezoneAfter "$__RESH_TZ_AFTER" \
- >| "$fpath_last_run" 2>&1 || echo "resh-postcollect ERROR: $(head -n 1 $fpath_last_run)"
- fi
- __resh_reset_variables
+# Wrapper for resh-cli for calling resh directly
+resh() {
+ if [ "$(resh-cli -version)" != "$__RESH_VERSION" ] && [ -z "${__RESH_NO_RELOAD-}" ]; then
+ source ~/.resh/shellrc
+ # Show reload message from the updated shell files
+ __resh_reload_msg
+ # Rerun self but prevent another reload. Extra protection against infinite recursion.
+ __RESH_NO_RELOAD=1 resh "$@"
+ return $?
fi
- unset __RESH_COLLECT
+ local buffer
+ local git_remote; git_remote="$(git remote get-url origin 2>/dev/null)"
+ buffer=$(resh-cli -requireVersion "$__RESH_VERSION" \
+ --git-origin-remote "$git_remote" \
+ --pwd "$PWD" \
+ --session-id "$__RESH_SESSION_ID" \
+ "$@"
+ )
+ status_code=$?
+ if [ $status_code = 111 ]; then
+ # execute
+ echo "$buffer"
+ eval "$buffer"
+ elif [ $status_code = 0 ]; then
+ # paste
+ echo "$buffer"
+ elif [ $status_code = 130 ]; then
+ true
+ else
+ printf "%s" "$buffer" >&2
+ fi
+}
+
+__resh_widget_control_R_compat() {
+ __bindfunc_compat_wrapper __resh_widget_control_R
+}
+
+__resh_nop() {
+ # does nothing
+ true
+}
+
+# shellcheck source=../submodules/bash-zsh-compat-widgets/bindfunc.sh
+. ~/.resh/bindfunc.sh
+
+__resh_bind_control_R() {
+ bindfunc '\C-r' __resh_widget_control_R_compat
+ if [ -n "${BASH_VERSION-}" ]; then
+ # fuck bash
+ bind '"\C-r": "\u[31~\u[32~"'
+ bind -x '"\u[31~": __resh_widget_control_R_compat'
+
+ # execute
+ # bind '"\u[32~": accept-line'
+
+ # just paste
+ # bind -x '"\u[32~": __resh_nop'
+ true
+ fi
+ return 0
}
diff --git a/scripts/install.sh b/scripts/install.sh
index 1b22d156..8e7059e0 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -1,264 +1,196 @@
-#!/usr/bin/env bash
+#!/usr/bin/env sh
set -euo pipefail
+# TODO: There is a lot of hardcoded stuff here (paths mostly)
+# TODO: Split this into installation and setup because we want to suport package manager installation eventually
+# TODO: "installation" should stay here and be simple, "setup" should be moved behind "reshctl setup"
+
echo
echo "Checking your system ..."
+printf '\e[31;1m' # red color on
+
+cleanup() {
+ printf '\e[0m' # reset
+ exit
+}
+trap cleanup EXIT INT TERM
# /usr/bin/zsh -> zsh
login_shell=$(echo "$SHELL" | rev | cut -d'/' -f1 | rev)
if [ "$login_shell" != bash ] && [ "$login_shell" != zsh ]; then
- echo "ERROR: Unsupported/unknown login shell: $login_shell"
- exit 1
+ echo "* UNSUPPORTED login shell: $login_shell"
+ echo " -> RESH supports zsh and bash"
+ echo
+ if [ -z "${RESH_INSTALL_IGNORE_LOGIN_SHELL-}" ]; then
+ echo 'EXITING!'
+ echo ' -> You can skip this check with `export RESH_INSTALL_IGNORE_LOGIN_SHELL=1`'
+ exit 1
+ fi
fi
-echo " * Login shell: $login_shell - OK"
+# TODO: Explicitly ask users if they want to enable RESH in shells
+# Only offer shells with supported versions
+# E.g. Enable RESH in: Zsh (your login shell), Bash, Both shells
-# check like we are not running bash
bash_version=$(bash -c 'echo ${BASH_VERSION}')
bash_version_major=$(bash -c 'echo ${BASH_VERSINFO[0]}')
bash_version_minor=$(bash -c 'echo ${BASH_VERSINFO[1]}')
-bash_too_old=""
-if [ "$bash_version_major" -le 3 ]; then
- bash_too_old=true
-elif [ "$bash_version_major" -eq 4 ] && [ "$bash_version_minor" -lt 3 ]; then
- bash_too_old=true
+bash_ok=1
+if [ "$bash_version_major" -le 3 ]; then
+ bash_ok=0
+elif [ "$bash_version_major" -eq 4 ] && [ "$bash_version_minor" -lt 3 ]; then
+ bash_ok=0
fi
-if [ "$bash_too_old" = true ]; then
- echo " * Bash version: $bash_version - WARNING!"
- if [ "$login_shell" = bash ]; then
- echo " > Your bash version is old."
- echo " > Bash is also your login shell."
- echo " > Updating to bash 4.3+ is strongly RECOMMENDED!"
- else
- echo " > Your bash version is old"
- echo " > Bash is not your login shell so it should not be an issue."
- echo " > Updating to bash 4.3+ is recommended."
- fi
-else
- echo " * Bash version: $bash_version - OK"
+if [ "$bash_ok" != 1 ]; then
+ echo "* UNSUPPORTED bash version: $bash_version"
+ echo " -> Update to bash 4.3+ if you want to use RESH in bash"
+ echo
fi
-
+zsh_ok=1
if ! zsh --version >/dev/null 2>&1; then
- echo " * Zsh version: ? - not installed!"
+ echo "* Zsh not installed"
+ zsh_ok=0
else
zsh_version=$(zsh -c 'echo ${ZSH_VERSION}')
zsh_version_major=$(echo "$zsh_version" | cut -d'.' -f1)
- if [ "$zsh_version_major" -lt 5 ]; then
- echo " * Zsh version: $zsh_version - UNSUPPORTED!"
- if [ "$login_shell" = zsh ]; then
- echo " > Your zsh version is old."
- echo " > Zsh is also your login shell."
- echo " > Updating to Zsh 5.0+ is strongly RECOMMENDED!"
- else
- echo " > Your zsh version is old"
- echo " > Zsh is not your login shell so it should not be an issue."
- echo " > Updating to zsh 5.0+ is recommended."
- fi
- else
- echo " * Zsh version: $zsh_version - OK"
+ if [ "$zsh_version_major" -lt 5 ]; then
+ echo "* UNSUPPORTED zsh version: $zsh_version"
+ echo " -> Updatie to zsh 5.0+ if you want to use RESH in zsh"
+ echo
+ zsh_ok=0
fi
fi
-
-if [ "$(uname)" = Darwin ]; then
- if gnohup --version >/dev/null 2>&1; then
- echo " * Nohup installed: OK"
- else
- echo " * Nohup installed: NOT INSTALLED!"
- echo " > You don't have nohup"
- echo " > Please install GNU coreutils"
- echo
- echo " $ brew install coreutils"
- echo
- exit 1
- fi
-else
- if setsid --version >/dev/null 2>&1; then
- echo " * Setsid installed: OK"
- else
- echo " * Setsid installed: NOT INSTALLED!"
- echo " > You don't have setsid"
- echo " > Please install unix-util"
+if [ "$bash_ok" != 1 ] && [ "$zsh_ok" != 1 ]; then
+ echo "* You have no shell that is supported by RESH!"
+ echo " -> Please install/update zsh or bash and run this installation again"
+ echo
+ if [ -z "${RESH_INSTALL_IGNORE_NO_SHELL-}" ]; then
+ echo 'EXITING!'
+ echo ' -> You can prevent this check by setting `export RESH_INSTALL_IGNORE_NO_SHELL=1`'
echo
exit 1
fi
fi
-# echo
+printf '\e[0m' # reset
# echo "Continue with installation? (Any key to CONTINUE / Ctrl+C to ABORT)"
# # shellcheck disable=2034
# read -r x
-echo
-echo "Creating directories ..."
+if [ -z "${__RESH_VERSION-}" ]; then
+ # First installation
+ # Stop the daemon anyway just to be sure
+ # But don't output anything
+ ./scripts/resh-daemon-stop.sh -q
+else
+ ./scripts/resh-daemon-stop.sh
+fi
+
+echo "Installing ..."
+# Crete dirs first to get rid of edge-cases
+# If we fail we don't roll back - directories are harmless
mkdir_if_not_exists() {
if [ ! -d "$1" ]; then
- mkdir "$1"
+ mkdir "$1"
fi
}
mkdir_if_not_exists ~/.resh
mkdir_if_not_exists ~/.resh/bin
-mkdir_if_not_exists ~/.resh/bash_completion.d
-mkdir_if_not_exists ~/.resh/zsh_completion.d
mkdir_if_not_exists ~/.config
-echo "Copying files ..."
+# Run setup and update tasks
+
+./bin/resh-install-utils setup-device
+# migrate-all updates format of config and history
+# migrate-all restores original config and history on error
+# There is no need to roll back anything else because we haven't replaced
+# anything in the previous installation.
+./bin/resh-install-utils migrate-all
+
+
+# Copy files
+
+# echo "Copying files ..."
cp -f submodules/bash-preexec/bash-preexec.sh ~/.bash-preexec.sh
cp -f submodules/bash-zsh-compat-widgets/bindfunc.sh ~/.resh/bindfunc.sh
cp -f scripts/shellrc.sh ~/.resh/shellrc
-cp -f scripts/reshctl.sh scripts/widgets.sh scripts/hooks.sh scripts/util.sh ~/.resh/
+cp -f scripts/resh-daemon-start.sh ~/.resh/bin/resh-daemon-start
+cp -f scripts/resh-daemon-stop.sh ~/.resh/bin/resh-daemon-stop
+cp -f scripts/resh-daemon-restart.sh ~/.resh/bin/resh-daemon-restart
+cp -f scripts/hooks.sh ~/.resh/
cp -f scripts/rawinstall.sh ~/.resh/
-update_config() {
- version=$1
- key=$2
- value=$3
- # TODO: create bin/semver-lt
- if bin/semver-lt "${__RESH_VERSION:-0.0.0}" "$1" && [ "$(bin/resh-config -key $key)" != "$value" ] ; then
- echo " * config option $key was updated to $value"
- # TODO: enable resh-config value setting
- # resh-config -key "$key" -value "$value"
- fi
-}
+# echo "Copying more files ..."
+# Copy all go executables. We don't really need to omit install-utils from the bin directory
+cp -f bin/resh-* ~/.resh/bin/
+# Rename reshctl
+mv ~/.resh/bin/resh-control ~/.resh/bin/reshctl
-# Do not overwrite config if it exists
-if [ ! -f ~/.config/resh.toml ]; then
- echo "Copying config file ..."
- cp -f conf/config.toml ~/.config/resh.toml
-# else
- # echo "Merging config files ..."
- # NOTE: This is where we will merge configs when we make changes to the upstream config
- # HINT: check which version are we updating FROM and make changes to config based on that
+echo "Handling shell files ..."
+# Only add shell directives into bash if it passed version checks
+if [ "$bash_ok" = 1 ]; then
+ if [ ! -f ~/.bashrc ]; then
+ touch ~/.bashrc
+ fi
+ # Adding resh shellrc to .bashrc ...
+ grep -q '[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' ~/.bashrc ||\
+ echo -e '\n[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc # this line was added by RESH' >> ~/.bashrc
+ # Adding bash-preexec to .bashrc ...
+ grep -q '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' ~/.bashrc ||\
+ echo -e '\n[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # this line was added by RESH' >> ~/.bashrc
fi
-echo "Generating completions ..."
-bin/resh-control completion bash > ~/.resh/bash_completion.d/_reshctl
-bin/resh-control completion zsh > ~/.resh/zsh_completion.d/_reshctl
-
-echo "Copying more files ..."
-cp -f scripts/uuid.sh ~/.resh/bin/resh-uuid
-cp -f bin/* ~/.resh/bin/
-cp -f scripts/resh-evaluate-plot.py ~/.resh/bin/
-cp -fr data/sanitizer ~/.resh/sanitizer_data
-
-# backward compatibility: We have a new location for resh history file
-[ ! -f ~/.resh/history.json ] || mv ~/.resh/history.json ~/.resh_history.json
-
-echo "Finishing up ..."
-# Adding resh shellrc to .bashrc ...
-if [ ! -f ~/.bashrc ]; then
- touch ~/.bashrc
-fi
-grep -q '[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' ~/.bashrc ||\
- echo -e '\n[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc # this line was added by RESH (Rich Enchanced Shell History)' >> ~/.bashrc
-# Adding bash-preexec to .bashrc ...
-grep -q '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' ~/.bashrc ||\
- echo -e '\n[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # this line was added by RESH (Rich Enchanced Shell History)' >> ~/.bashrc
-# Adding resh shellrc to .zshrc ...
-if [ -f ~/.zshrc ]; then
- grep -q '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' ~/.zshrc ||\
- echo -e '\n[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc # this line was added by RESH (Rich Enchanced Shell History)' >> ~/.zshrc
+# Only add shell directives into zsh if it passed version checks
+if [ "$zsh_ok" = 1 ]; then
+ # Adding resh shellrc to .zshrc ...
+ if [ -f ~/.zshrc ]; then
+ grep -q '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' ~/.zshrc ||\
+ echo -e '\n[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc # this line was added by RESH' >> ~/.zshrc
+ fi
fi
-# Deleting zsh completion cache - for future use
-# [ ! -e ~/.zcompdump ] || rm ~/.zcompdump
+~/.resh/bin/resh-daemon-start
-# Final touch
-touch ~/.resh_history.json
+# bright green
+high='\e[1m'
+reset='\e[0m'
-# Generating resh-uuid ...
-[ -e ~/.resh/resh-uuid ] \
- || cat /proc/sys/kernel/random/uuid > ~/.resh/resh-uuid 2>/dev/null \
- || scripts/uuid.sh > ~/.resh/resh-uuid 2>/dev/null
+printf '
+Installation finished successfully.
-# Source utils to get __resh_run_daemon function
-# shellcheck source=util.sh
-. ~/.resh/util.sh
-# Restarting resh daemon ...
-if [ -f ~/.resh/resh.pid ]; then
- kill -SIGTERM "$(cat ~/.resh/resh.pid)" || true
- rm ~/.resh/resh.pid
-else
- pkill -SIGTERM "resh-daemon" || true
+QUICK START
+\e[32;1m Press CTRL+R to launch RESH SEARCH \e[0m
+'
+if [ -z "${__RESH_VERSION-}" ]; then
+ printf 'You will need to restart your terminal first!\n'
fi
-# daemon uses xdg path variables
-__resh_set_xdg_home_paths
-__resh_run_daemon
-
-
-info="---- Scroll down using arrow keys ----
-#####################################
-# ____ _____ ____ _ _ #
-# | _ \| ____/ ___|| | | | #
-# | |_) | _| \___ \| |_| | #
-# | _ <| |___ ___) | _ | #
-# |_| \_\_____|____/|_| |_| #
-# Rich Enhanced Shell History #
-#####################################
-"
-
-info="$info
-RESH SEARCH APPLICATION = Redesigned reverse search that actually works
-
- >>> Launch RESH SEARCH app by pressing CTRL+R <<<
- (you will need to restart your terminal first)
-
- Search your history by commands.
- Host, directories, git remote, and exit status is used to display relevant results first.
-
- At first, the search application will use the standard shell history without context.
- All history recorded from now on will have context which will by the RESH SEARCH app.
-
- Enable/disable Ctrl+R binding using reshctl command:
- $ reshctl enable ctrl_r_binding
- $ reshctl disable ctrl_r_binding
-
-CHECK FOR UPDATES
- To check for (and install) updates use reshctl command:
- $ reshctl update
-
-HISTORY
- Your resh history will be recorded to '~/.resh_history.json'
- Look at it using e.g. following command (you might need to install jq)
- $ tail -f ~/.resh_history.json | jq
-
-ISSUES & FEEDBACK
- Please report issues to: https://github.com/curusarn/resh/issues
- Feedback and suggestions are very welcome!
-"
-if [ -z "${__RESH_VERSION:-}" ]; then info="$info
-##############################################################
-# #
-# Finish the installation by RESTARTING this terminal! #
-# #
-##############################################################"
-fi
-
-info="$info
----- Close this by pressing Q ----"
+printf '
+ Full-text search your shell history.
+ Relevant results are displayed first based on current directory, git repo, and exit status.
+ RESH will locally record and save shell history with context (directory, time, exit status, ...)
+ Start using RESH right away because bash and zsh history are also searched.
-echo "$info" | ${PAGER:-less}
+ Update RESH by running: reshctl update
+ Thank you for using RESH!
+'
-echo
-echo "All done!"
-echo "Thank you for using RESH"
-echo "Issues go here: https://github.com/curusarn/resh/issues"
-echo "Ctrl+R launches the RESH SEARCH app"
-# echo "Do not forget to restart your terminal"
-if [ -z "${__RESH_VERSION:-}" ]; then echo "
+# Show banner if RESH is not loaded in the terminal
+if [ -z "${__RESH_VERSION-}" ]; then printf '
##############################################################
# #
# Finish the installation by RESTARTING this terminal! #
# #
-##############################################################"
+##############################################################
+'
fi
diff --git a/scripts/resh-daemon-restart.sh b/scripts/resh-daemon-restart.sh
new file mode 100755
index 00000000..42ed1610
--- /dev/null
+++ b/scripts/resh-daemon-restart.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env sh
+resh-daemon-stop "$@"
+resh-daemon-start "$@"
\ No newline at end of file
diff --git a/scripts/resh-daemon-start.sh b/scripts/resh-daemon-start.sh
new file mode 100755
index 00000000..ecb75435
--- /dev/null
+++ b/scripts/resh-daemon-start.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env sh
+if [ "${1-}" != "-q" ]; then
+ echo "Starting RESH daemon ..."
+ printf "Logs are in: %s\n" "${XDG_DATA_HOME-~/.local/share}/resh/log.json"
+fi
+# Run daemon in background - don't block
+# Redirect stdin, stdout, and stderr to /dev/null - detach all I/O
+resh-daemon /dev/null 2>/dev/null &
+
+# After resh-daemon-start.sh exits the resh-daemon process loses its parent
+# and it gets adopted by init
+
+# NOTES:
+# No disown - job control of this shell doesn't affect the parent shell
+# No nohup - SIGHUP signals won't be sent to orphaned resh-daemon (plus the daemon ignores them)
+# No setsid - SIGINT signals won't be sent to orphaned resh-daemon (plus the daemon ignores them)
\ No newline at end of file
diff --git a/scripts/resh-daemon-stop.sh b/scripts/resh-daemon-stop.sh
new file mode 100755
index 00000000..742c2d36
--- /dev/null
+++ b/scripts/resh-daemon-stop.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env sh
+
+failed_to_kill() {
+ [ "${1-}" != "-q" ] && echo "Failed to kill the RESH daemon - it probably isn't running"
+}
+
+xdg_pid() {
+ local path="${XDG_DATA_HOME-}"/resh/daemon.pid
+ [ -n "${XDG_DATA_HOME-}" ] && [ -f "$path" ] || return 1
+ cat "$path"
+}
+default_pid() {
+ local path=~/.local/share/resh/daemon.pid
+ [ -f "$path" ] || return 1
+ cat "$path"
+}
+legacy_pid() {
+ local path=~/.resh/resh.pid
+ [ -f "$path" ] || return 1
+ cat "$path"
+}
+pid=$(xdg_pid || default_pid || legacy_pid)
+
+if [ -n "$pid" ]; then
+ [ "${1-}" != "-q" ] && printf "Stopping RESH daemon ... (PID: %s)\n" "$pid"
+ kill "$pid" || failed_to_kill
+else
+ [ "${1-}" != "-q" ] && printf "Stopping RESH daemon ...\n"
+ killall -q resh-daemon || failed_to_kill
+fi
+
diff --git a/scripts/resh-evaluate-plot.py b/scripts/resh-evaluate-plot.py
deleted file mode 100755
index 89792cbb..00000000
--- a/scripts/resh-evaluate-plot.py
+++ /dev/null
@@ -1,1218 +0,0 @@
-#!/usr/bin/env python3
-
-
-import traceback
-import sys
-import json
-from collections import defaultdict
-import numpy as np
-from graphviz import Digraph
-from datetime import datetime
-
-from matplotlib import rcParams
-rcParams['font.family'] = 'serif'
-# rcParams['font.serif'] = ['']
-
-import matplotlib.pyplot as plt
-import matplotlib.path as mpath
-import matplotlib.patches as mpatches
-
-PLOT_WIDTH = 10 # inches
-PLOT_HEIGHT = 7 # inches
-
-PLOT_SIZE_zipf = 20
-
-data = json.load(sys.stdin)
-
-DATA_records = []
-DATA_records_by_session = defaultdict(list)
-DATA_records_by_user = defaultdict(list)
-for user in data["UsersRecords"]:
- if user["Devices"] is None:
- continue
- for device in user["Devices"]:
- if device["Records"] is None:
- continue
- for record in device["Records"]:
- if "invalid" in record and record["invalid"]:
- continue
-
- DATA_records.append(record)
- DATA_records_by_session[record["seqSessionId"]].append(record)
- DATA_records_by_user[user["Name"] + ":" + device["Name"]].append(record)
-
-DATA_records = list(sorted(DATA_records, key=lambda x: x["realtimeAfterLocal"]))
-
-for pid, session in DATA_records_by_session.items():
- session = list(sorted(session, key=lambda x: x["realtimeAfterLocal"]))
-
-# TODO: this should be a cmdline option
-async_draw = True
-
-# for strategy in data["Strategies"]:
-# print(json.dumps(strategy))
-
-def zipf(length):
- return list(map(lambda x: 1/2**x, range(0, length)))
-
-
-def trim(text, length, add_elipse=True):
- if add_elipse and len(text) > length:
- return text[:length-1] + "…"
- return text[:length]
-
-
-# Figure 3.1. The normalized command frequency, compared with Zipf.
-def plot_cmdLineFrq_rank(plotSize=PLOT_SIZE_zipf, show_labels=False):
- cmdLine_count = defaultdict(int)
- for record in DATA_records:
- cmdLine_count[record["cmdLine"]] += 1
-
- tmp = sorted(cmdLine_count.items(), key=lambda x: x[1], reverse=True)[:plotSize]
- cmdLineFrq = list(map(lambda x: x[1] / tmp[0][1], tmp))
- labels = list(map(lambda x: trim(x[0], 7), tmp))
-
- ranks = range(1, len(cmdLineFrq)+1)
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.plot(ranks, zipf(len(ranks)), '-')
- plt.plot(ranks, cmdLineFrq, 'o-')
- plt.title("Commandline frequency / rank")
- plt.ylabel("Normalized commandline frequency")
- plt.xlabel("Commandline rank")
- plt.legend(("Zipf", "Commandline"), loc="best")
- if show_labels:
- plt.xticks(ranks, labels, rotation=-60)
- # TODO: make xticks integral
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-# similar to ~ Figure 3.1. The normalized command frequency, compared with Zipf.
-def plot_cmdFrq_rank(plotSize=PLOT_SIZE_zipf, show_labels=False):
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Command frequency / rank")
- plt.ylabel("Normalized command frequency")
- plt.xlabel("Command rank")
- legend = []
-
-
- cmd_count = defaultdict(int)
- len_records = 0
- for record in DATA_records:
- cmd = record["command"]
- if cmd == "":
- continue
- cmd_count[cmd] += 1
- len_records += 1
-
- tmp = sorted(cmd_count.items(), key=lambda x: x[1], reverse=True)[:plotSize]
- cmdFrq = list(map(lambda x: x[1] / tmp[0][1], tmp))
- labels = list(map(lambda x: trim(x[0], 7), tmp))
-
- top100percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(1 * len(cmd_count))])) / len_records
- top10percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(0.1 * len(cmd_count))])) / len_records
- top20percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(0.2 * len(cmd_count))])) / len_records
- print("% ALL: Top {} %% of cmds amounts for {} %% of all command lines".format(100, top100percent))
- print("% ALL: Top {} %% of cmds amounts for {} %% of all command lines".format(10, top10percent))
- print("% ALL: Top {} %% of cmds amounts for {} %% of all command lines".format(20, top20percent))
- ranks = range(1, len(cmdFrq)+1)
- plt.plot(ranks, zipf(len(ranks)), '-')
- legend.append("Zipf distribution")
- plt.plot(ranks, cmdFrq, 'o-')
- legend.append("All subjects")
-
-
- for user in DATA_records_by_user.items():
- cmd_count = defaultdict(int)
- len_records = 0
- name, records = user
- for record in records:
- cmd = record["command"]
- if cmd == "":
- continue
- cmd_count[cmd] += 1
- len_records += 1
-
- tmp = sorted(cmd_count.items(), key=lambda x: x[1], reverse=True)[:plotSize]
- cmdFrq = list(map(lambda x: x[1] / tmp[0][1], tmp))
- labels = list(map(lambda x: trim(x[0], 7), tmp))
-
- top100percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(1 * len(cmd_count))])) / len_records
- top10percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(0.1 * len(cmd_count))])) / len_records
- top20percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(0.2 * len(cmd_count))])) / len_records
- print("% {}: Top {} %% of cmds amounts for {} %% of all command lines".format(name, 100, top100percent))
- print("% {}: Top {} %% of cmds amounts for {} %% of all command lines".format(name, 10, top10percent))
- print("% {}: Top {} %% of cmds amounts for {} %% of all command lines".format(name, 20, top20percent))
- ranks = range(1, len(cmdFrq)+1)
- plt.plot(ranks, cmdFrq, 'o-')
- legend.append("{} (sanitize!)".format(name))
-
- plt.legend(legend, loc="best")
-
- if show_labels:
- plt.xticks(ranks, labels, rotation=-60)
- # TODO: make xticks integral
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-# Figure 3.2. Command vocabulary size vs. the number of command lines entered for four individuals.
-def plot_cmdVocabularySize_cmdLinesEntered():
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Command vocabulary size vs. the number of command lines entered")
- plt.ylabel("Command vocabulary size")
- plt.xlabel("# of command lines entered")
- legend = []
-
- # x_count = max(map(lambda x: len(x[1]), DATA_records_by_user.items()))
- # x_values = range(0, x_count)
- for user in DATA_records_by_user.items():
- new_cmds_after_1k = 0
- new_cmds_after_2k = 0
- new_cmds_after_3k = 0
- cmd_vocabulary = set()
- y_cmd_count = [0]
- name, records = user
- for record in records:
- cmd = record["command"]
- if cmd == "":
- continue
- if cmd in cmd_vocabulary:
- # repeat last value
- y_cmd_count.append(y_cmd_count[-1])
- else:
- cmd_vocabulary.add(cmd)
- # append last value +1
- y_cmd_count.append(y_cmd_count[-1] + 1)
- if len(y_cmd_count) > 1000:
- new_cmds_after_1k+=1
- if len(y_cmd_count) > 2000:
- new_cmds_after_2k+=1
- if len(y_cmd_count) > 3000:
- new_cmds_after_3k+=1
-
- if len(y_cmd_count) == 1000:
- print("% {}: Cmd adoption rate at 1k (between 0 and 1k) cmdlines = {}".format(name ,len(cmd_vocabulary) / (len(y_cmd_count))))
- if len(y_cmd_count) == 2000:
- print("% {}: Cmd adoption rate at 2k cmdlines = {}".format(name ,len(cmd_vocabulary) / (len(y_cmd_count))))
- print("% {}: Cmd adoption rate between 1k and 2k cmdlines = {}".format(name ,new_cmds_after_1k / (len(y_cmd_count) - 1000)))
- if len(y_cmd_count) == 3000:
- print("% {}: Cmd adoption rate between 2k and 3k cmdlines = {}".format(name ,new_cmds_after_2k / (len(y_cmd_count) - 2000)))
-
- print("% {}: New cmd adoption rate after 1k cmdlines = {}".format(name ,new_cmds_after_1k / (len(y_cmd_count) - 1000)))
- print("% {}: New cmd adoption rate after 2k cmdlines = {}".format(name ,new_cmds_after_2k / (len(y_cmd_count) - 2000)))
- print("% {}: New cmd adoption rate after 3k cmdlines = {}".format(name ,new_cmds_after_3k / (len(y_cmd_count) - 3000)))
- x_cmds_entered = range(0, len(y_cmd_count))
- plt.plot(x_cmds_entered, y_cmd_count, '-')
- legend.append(name + " (TODO: sanitize!)")
-
- # print(cmd_vocabulary)
-
- plt.legend(legend, loc="best")
-
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-def plot_cmdVocabularySize_daily():
- SECONDS_IN_A_DAY = 86400
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Command vocabulary size in days")
- plt.ylabel("Command vocabulary size")
- plt.xlabel("Days")
- legend = []
-
- # x_count = max(map(lambda x: len(x[1]), DATA_records_by_user.items()))
- # x_values = range(0, x_count)
- for user in DATA_records_by_user.items():
- new_cmds_after_100 = 0
- new_cmds_after_200 = 0
- new_cmds_after_300 = 0
- cmd_vocabulary = set()
- y_cmd_count = [0]
- name, records = user
-
- cmd_fail_count = 0
-
- if not len(records):
- print("ERROR: no records for user {}".format(name))
- continue
-
- first_day = records[0]["realtimeAfter"]
- this_day = first_day
-
- for record in records:
- cmd = record["command"]
- timestamp = record["realtimeAfter"]
-
- if cmd == "":
- cmd_fail_count += 1
- continue
-
- if timestamp >= this_day + SECONDS_IN_A_DAY:
- this_day += SECONDS_IN_A_DAY
- while timestamp >= this_day + SECONDS_IN_A_DAY:
- y_cmd_count.append(-10)
- this_day += SECONDS_IN_A_DAY
-
- y_cmd_count.append(len(cmd_vocabulary))
- cmd_vocabulary = set() # wipes the vocabulary each day
-
- if len(y_cmd_count) > 100:
- new_cmds_after_100+=1
- if len(y_cmd_count) > 200:
- new_cmds_after_200+=1
- if len(y_cmd_count) > 300:
- new_cmds_after_300+=1
-
- if len(y_cmd_count) == 100:
- print("% {}: Cmd adoption rate at 100 days (between 0 and 100 days) = {}".format(name, len(cmd_vocabulary) / (len(y_cmd_count))))
- if len(y_cmd_count) == 200:
- print("% {}: Cmd adoption rate at 200 days days = {}".format(name, len(cmd_vocabulary) / (len(y_cmd_count))))
- print("% {}: Cmd adoption rate between 100 and 200 days = {}".format(name, new_cmds_after_100 / (len(y_cmd_count) - 100)))
- if len(y_cmd_count) == 300:
- print("% {}: Cmd adoption rate between 200 and 300 days = {}".format(name, new_cmds_after_200 / (len(y_cmd_count) - 200)))
-
- if cmd not in cmd_vocabulary:
- cmd_vocabulary.add(cmd)
-
-
- print("% {}: New cmd adoption rate after 100 days = {}".format(name, new_cmds_after_100 / (len(y_cmd_count) - 100)))
- print("% {}: New cmd adoption rate after 200 days = {}".format(name, new_cmds_after_200 / (len(y_cmd_count) - 200)))
- print("% {}: New cmd adoption rate after 300 days = {}".format(name, new_cmds_after_300 / (len(y_cmd_count) - 300)))
- print("% {}: cmd_fail_count = {}".format(name, cmd_fail_count))
- x_cmds_entered = range(0, len(y_cmd_count))
- plt.plot(x_cmds_entered, y_cmd_count, 'o', markersize=2)
- legend.append(name + " (TODO: sanitize!)")
-
- # print(cmd_vocabulary)
-
- plt.legend(legend, loc="best")
- plt.ylim(bottom=-5)
-
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-def matplotlib_escape(ss):
- ss = ss.replace('$', '\\$')
- return ss
-
-
-def plot_cmdUsage_in_time(sort_cmds=False, num_cmds=None):
- SECONDS_IN_A_DAY = 86400
- tab_colors = ("tab:blue", "tab:orange", "tab:green", "tab:red", "tab:purple", "tab:brown", "tab:pink", "tab:gray")
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Command use in time")
- plt.ylabel("Commands")
- plt.xlabel("Days")
- legend_patches = []
-
- cmd_ids = {}
- y_labels = []
-
- all_x_values = []
- all_y_values = []
- all_s_values = [] # size
- all_c_values = [] # color
-
- x_values = []
- y_values = []
- s_values = [] # size
- c_values = [] # color
-
- if sort_cmds:
- cmd_count = defaultdict(int)
- for user in DATA_records_by_user.items():
- name, records = user
- for record in records:
- cmd = record["command"]
- cmd_count[cmd] += 1
-
- sorted_cmds = map(lambda x: x[0], sorted(cmd_count.items(), key=lambda x: x[1], reverse=True))
-
- for cmd in sorted_cmds:
- cmd_ids[cmd] = len(cmd_ids)
- y_labels.append(matplotlib_escape(cmd))
-
-
- for user_idx, user in enumerate(DATA_records_by_user.items()):
- name, records = user
-
- if not len(records):
- print("ERROR: no records for user {}".format(name))
- continue
-
-
- first_day = records[0]["realtimeAfter"]
- this_day = first_day
- day_no = 0
- today_cmds = defaultdict(int)
-
- for record in records:
- cmd = record["command"]
- timestamp = record["realtimeAfter"]
-
- if cmd == "":
- print("NOTICE: Empty cmd for {}".format(record["cmdLine"]))
- continue
-
- if timestamp >= this_day + SECONDS_IN_A_DAY:
- for item in today_cmds.items():
- cmd, count = item
- cmd_id = cmd_ids[cmd]
- # skip commands with high ids
- if num_cmds is not None and cmd_id >= num_cmds:
- continue
-
- x_values.append(day_no)
- y_values.append(cmd_id)
- s_values.append(count)
- c_values.append(tab_colors[user_idx])
-
- today_cmds = defaultdict(int)
-
- this_day += SECONDS_IN_A_DAY
- day_no += 1
- while timestamp >= this_day + SECONDS_IN_A_DAY:
- this_day += SECONDS_IN_A_DAY
- day_no += 1
-
- if cmd not in cmd_ids:
- cmd_ids[cmd] = len(cmd_ids)
- y_labels.append(matplotlib_escape(cmd))
-
- today_cmds[cmd] += 1
-
- all_x_values.extend(x_values)
- all_y_values.extend(y_values)
- all_s_values.extend(s_values)
- all_c_values.extend(c_values)
- x_values = []
- y_values = []
- s_values = []
- c_values = []
- legend_patches.append(mpatches.Patch(color=tab_colors[user_idx], label="{} ({}) (TODO: sanitize!)".format(name, user_idx)))
-
- if num_cmds is not None and len(y_labels) > num_cmds:
- y_labels = y_labels[:num_cmds]
- plt.yticks(ticks=range(0, len(y_labels)), labels=y_labels, fontsize=6)
- plt.scatter(all_x_values, all_y_values, s=all_s_values, c=all_c_values, marker='o')
- plt.legend(handles=legend_patches, loc="best")
-
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-# Figure 5.6. Command line vocabulary size vs. the number of commands entered for four typical individuals.
-def plot_cmdVocabularySize_time():
- SECONDS_IN_A_DAY = 86400
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Command vocabulary size growth in time")
- plt.ylabel("Command vocabulary size")
- plt.xlabel("Days")
- legend = []
-
- # x_count = max(map(lambda x: len(x[1]), DATA_records_by_user.items()))
- # x_values = range(0, x_count)
- for user in DATA_records_by_user.items():
- new_cmds_after_100 = 0
- new_cmds_after_200 = 0
- new_cmds_after_300 = 0
- cmd_vocabulary = set()
- y_cmd_count = [0]
- name, records = user
-
- cmd_fail_count = 0
-
- if not len(records):
- print("ERROR: no records for user {}".format(name))
- continue
-
- first_day = records[0]["realtimeAfter"]
- this_day = first_day
-
- for record in records:
- cmd = record["command"]
- timestamp = record["realtimeAfter"]
-
- if cmd == "":
- cmd_fail_count += 1
- continue
-
- if timestamp >= this_day + SECONDS_IN_A_DAY:
- this_day += SECONDS_IN_A_DAY
- while timestamp >= this_day + SECONDS_IN_A_DAY:
- y_cmd_count.append(-10)
- this_day += SECONDS_IN_A_DAY
-
- y_cmd_count.append(len(cmd_vocabulary))
-
- if len(y_cmd_count) > 100:
- new_cmds_after_100+=1
- if len(y_cmd_count) > 200:
- new_cmds_after_200+=1
- if len(y_cmd_count) > 300:
- new_cmds_after_300+=1
-
- if len(y_cmd_count) == 100:
- print("% {}: Cmd adoption rate at 100 days (between 0 and 100 days) = {}".format(name, len(cmd_vocabulary) / (len(y_cmd_count))))
- if len(y_cmd_count) == 200:
- print("% {}: Cmd adoption rate at 200 days days = {}".format(name, len(cmd_vocabulary) / (len(y_cmd_count))))
- print("% {}: Cmd adoption rate between 100 and 200 days = {}".format(name, new_cmds_after_100 / (len(y_cmd_count) - 100)))
- if len(y_cmd_count) == 300:
- print("% {}: Cmd adoption rate between 200 and 300 days = {}".format(name, new_cmds_after_200 / (len(y_cmd_count) - 200)))
-
- if cmd not in cmd_vocabulary:
- cmd_vocabulary.add(cmd)
-
-
- print("% {}: New cmd adoption rate after 100 days = {}".format(name, new_cmds_after_100 / (len(y_cmd_count) - 100)))
- print("% {}: New cmd adoption rate after 200 days = {}".format(name, new_cmds_after_200 / (len(y_cmd_count) - 200)))
- print("% {}: New cmd adoption rate after 300 days = {}".format(name, new_cmds_after_300 / (len(y_cmd_count) - 300)))
- print("% {}: cmd_fail_count = {}".format(name, cmd_fail_count))
- x_cmds_entered = range(0, len(y_cmd_count))
- plt.plot(x_cmds_entered, y_cmd_count, 'o', markersize=2)
- legend.append(name + " (TODO: sanitize!)")
-
- # print(cmd_vocabulary)
-
- plt.legend(legend, loc="best")
- plt.ylim(bottom=0)
-
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-# Figure 5.6. Command line vocabulary size vs. the number of commands entered for four typical individuals.
-def plot_cmdLineVocabularySize_cmdLinesEntered():
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Command line vocabulary size vs. the number of command lines entered")
- plt.ylabel("Command line vocabulary size")
- plt.xlabel("# of command lines entered")
- legend = []
-
- for user in DATA_records_by_user.items():
- cmdLine_vocabulary = set()
- y_cmdLine_count = [0]
- name, records = user
- for record in records:
- cmdLine = record["cmdLine"]
- if cmdLine in cmdLine_vocabulary:
- # repeat last value
- y_cmdLine_count.append(y_cmdLine_count[-1])
- else:
- cmdLine_vocabulary.add(cmdLine)
- # append last value +1
- y_cmdLine_count.append(y_cmdLine_count[-1] + 1)
-
- # print(cmdLine_vocabulary)
- x_cmdLines_entered = range(0, len(y_cmdLine_count))
- plt.plot(x_cmdLines_entered, y_cmdLine_count, '-')
- legend.append(name + " (TODO: sanitize!)")
-
- plt.legend(legend, loc="best")
-
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-# Figure 3.3. Sequential structure of UNIX command usage, from Figure 4 in Hanson et al. (1984).
-# Ball diameters are proportional to stationary probability. Lines indicate significant dependencies,
-# solid ones being more probable (p < .0001) and dashed ones less probable (.005 < p < .0001).
-def graph_cmdSequences(node_count=33, edge_minValue=0.05, view_graph=True):
- START_CMD = "_start_"
- END_CMD = "_end_"
- cmd_count = defaultdict(int)
- cmdSeq_count = defaultdict(lambda: defaultdict(int))
- cmd_id = dict()
- x = 0
- cmd_id[START_CMD] = str(x)
- x += 1
- cmd_id[END_CMD] = str(x)
- for pid, session in DATA_records_by_session.items():
- cmd_count[START_CMD] += 1
- prev_cmd = START_CMD
- for record in session:
- cmd = record["command"]
- if cmd == "":
- continue
- cmdSeq_count[prev_cmd][cmd] += 1
- cmd_count[cmd] += 1
- if cmd not in cmd_id:
- x += 1
- cmd_id[cmd] = str(x)
- prev_cmd = cmd
- # end the session
- cmdSeq_count[prev_cmd][END_CMD] += 1
- cmd_count[END_CMD] += 1
-
-
- # get `node_count` of largest nodes
- sorted_cmd_count = sorted(cmd_count.items(), key=lambda x: x[1], reverse=True)
- print(sorted_cmd_count)
- cmds_to_graph = list(map(lambda x: x[0], sorted_cmd_count))[:node_count]
-
- # use 3 biggest nodes as a reference point for scaling
- biggest_node = cmd_count[cmds_to_graph[0]]
- nd_biggest_node = cmd_count[cmds_to_graph[1]]
- rd_biggest_node = cmd_count[cmds_to_graph[1]]
- count2scale_coef = 3 / (biggest_node + nd_biggest_node + rd_biggest_node)
-
- # scaling constant
- # affects node size and node label
- base_scaling_factor = 21
- # extra scaling for experiments - not really useful imho
- # affects everything nodes, edges, node labels, treshold for turning label into xlabel, xlabel size, ...
- extra_scaling_factor = 1.0
- for x in range(0, 10):
- # graphviz is not the most reliable piece of software
- # -> retry on fail but scale nodes down by 1%
- scaling_factor = base_scaling_factor * (1 - x * 0.01)
-
- # overlap: scale -> solve overlap by scaling the graph
- # overlap_shrink -> try to shrink the graph a bit after you are done
- # splines -> don't draw edges over nodes
- # sep: 2.5 -> assume that nodes are 2.5 inches larger
- graph_attr={'overlap':'scale', 'overlap_shrink':'true',
- 'splines':'true', 'sep':'0.25'}
- graph = Digraph(name='command_sequentiality', engine='neato', graph_attr=graph_attr)
-
- # iterate over all nodes
- for cmd in cmds_to_graph:
- seq = cmdSeq_count[cmd]
- count = cmd_count[cmd]
-
- # iterate over all "following" commands (for each node)
- for seq_entry in seq.items():
- cmd2, seq_count = seq_entry
- relative_seq_count = seq_count / count
-
- # check if "follow" command is supposed to be in the graph
- if cmd2 not in cmds_to_graph:
- continue
- # check if the edge value is high enough
- if relative_seq_count < edge_minValue:
- continue
-
- # create starting node and end node for the edge
- # duplicates don't matter
- for id_, cmd_ in ((cmd_id[cmd], cmd), (cmd_id[cmd2], cmd2)):
- count_ = cmd_count[cmd_]
- scale_ = count_ * count2scale_coef * scaling_factor * extra_scaling_factor
- width_ = 0.08 * scale_
- fontsize_ = 8.5 * scale_ / (len(cmd_) + 3)
-
- width_ = str(width_)
- if fontsize_ < 12 * extra_scaling_factor:
- graph.node(id_, ' ', shape='circle', fixedsize='true', fontname='monospace bold',
- width=width_, fontsize=str(12 * extra_scaling_factor), forcelabels='true', xlabel=cmd_)
- else:
- fontsize_ = str(fontsize_)
- graph.node(id_, cmd_, shape='circle', fixedsize='true', fontname='monospace bold',
- width=width_, fontsize=fontsize_, forcelabels='true', labelloc='c')
-
- # value of the edge (percentage) 1.0 is max
- scale_ = seq_count / cmd_count[cmd]
- penwidth_ = str((0.5 + 4.5 * scale_) * extra_scaling_factor)
- #penwidth_bold_ = str(8 * scale_)
- # if scale_ > 0.5:
- # graph.edge(cmd_id[cmd], cmd_id[cmd2], constraint='true', splines='curved',
- # penwidth=penwidth_, style='bold', arrowhead='diamond')
- # elif scale_ > 0.2:
- if scale_ > 0.3:
- scale_ = str(int(scale_ * 100)/100)
- graph.edge(cmd_id[cmd], cmd_id[cmd2], constraint='true', splines='curved',
- penwidth=penwidth_, forcelables='true', label=scale_)
- elif scale_ > 0.2:
- graph.edge(cmd_id[cmd], cmd_id[cmd2], constraint='true', splines='curved',
- penwidth=penwidth_, style='dashed')
- # elif scale_ > 0.1:
- else:
- graph.edge(cmd_id[cmd], cmd_id[cmd2], constraint='false', splines='curved',
- penwidth=penwidth_, style='dotted', arrowhead='empty')
-
- # graphviz sometimes fails - see above
- try:
- # graph.view()
- graph.render('/tmp/resh-graph-command_sequence-nodeCount_{}-edgeMinVal_{}.gv'.format(node_count, edge_minValue), view=view_graph)
- break
- except Exception as e:
- trace = traceback.format_exc()
- print("GRAPHVIZ EXCEPTION: <{}>\nGRAPHVIZ TRACE: <{}>".format(str(e), trace))
-
-
-def plot_strategies_matches(plot_size=50, selected_strategies=[], show_strat_title=True, force_strat_title=None):
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Matches at distance <{}>".format(datetime.now().strftime('%H:%M:%S')))
- plt.ylabel('%' + " of matches")
- plt.xlabel("Distance")
- legend = []
- x_values = range(1, plot_size+1)
- saved_matches_total = None
- saved_dataPoint_count = None
- for strategy in data["Strategies"]:
- strategy_title = strategy["Title"]
- # strategy_description = strategy["Description"]
-
- dataPoint_count = 0
- matches = [0] * plot_size
- matches_total = 0
- charsRecalled = [0] * plot_size
- charsRecalled_total = 0
-
- for match in strategy["Matches"]:
- dataPoint_count += 1
-
- if not match["Match"]:
- continue
-
- chars = match["CharsRecalled"]
- charsRecalled_total += chars
- matches_total += 1
-
- dist = match["Distance"]
- if dist > plot_size:
- continue
-
- matches[dist-1] += 1
- charsRecalled[dist-1] += chars
-
- # recent is very simple strategy so we will believe
- # that there is no bug in it and we can use it to determine total
- if strategy_title == "recent":
- saved_matches_total = matches_total
- saved_dataPoint_count = dataPoint_count
-
- if len(selected_strategies) and strategy_title not in selected_strategies:
- continue
-
- acc = 0
- matches_cumulative = []
- for x in matches:
- acc += x
- matches_cumulative.append(acc)
- # matches_cumulative.append(matches_total)
- matches_percent = list(map(lambda x: 100 * x / dataPoint_count, matches_cumulative))
-
- plt.plot(x_values, matches_percent, 'o-')
- if force_strat_title is not None:
- legend.append(force_strat_title)
- else:
- legend.append(strategy_title)
-
-
- assert(saved_matches_total is not None)
- assert(saved_dataPoint_count is not None)
- max_values = [100 * saved_matches_total / saved_dataPoint_count] * len(x_values)
- print("% >>> Avg recurrence rate = {}".format(max_values[0]))
- plt.plot(x_values, max_values, 'r-')
- legend.append("maximum possible")
-
- x_ticks = list(range(1, plot_size+1, 2))
- x_labels = x_ticks[:]
- plt.xticks(x_ticks, x_labels)
- plt.ylim(bottom=0)
- if show_strat_title:
- plt.legend(legend, loc="best")
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-def plot_strategies_charsRecalled(plot_size=50, selected_strategies=[]):
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Average characters recalled at distance <{}>".format(datetime.now().strftime('%H:%M:%S')))
- plt.ylabel("Average characters recalled")
- plt.xlabel("Distance")
- x_values = range(1, plot_size+1)
- legend = []
- saved_charsRecalled_total = None
- saved_dataPoint_count = None
- for strategy in data["Strategies"]:
- strategy_title = strategy["Title"]
- # strategy_description = strategy["Description"]
-
- dataPoint_count = 0
- matches = [0] * plot_size
- matches_total = 0
- charsRecalled = [0] * plot_size
- charsRecalled_total = 0
-
- for match in strategy["Matches"]:
- dataPoint_count += 1
-
- if not match["Match"]:
- continue
-
- chars = match["CharsRecalled"]
- charsRecalled_total += chars
- matches_total += 1
-
- dist = match["Distance"]
- if dist > plot_size:
- continue
-
- matches[dist-1] += 1
- charsRecalled[dist-1] += chars
-
- # recent is very simple strategy so we will believe
- # that there is no bug in it and we can use it to determine total
- if strategy_title == "recent":
- saved_charsRecalled_total = charsRecalled_total
- saved_dataPoint_count = dataPoint_count
-
- if len(selected_strategies) and strategy_title not in selected_strategies:
- continue
-
- acc = 0
- charsRecalled_cumulative = []
- for x in charsRecalled:
- acc += x
- charsRecalled_cumulative.append(acc)
- charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled_cumulative))
-
- plt.plot(x_values, charsRecalled_average, 'o-')
- legend.append(strategy_title)
-
- assert(saved_charsRecalled_total is not None)
- assert(saved_dataPoint_count is not None)
- max_values = [saved_charsRecalled_total / saved_dataPoint_count] * len(x_values)
- print("% >>> Max avg recalled characters = {}".format(max_values[0]))
- plt.plot(x_values, max_values, 'r-')
- legend.append("maximum possible")
-
- x_ticks = list(range(1, plot_size+1, 2))
- x_labels = x_ticks[:]
- plt.xticks(x_ticks, x_labels)
- plt.ylim(bottom=0)
- plt.legend(legend, loc="best")
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-def plot_strategies_charsRecalled_prefix(plot_size=50, selected_strategies=[]):
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Average characters recalled at distance (including prefix matches) <{}>".format(datetime.now().strftime('%H:%M:%S')))
- plt.ylabel("Average characters recalled (including prefix matches)")
- plt.xlabel("Distance")
- x_values = range(1, plot_size+1)
- legend = []
- saved_charsRecalled_total = None
- saved_dataPoint_count = None
- for strategy in data["Strategies"]:
- strategy_title = strategy["Title"]
- # strategy_description = strategy["Description"]
-
- dataPoint_count = 0
- matches_total = 0
- charsRecalled = [0] * plot_size
- charsRecalled_total = 0
-
- for multiMatch in strategy["PrefixMatches"]:
- dataPoint_count += 1
-
- if not multiMatch["Match"]:
- continue
- matches_total += 1
-
- last_charsRecalled = 0
- for match in multiMatch["Entries"]:
-
- chars = match["CharsRecalled"]
- charsIncrease = chars - last_charsRecalled
- assert(charsIncrease > 0)
- charsRecalled_total += charsIncrease
-
- dist = match["Distance"]
- if dist > plot_size:
- continue
-
- charsRecalled[dist-1] += charsIncrease
- last_charsRecalled = chars
-
- # recent is very simple strategy so we will believe
- # that there is no bug in it and we can use it to determine total
- if strategy_title == "recent":
- saved_charsRecalled_total = charsRecalled_total
- saved_dataPoint_count = dataPoint_count
-
- if len(selected_strategies) and strategy_title not in selected_strategies:
- continue
-
- acc = 0
- charsRecalled_cumulative = []
- for x in charsRecalled:
- acc += x
- charsRecalled_cumulative.append(acc)
- charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled_cumulative))
-
- plt.plot(x_values, charsRecalled_average, 'o-')
- legend.append(strategy_title)
-
- assert(saved_charsRecalled_total is not None)
- assert(saved_dataPoint_count is not None)
- max_values = [saved_charsRecalled_total / saved_dataPoint_count] * len(x_values)
- print("% >>> Max avg recalled characters (including prefix matches) = {}".format(max_values[0]))
- plt.plot(x_values, max_values, 'r-')
- legend.append("maximum possible")
-
- x_ticks = list(range(1, plot_size+1, 2))
- x_labels = x_ticks[:]
- plt.xticks(x_ticks, x_labels)
- plt.ylim(bottom=0)
- plt.legend(legend, loc="best")
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-def plot_strategies_matches_noncummulative(plot_size=50, selected_strategies=["recent (bash-like)"], show_strat_title=False, force_strat_title=None):
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Matches at distance (noncumulative) <{}>".format(datetime.now().strftime('%H:%M:%S')))
- plt.ylabel('%' + " of matches")
- plt.xlabel("Distance")
- legend = []
- x_values = range(1, plot_size+1)
- saved_matches_total = None
- saved_dataPoint_count = None
- for strategy in data["Strategies"]:
- strategy_title = strategy["Title"]
- # strategy_description = strategy["Description"]
-
- dataPoint_count = 0
- matches = [0] * plot_size
- matches_total = 0
- charsRecalled = [0] * plot_size
- charsRecalled_total = 0
-
- for match in strategy["Matches"]:
- dataPoint_count += 1
-
- if not match["Match"]:
- continue
-
- chars = match["CharsRecalled"]
- charsRecalled_total += chars
- matches_total += 1
-
- dist = match["Distance"]
- if dist > plot_size:
- continue
-
- matches[dist-1] += 1
- charsRecalled[dist-1] += chars
-
- # recent is very simple strategy so we will believe
- # that there is no bug in it and we can use it to determine total
- if strategy_title == "recent":
- saved_matches_total = matches_total
- saved_dataPoint_count = dataPoint_count
-
- if len(selected_strategies) and strategy_title not in selected_strategies:
- continue
-
- # acc = 0
- # matches_cumulative = []
- # for x in matches:
- # acc += x
- # matches_cumulative.append(acc)
- # # matches_cumulative.append(matches_total)
- matches_percent = list(map(lambda x: 100 * x / dataPoint_count, matches))
-
- plt.plot(x_values, matches_percent, 'o-')
- if force_strat_title is not None:
- legend.append(force_strat_title)
- else:
- legend.append(strategy_title)
-
- assert(saved_matches_total is not None)
- assert(saved_dataPoint_count is not None)
- # max_values = [100 * saved_matches_total / saved_dataPoint_count] * len(x_values)
- # print("% >>> Avg recurrence rate = {}".format(max_values[0]))
- # plt.plot(x_values, max_values, 'r-')
- # legend.append("maximum possible")
-
- x_ticks = list(range(1, plot_size+1, 2))
- x_labels = x_ticks[:]
- plt.xticks(x_ticks, x_labels)
- # plt.ylim(bottom=0)
- if show_strat_title:
- plt.legend(legend, loc="best")
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-def plot_strategies_charsRecalled_noncummulative(plot_size=50, selected_strategies=["recent (bash-like)"], show_strat_title=False):
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Average characters recalled at distance (noncumulative) <{}>".format(datetime.now().strftime('%H:%M:%S')))
- plt.ylabel("Average characters recalled")
- plt.xlabel("Distance")
- x_values = range(1, plot_size+1)
- legend = []
- saved_charsRecalled_total = None
- saved_dataPoint_count = None
- for strategy in data["Strategies"]:
- strategy_title = strategy["Title"]
- # strategy_description = strategy["Description"]
-
- dataPoint_count = 0
- matches = [0] * plot_size
- matches_total = 0
- charsRecalled = [0] * plot_size
- charsRecalled_total = 0
-
- for match in strategy["Matches"]:
- dataPoint_count += 1
-
- if not match["Match"]:
- continue
-
- chars = match["CharsRecalled"]
- charsRecalled_total += chars
- matches_total += 1
-
- dist = match["Distance"]
- if dist > plot_size:
- continue
-
- matches[dist-1] += 1
- charsRecalled[dist-1] += chars
-
- # recent is very simple strategy so we will believe
- # that there is no bug in it and we can use it to determine total
- if strategy_title == "recent":
- saved_charsRecalled_total = charsRecalled_total
- saved_dataPoint_count = dataPoint_count
-
- if len(selected_strategies) and strategy_title not in selected_strategies:
- continue
-
- # acc = 0
- # charsRecalled_cumulative = []
- # for x in charsRecalled:
- # acc += x
- # charsRecalled_cumulative.append(acc)
- # charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled_cumulative))
- charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled))
-
- plt.plot(x_values, charsRecalled_average, 'o-')
- legend.append(strategy_title)
-
- assert(saved_charsRecalled_total is not None)
- assert(saved_dataPoint_count is not None)
- # max_values = [saved_charsRecalled_total / saved_dataPoint_count] * len(x_values)
- # print("% >>> Max avg recalled characters = {}".format(max_values[0]))
- # plt.plot(x_values, max_values, 'r-')
- # legend.append("maximum possible")
-
- x_ticks = list(range(1, plot_size+1, 2))
- x_labels = x_ticks[:]
- plt.xticks(x_ticks, x_labels)
- # plt.ylim(bottom=0)
- if show_strat_title:
- plt.legend(legend, loc="best")
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-def plot_strategies_charsRecalled_prefix_noncummulative(plot_size=50, selected_strategies=["recent (bash-like)"], show_strat_title=False):
- plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT))
- plt.title("Average characters recalled at distance (including prefix matches) (noncummulative) <{}>".format(datetime.now().strftime('%H:%M:%S')))
- plt.ylabel("Average characters recalled (including prefix matches)")
- plt.xlabel("Distance")
- x_values = range(1, plot_size+1)
- legend = []
- saved_charsRecalled_total = None
- saved_dataPoint_count = None
- for strategy in data["Strategies"]:
- strategy_title = strategy["Title"]
- # strategy_description = strategy["Description"]
-
- dataPoint_count = 0
- matches_total = 0
- charsRecalled = [0] * plot_size
- charsRecalled_total = 0
-
- for multiMatch in strategy["PrefixMatches"]:
- dataPoint_count += 1
-
- if not multiMatch["Match"]:
- continue
- matches_total += 1
-
- last_charsRecalled = 0
- for match in multiMatch["Entries"]:
-
- chars = match["CharsRecalled"]
- charsIncrease = chars - last_charsRecalled
- assert(charsIncrease > 0)
- charsRecalled_total += charsIncrease
-
- dist = match["Distance"]
- if dist > plot_size:
- continue
-
- charsRecalled[dist-1] += charsIncrease
- last_charsRecalled = chars
-
- # recent is very simple strategy so we will believe
- # that there is no bug in it and we can use it to determine total
- if strategy_title == "recent":
- saved_charsRecalled_total = charsRecalled_total
- saved_dataPoint_count = dataPoint_count
-
- if len(selected_strategies) and strategy_title not in selected_strategies:
- continue
-
- # acc = 0
- # charsRecalled_cumulative = []
- # for x in charsRecalled:
- # acc += x
- # charsRecalled_cumulative.append(acc)
- # charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled_cumulative))
- charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled))
-
- plt.plot(x_values, charsRecalled_average, 'o-')
- legend.append(strategy_title)
-
- assert(saved_charsRecalled_total is not None)
- assert(saved_dataPoint_count is not None)
- # max_values = [saved_charsRecalled_total / saved_dataPoint_count] * len(x_values)
- # print("% >>> Max avg recalled characters (including prefix matches) = {}".format(max_values[0]))
- # plt.plot(x_values, max_values, 'r-')
- # legend.append("maximum possible")
-
- x_ticks = list(range(1, plot_size+1, 2))
- x_labels = x_ticks[:]
- plt.xticks(x_ticks, x_labels)
- # plt.ylim(bottom=0)
- if show_strat_title:
- plt.legend(legend, loc="best")
- if async_draw:
- plt.draw()
- else:
- plt.show()
-
-
-def print_top_cmds(num_cmds=20):
- cmd_count = defaultdict(int)
- cmd_total = 0
- for pid, session in DATA_records_by_session.items():
- for record in session:
- cmd = record["command"]
- if cmd == "":
- continue
- cmd_count[cmd] += 1
- cmd_total += 1
-
- # get `node_count` of largest nodes
- sorted_cmd_count = list(sorted(cmd_count.items(), key=lambda x: x[1], reverse=True))
- print("\n\n% All subjects: Top commands")
- for cmd, count in sorted_cmd_count[:num_cmds]:
- print("{} {}".format(cmd, count))
- # print(sorted_cmd_count)
- # cmds_to_graph = list(map(lambda x: x[0], sorted_cmd_count))[:cmd_count]
-
-
-def print_top_cmds_by_user(num_cmds=20):
- for user in DATA_records_by_user.items():
- name, records = user
- cmd_count = defaultdict(int)
- cmd_total = 0
- for record in records:
- cmd = record["command"]
- if cmd == "":
- continue
- cmd_count[cmd] += 1
- cmd_total += 1
-
- # get `node_count` of largest nodes
- sorted_cmd_count = list(sorted(cmd_count.items(), key=lambda x: x[1], reverse=True))
- print("\n\n% {}: Top commands".format(name))
- for cmd, count in sorted_cmd_count[:num_cmds]:
- print("{} {}".format(cmd, count))
- # print(sorted_cmd_count)
- # cmds_to_graph = list(map(lambda x: x[0], sorted_cmd_count))[:cmd_count]
-
-
-def print_avg_cmdline_length():
- cmd_len_total = 0
- cmd_total = 0
- for pid, session in DATA_records_by_session.items():
- for record in session:
- cmd = record["cmdLine"]
- if cmd == "":
- continue
- cmd_len_total += len(cmd)
- cmd_total += 1
-
- print("% ALL avg cmdline = {}".format(cmd_len_total / cmd_total))
- # print(sorted_cmd_count)
- # cmds_to_graph = list(map(lambda x: x[0], sorted_cmd_count))[:cmd_count]
-
-
-# plot_cmdLineFrq_rank()
-# plot_cmdFrq_rank()
-print_top_cmds(30)
-print_top_cmds_by_user(30)
-# print_avg_cmdline_length()
-#
-# plot_cmdLineVocabularySize_cmdLinesEntered()
-plot_cmdVocabularySize_cmdLinesEntered()
-plot_cmdVocabularySize_time()
-# plot_cmdVocabularySize_daily()
-plot_cmdUsage_in_time(num_cmds=100)
-plot_cmdUsage_in_time(sort_cmds=True, num_cmds=100)
-#
-recent_strats=("recent", "recent (bash-like)")
-recurrence_strat=("recent (bash-like)",)
-# plot_strategies_matches(20)
-# plot_strategies_charsRecalled(20)
-# plot_strategies_charsRecalled_prefix(20)
-# plot_strategies_charsRecalled_noncummulative(20, selected_strategies=recent_strats)
-# plot_strategies_matches_noncummulative(20)
-# plot_strategies_charsRecalled_noncummulative(20)
-# plot_strategies_charsRecalled_prefix_noncummulative(20)
-# plot_strategies_matches(20, selected_strategies=recurrence_strat, show_strat_title=True, force_strat_title="recurrence rate")
-# plot_strategies_matches_noncummulative(20, selected_strategies=recurrence_strat, show_strat_title=True, force_strat_title="recurrence rate")
-
-# graph_cmdSequences(node_count=33, edge_minValue=0.048)
-
-# graph_cmdSequences(node_count=28, edge_minValue=0.06)
-
-# new improved
-# for n in range(40, 43):
-# for e in range(94, 106, 2):
-# e *= 0.001
-# graph_cmdSequences(node_count=n, edge_minValue=e, view_graph=False)
-
-#for n in range(29, 35):
-# for e in range(44, 56, 2):
-# e *= 0.001
-# graph_cmdSequences(node_count=n, edge_minValue=e, view_graph=False)
-
-# be careful and check if labels fit the display
-
-if async_draw:
- plt.show()
diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh
deleted file mode 100644
index db021470..00000000
--- a/scripts/reshctl.sh
+++ /dev/null
@@ -1,155 +0,0 @@
-
-# shellcheck source=../submodules/bash-zsh-compat-widgets/bindfunc.sh
-. ~/.resh/bindfunc.sh
-# shellcheck source=widgets.sh
-. ~/.resh/widgets.sh
-
-__resh_nop() {
- # does nothing
- true
-}
-
-__resh_bind_control_R() {
- bindfunc --revert '\C-r' __resh_widget_control_R_compat
- if [ "${__RESH_control_R_bind_enabled-0}" != 0 ]; then
- # Re-binding is a valid usecase but it shouldn't happen much
- # so this is a warning
- echo "Re-binding RESH SEARCH app to Ctrl+R ..."
- else
- # Only save original binding if resh binding was not enabled
- __RESH_bindfunc_revert_control_R_bind=$_bindfunc_revert
- fi
- __RESH_control_R_bind_enabled=1
- if [ -n "${BASH_VERSION-}" ]; then
- # fuck bash
- bind '"\C-r": "\u[31~\u[32~"'
- bind -x '"\u[31~": __resh_widget_control_R_compat'
-
- # execute
- # bind '"\u[32~": accept-line'
-
- # just paste
- # bind -x '"\u[32~": __resh_nop'
- true
- fi
- return 0
-}
-
-__resh_unbind_control_R() {
- if [ "${__RESH_control_R_bind_enabled-0}" != 1 ]; then
- echo "RESH SEARCH app Ctrl+R binding is already disabled!"
- return 1
- fi
- if [ -z "${__RESH_bindfunc_revert_control_R_bind+x}" ]; then
- echo "Warn: Couldn't revert Ctrl+R binding because 'revert command' is empty."
- else
- eval "$__RESH_bindfunc_revert_control_R_bind"
- fi
- __RESH_control_R_bind_enabled=0
- return 0
-}
-
-__resh_bind_all() {
- __resh_bind_control_R
-}
-
-__resh_unbind_all() {
- __resh_unbind_control_R
-}
-
-# wrapper for resh-cli for calling resh directly
-resh() {
- local buffer
- local git_remote; git_remote="$(git remote get-url origin 2>/dev/null)"
- buffer=$(resh-cli --sessionID "$__RESH_SESSION_ID" --host "$__RESH_HOST" --pwd "$PWD" --gitOriginRemote "$git_remote" "$@")
- status_code=$?
- if [ $status_code = 111 ]; then
- # execute
- echo "$buffer"
- eval "$buffer"
- elif [ $status_code = 0 ]; then
- # paste
- echo "$buffer"
- elif [ $status_code = 130 ]; then
- true
- else
- local fpath_last_run="$__RESH_XDG_CACHE_HOME/cli_last_run_out.txt"
- echo "$buffer" >| "$fpath_last_run"
- echo "resh-cli failed - check '$fpath_last_run' and '~/.resh/cli.log'"
- fi
-}
-
-reshctl() {
- # 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
- # 30)
- # # enable all
- # __resh_bind_all
- # return 0
- # ;;
- 32)
- # enable control R
- __resh_bind_control_R
- return 0
- ;;
- # disable
- # 40)
- # # disable all
- # __resh_unbind_all
- # return 0
- # ;;
- 42)
- # disable control R
- __resh_unbind_control_R
- return 0
- ;;
- 50)
- # reload rc files
- . ~/.resh/shellrc
- return 0
- ;;
- 51)
- # inspect session history
- # reshctl debug inspect N
- resh-inspect --sessionID "$__RESH_SESSION_ID" --count "${3-10}"
- return 0
- ;;
- 52)
- # show status
- echo
- echo 'Control R binding ...'
- if [ "$(resh-config --key BindControlR)" = true ]; then
- echo ' * future sessions: ENABLED'
- else
- echo ' * future sessions: DISABLED'
- fi
- if [ "${__RESH_control_R_bind_enabled-0}" != 0 ]; then
- echo ' * this session: ENABLED'
- else
- echo ' * this session: DISABLED'
- fi
- return 0
- ;;
- *)
- echo "reshctl() FATAL ERROR: unknown status ($_status)" >&2
- echo "Possibly caused by version mismatch between installed resh and resh in this session." >&2
- echo "Please REPORT this issue here: https://github.com/curusarn/resh/issues" >&2
- echo "Please RESTART your terminal window." >&2
- return "$_status"
- ;;
- esac
-}
diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh
index c21f3358..b6a02984 100644
--- a/scripts/shellrc.sh
+++ b/scripts/shellrc.sh
@@ -1,96 +1,42 @@
+#!/hint/sh
PATH=$PATH:~/.resh/bin
-# if [ -n "$ZSH_VERSION" ]; then
-# zmodload zsh/datetime
-# fi
# shellcheck source=hooks.sh
. ~/.resh/hooks.sh
-# shellcheck source=util.sh
-. ~/.resh/util.sh
-# shellcheck source=reshctl.sh
-. ~/.resh/reshctl.sh
-
-__RESH_MACOS=0
-__RESH_LINUX=0
-__RESH_UNAME=$(uname)
-
-if [ "$__RESH_UNAME" = "Darwin" ]; then
- __RESH_MACOS=1
-elif [ "$__RESH_UNAME" = "Linux" ]; then
- __RESH_LINUX=1
-else
- echo "resh PANIC unrecognized OS"
-fi
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
__RESH_SHELL="bash"
- __RESH_HOST="$HOSTNAME"
- __RESH_HOSTTYPE="$HOSTTYPE"
- __resh_bash_completion_init
else
- echo "resh PANIC unrecognized shell"
-fi
-
-# posix
-__RESH_HOME="$HOME"
-__RESH_LOGIN="$LOGNAME"
-__RESH_SHELL_ENV="$SHELL"
-__RESH_TERM="$TERM"
-
-# non-posix
-__RESH_RT_SESSION=$(__resh_get_epochrealtime)
-__RESH_OSTYPE="$OSTYPE"
-__RESH_MACHTYPE="$MACHTYPE"
-
-if [ $__RESH_LINUX -eq 1 ]; then
- __RESH_OS_RELEASE_ID=$(. /etc/os-release; echo "$ID")
- __RESH_OS_RELEASE_VERSION_ID=$(. /etc/os-release; echo "$VERSION_ID")
- __RESH_OS_RELEASE_ID_LIKE=$(. /etc/os-release; echo "$ID_LIKE")
- __RESH_OS_RELEASE_NAME=$(. /etc/os-release; echo "$NAME")
- __RESH_OS_RELEASE_PRETTY_NAME=$(. /etc/os-release; echo "$PRETTY_NAME")
- __RESH_RT_SESS_SINCE_BOOT=$(cut -d' ' -f1 /proc/uptime)
-elif [ $__RESH_MACOS -eq 1 ]; then
- __RESH_OS_RELEASE_ID="macos"
- __RESH_OS_RELEASE_VERSION_ID=$(sw_vers -productVersion 2>/dev/null)
- __RESH_OS_RELEASE_NAME="macOS"
- __RESH_OS_RELEASE_PRETTY_NAME="Mac OS X"
- __RESH_RT_SESS_SINCE_BOOT=$(sysctl -n kern.boottime | awk '{print $4}' | sed 's/,//g')
+ echo "RESH PANIC: unrecognized shell - please report this to https://github.com/curusarn/resh/issues"
fi
# shellcheck disable=2155
export __RESH_VERSION=$(resh-collect -version)
-# shellcheck disable=2155
-export __RESH_REVISION=$(resh-collect -revision)
-__resh_set_xdg_home_paths
-
-__resh_run_daemon
+resh-daemon-start -q
[ "$(resh-config --key BindControlR)" = true ] && __resh_bind_control_R
# block for anything we only want to do once per session
# NOTE: nested shells are still the same session
+# i.e. $__RESH_SESSION_ID will be set in nested shells
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
+ # shellcheck disable=2155
+ export __RESH_SESSION_ID=$(resh-generate-uuid)
+
__resh_session_init
fi
# block for anything we only want to do once per shell
+# NOTE: nested shells are new shells
+# i.e. $__RESH_INIT_DONE will NOT be set in nested shells
if [ -z "${__RESH_INIT_DONE+x}" ]; then
preexec_functions+=(__resh_preexec)
precmd_functions+=(__resh_precmd)
- __resh_reset_variables
-
__RESH_INIT_DONE=1
fi
\ No newline at end of file
diff --git a/scripts/test.sh b/scripts/test.sh
index 6c23446b..942e9ae1 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -1,20 +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=sh --severity=error || exit 1
done
-for f in scripts/{shellrc,util,reshctl,hooks}.sh; do
+for f in scripts/{shellrc,hooks}.sh; do
echo "Checking Zsh syntax of $f ..."
! zsh -n "$f" && echo "Zsh syntax check failed!" && exit 1
done
-if [ "$1" == "--all" ]; then
+if [ "$1" = "--all" ]; then
for sh in bash zsh; do
echo "Running functions in scripts/shellrc.sh using $sh ..."
! $sh -c ". scripts/shellrc.sh; __resh_preexec; __resh_precmd" && echo "Error while running functions!" && exit 1
diff --git a/scripts/util.sh b/scripts/util.sh
deleted file mode 100644
index 4746eb03..00000000
--- a/scripts/util.sh
+++ /dev/null
@@ -1,197 +0,0 @@
-# util.sh - resh utility functions
-__resh_get_uuid() {
- cat /proc/sys/kernel/random/uuid 2>/dev/null || resh-uuid
-}
-
-__resh_get_pid() {
- if [ -n "${ZSH_VERSION-}" ]; then
- # assume Zsh
- local __RESH_PID="$$" # current pid
- elif [ -n "${BASH_VERSION-}" ]; then
- # assume Bash
- if [ "${BASH_VERSINFO[0]}" -ge "4" ]; then
- # $BASHPID is only available in bash4+
- # $$ is fairly similar so it should not be an issue
- local __RESH_PID="$BASHPID" # current pid
- else
- local __RESH_PID="$$" # current pid
- fi
- fi
- echo "$__RESH_PID"
-}
-
-__resh_get_epochrealtime() {
- if date +%s.%N | grep -vq 'N'; then
- # GNU date
- date +%s.%N
- elif gdate --version >/dev/null && gdate +%s.%N | grep -vq 'N'; then
- # GNU date take 2
- gdate +%s.%N
- elif [ -n "${ZSH_VERSION-}" ]; then
- # zsh fallback using $EPOCHREALTIME
- if [ -z "${__RESH_ZSH_LOADED_DATETIME+x}" ]; then
- zmodload zsh/datetime
- __RESH_ZSH_LOADED_DATETIME=1
- fi
- echo "$EPOCHREALTIME"
- else
- # dumb date
- # XXX: we lost precison beyond seconds
- date +%s
- if [ -z "${__RESH_DATE_WARN+x}" ]; then
- echo "resh WARN: can't get precise time - consider installing GNU date!"
- __RESH_DATE_WARN=1
- fi
- fi
-}
-
-__resh_run_daemon() {
- if [ -n "${ZSH_VERSION-}" ]; then
- setopt LOCAL_OPTIONS NO_NOTIFY NO_MONITOR
- fi
- local fpath_last_run="$__RESH_XDG_CACHE_HOME/daemon_last_run_out.txt"
- if [ "$(uname)" = Darwin ]; then
- # hotfix
- gnohup resh-daemon >| "$fpath_last_run" 2>&1 & disown
- else
- # TODO: switch to nohup for consistency once you confirm that daemon is
- # not getting killed anymore on macOS
- # nohup resh-daemon >| "$fpath_last_run" 2>&1 & disown
- setsid resh-daemon >| "$fpath_last_run" 2>&1 & disown
- fi
-}
-
-__resh_bash_completion_init() {
- # primitive check to find out if bash_completions are installed
- # skip completion init if they are not
- _get_comp_words_by_ref >/dev/null 2>/dev/null
- [[ $? == 127 ]] && return
- local bash_completion_dir=~/.resh/bash_completion.d
- if [[ -d $bash_completion_dir && -r $bash_completion_dir && \
- -x $bash_completion_dir ]]; then
- for i in $(LC_ALL=C command ls "$bash_completion_dir"); do
- i=$bash_completion_dir/$i
- # shellcheck disable=SC2154
- # shellcheck source=/dev/null
- [[ -f "$i" && -r "$i" ]] && . "$i"
- done
- fi
-}
-
-__resh_zsh_completion_init() {
- # 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
- if typeset -f compdef >/dev/null 2>&1; then
- source ~/.resh/zsh_completion.d/_reshctl && compdef _reshctl reshctl
- else
- # fallback I guess
- fpath=(~/.resh/zsh_completion.d $fpath)
- __RESH_zsh_no_compdef=1
- fi
-
- # 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() {
- # posix
- local __RESH_COLS="$COLUMNS"
- local __RESH_LANG="$LANG"
- local __RESH_LC_ALL="$LC_ALL"
- # other LC ?
- local __RESH_LINES="$LINES"
- local __RESH_PWD="$PWD"
-
- # non-posix
- local __RESH_SHLVL="$SHLVL"
-
- # pid
- local __RESH_PID; __RESH_PID=$(__resh_get_pid)
-
- # time
- local __RESH_TZ_BEFORE; __RESH_TZ_BEFORE=$(date +%z)
- local __RESH_RT_BEFORE; __RESH_RT_BEFORE=$(__resh_get_epochrealtime)
-
- if [ "$__RESH_VERSION" != "$(resh-session-init -version)" ]; then
- # shellcheck source=shellrc.sh
- source ~/.resh/shellrc
- if [ "$__RESH_VERSION" != "$(resh-session-init -version)" ]; then
- echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh version: $(resh-session-init -version); resh version of this terminal session: ${__RESH_VERSION})"
- else
- echo "RESH INFO: New RESH shellrc script was loaded - if you encounter any issues please restart this terminal session."
- fi
- elif [ "$__RESH_REVISION" != "$(resh-session-init -revision)" ]; then
- # shellcheck source=shellrc.sh
- source ~/.resh/shellrc
- if [ "$__RESH_REVISION" != "$(resh-session-init -revision)" ]; then
- echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh revision: $(resh-session-init -revision); resh revision of this terminal session: ${__RESH_REVISION})"
- fi
- fi
- if [ "$__RESH_VERSION" = "$(resh-session-init -version)" ] && [ "$__RESH_REVISION" = "$(resh-session-init -revision)" ]; then
- local fpath_last_run="$__RESH_XDG_CACHE_HOME/session_init_last_run_out.txt"
- resh-session-init -requireVersion "$__RESH_VERSION" \
- -requireRevision "$__RESH_REVISION" \
- -shell "$__RESH_SHELL" \
- -uname "$__RESH_UNAME" \
- -sessionId "$__RESH_SESSION_ID" \
- -cols "$__RESH_COLS" \
- -home "$__RESH_HOME" \
- -lang "$__RESH_LANG" \
- -lcAll "$__RESH_LC_ALL" \
- -lines "$__RESH_LINES" \
- -login "$__RESH_LOGIN" \
- -shellEnv "$__RESH_SHELL_ENV" \
- -term "$__RESH_TERM" \
- -pid "$__RESH_PID" \
- -sessionPid "$__RESH_SESSION_PID" \
- -host "$__RESH_HOST" \
- -hosttype "$__RESH_HOSTTYPE" \
- -ostype "$__RESH_OSTYPE" \
- -machtype "$__RESH_MACHTYPE" \
- -shlvl "$__RESH_SHLVL" \
- -realtimeBefore "$__RESH_RT_BEFORE" \
- -realtimeSession "$__RESH_RT_SESSION" \
- -realtimeSessSinceBoot "$__RESH_RT_SESS_SINCE_BOOT" \
- -timezoneBefore "$__RESH_TZ_BEFORE" \
- -osReleaseId "$__RESH_OS_RELEASE_ID" \
- -osReleaseVersionId "$__RESH_OS_RELEASE_VERSION_ID" \
- -osReleaseIdLike "$__RESH_OS_RELEASE_ID_LIKE" \
- -osReleaseName "$__RESH_OS_RELEASE_NAME" \
- -osReleasePrettyName "$__RESH_OS_RELEASE_PRETTY_NAME" \
- >| "$fpath_last_run" 2>&1 || echo "resh-session-init ERROR: $(head -n 1 $fpath_last_run)"
- fi
-}
-
-__resh_set_xdg_home_paths() {
- if [ -z "${XDG_CONFIG_HOME-}" ]; then
- __RESH_XDG_CONFIG_FILE="$HOME/.config"
- else
- __RESH_XDG_CONFIG_FILE="$XDG_CONFIG_HOME"
- fi
- mkdir -p "$__RESH_XDG_CONFIG_FILE" >/dev/null 2>/dev/null
- __RESH_XDG_CONFIG_FILE="$__RESH_XDG_CONFIG_FILE/resh.toml"
-
-
- if [ -z "${XDG_CACHE_HOME-}" ]; then
- __RESH_XDG_CACHE_HOME="$HOME/.cache/resh"
- else
- __RESH_XDG_CACHE_HOME="$XDG_CACHE_HOME/resh"
- fi
- mkdir -p "$__RESH_XDG_CACHE_HOME" >/dev/null 2>/dev/null
- export __RESH_XDG_CACHE_HOME
-
-
- if [ -z "${XDG_DATA_HOME-}" ]; then
- __RESH_XDG_DATA_HOME="$HOME/.local/share/resh"
- else
- __RESH_XDG_DATA_HOME="$XDG_DATA_HOME/resh"
- fi
- mkdir -p "$__RESH_XDG_DATA_HOME" >/dev/null 2>/dev/null
-}
diff --git a/scripts/uuid.sh b/scripts/uuid.sh
deleted file mode 100755
index 8b4527b9..00000000
--- a/scripts/uuid.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env bash
-# https://gist.github.com/markusfisch/6110640
-
-# Generate a pseudo UUID
-uuid()
-{
- local N B C='89ab'
-
- for (( N=0; N < 16; ++N ))
- do
- B=$(( $RANDOM%256 ))
-
- case $N in
- 6)
- printf '4%x' $(( B%16 ))
- ;;
- 8)
- printf '%c%x' ${C:$RANDOM%${#C}:1} $(( B%16 ))
- ;;
- 3 | 5 | 7 | 9)
- printf '%02x-' $B
- ;;
- *)
- printf '%02x' $B
- ;;
- esac
- done
-
- echo
-}
-
-if [ "$BASH_SOURCE" == "$0" ]
-then
- uuid
-fi
diff --git a/scripts/widgets.sh b/scripts/widgets.sh
deleted file mode 100644
index defc6059..00000000
--- a/scripts/widgets.sh
+++ /dev/null
@@ -1,51 +0,0 @@
-
-# shellcheck source=hooks.sh
-. ~/.resh/hooks.sh
-
-__resh_widget_control_R() {
- # this is a very bad workaround
- # force bash-preexec to run repeatedly because otherwise premature run of bash-preexec overshadows the next poper run
- # I honestly think that it's impossible to make widgets work in bash without hacks like this
- # shellcheck disable=2034
- __bp_preexec_interactive_mode="on"
-
- # local __RESH_PREFIX=${BUFFER:0:CURSOR}
- # __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;control_R:$__RESH_PREFIX"
- local PREVBUFFER=$BUFFER
- __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS|||control_R:$BUFFER"
-
- local status_code
- local git_remote; git_remote="$(git remote get-url origin 2>/dev/null)"
- BUFFER=$(resh-cli --sessionID "$__RESH_SESSION_ID" --host "$__RESH_HOST" --pwd "$PWD" --gitOriginRemote "$git_remote" --query "$BUFFER")
- status_code=$?
- local fpath_last_run="$__RESH_XDG_CACHE_HOME/cli_last_run_out.txt"
- touch "$fpath_last_run"
- if [ $status_code = 111 ]; then
- # execute
- if [ -n "${ZSH_VERSION-}" ]; then
- # zsh
- zle accept-line
- elif [ -n "${BASH_VERSION-}" ]; then
- # bash
- # set chained keyseq to accept-line
- bind '"\u[32~": accept-line'
- fi
- elif [ $status_code = 0 ]; then
- if [ -n "${BASH_VERSION-}" ]; then
- # bash
- # set chained keyseq to nothing
- bind -x '"\u[32~": __resh_nop'
- fi
- else
- echo "$BUFFER" >| "$fpath_last_run"
- echo "# RESH SEARCH APP failed - sorry for the inconvinience - check '$fpath_last_run' and '~/.resh/cli.log'"
- BUFFER="$PREVBUFFER"
- fi
- CURSOR=${#BUFFER}
- # recorded to history
- __RESH_HIST_PREV_LINE=${BUFFER}
-}
-
-__resh_widget_control_R_compat() {
- __bindfunc_compat_wrapper __resh_widget_control_R
-}
diff --git a/test/Dockefile_macoslike b/test/Dockefile_macoslike
deleted file mode 100644
index 6e8c524f..00000000
--- a/test/Dockefile_macoslike
+++ /dev/null
@@ -1,12 +0,0 @@
-FROM debian:jessie
-
-RUN apt-get update && apt-get install -y wget tar gcc automake make bison flex curl vim makeinfo
-RUN wget https://ftp.gnu.org/gnu/bash/bash-3.2.57.tar.gz && \
- tar -xvzf bash-3.2.57.tar.gz && \
- cd bash-3.2.57 && \
- ./configure && \
- make && \
- make install
-RUN echo /usr/local/bin/bash >> /etc/shells && chsh -s /usr/local/bin/bash
-
-CMD bash
diff --git a/troubleshooting.md b/troubleshooting.md
new file mode 100644
index 00000000..60a45de0
--- /dev/null
+++ b/troubleshooting.md
@@ -0,0 +1,72 @@
+# Troubleshooting
+
+## First help
+
+Run RESH doctor to detect common issues:
+```sh
+reshctl doctor
+```
+
+## Restarting RESH daemon
+
+Sometimes restarting RESH daemon can help:
+```sh
+resh-daemon-restart
+```
+
+You can also start and stop RESH daemon with:
+```sh
+resh-daemon-start
+resh-daemon-stop
+```
+
+:warning: You will get error messages in your shell when RESH daemon is not running.
+
+## Recorded history
+
+Your RESH history is saved in one of:
+- `~/.local/share/resh/history.reshjson`
+- `$XDG_DATA_HOME/resh/history.reshjson`
+
+The format is JSON prefixed by version. Display it as json using:
+
+```sh
+cat ~/.local/share/resh/history.reshjson | sed 's/^v[^{]*{/{/' | jq .
+```
+
+ℹ️ You will need `jq` installed.
+
+## Configuration
+
+RESH config is read from one of:
+- `~/.config/resh.toml`
+- `$XDG_CONFIG_HOME/resh.toml`
+
+## Logs
+
+Logs can be useful for troubleshooting issues.
+
+Find RESH logs in one of:
+- `~/.local/share//resh/log.json`
+- `$XDG_DATA_HOME/resh/log.json`
+
+### Log verbosity
+
+Get more detailed logs by setting `LogLevel = "debug"` in [RESH config](#configuration).
+Restart RESH daemon for the config change to take effect: `resh-daemon-restart`
+
+## Common problems
+
+### Using RESH with bash on macOS
+
+ℹ️ It is recommended to use zsh on macOS.
+
+MacOS comes with really old bash (`bash 3.2`).
+Update it using: `brew install bash`
+
+On macOS, bash shell does not load `~/.bashrc` because every shell runs as login shell.
+Fix it by running: `echo '[ -f ~/.bashrc ] && . ~/.bashrc' >> ~/.bash_profile`
+
+## GitHub issues
+
+Problem persists? [Create an issue ⇗](https://github.com/curusarn/resh/issues)