diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c875707..a6c2cf3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,6 +16,6 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: - version: v1.46.2 + version: v1.51.2 args: -c .golang-ci.yml diff --git a/.golang-ci.yml b/.golang-ci.yml index 0668433..283529f 100644 --- a/.golang-ci.yml +++ b/.golang-ci.yml @@ -19,3 +19,13 @@ linters: - predeclared - nilnil - varnamelen + - nosnakecase + - varcheck + - interfacer + - structcheck + - golint + - scopelint + - deadcode + - ifshort + - rowserrcheck + - wastedassign diff --git a/.goreleaser.yml b/.goreleaser.yml index ba58b09..2c8ba4e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -17,12 +17,14 @@ builds: ldflags: - -s -w -X main.version={{.Version}} archives: -- replacements: - darwin: Darwin - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 + - id: replace + name_template: >- + {{- .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end -}} checksum: name_template: 'checksums.txt' snapshot: diff --git a/README.md b/README.md index 4459df2..91f831a 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ This fork allows to use viddy to show output data in different juju commands. | key | | |-----------|--------------------------------------------| | SPACE | Toggle time machine mode | -| s | Toggle suspend execution | -| b | Toggle ring terminal bell | -| d | Toggle diff | -| t | Toggle header display | +| s | Toggle suspend execution | +| b | Toggle ring terminal bell | +| d | Toggle diff | +| t | Toggle header/title display | | ? or h | Toggle help view | | / | Search text | | j | Pager: next line | @@ -48,6 +48,28 @@ This fork allows to use viddy to show output data in different juju commands. | Shift-O | (Time machine mode) Go to oldest position | | Shift-N | (Time machine mode) Go to current position | +## Configuration + +Install your config file on `$XDG_CONFIG_HOME/viddy.toml` +On macOS, the path is `~/Library/Application\ Support/viddy.toml`. + +```toml +[general] +no_shell = false +shell = "zsh" +shell_options = "" + +[keymap] +timemachine_go_to_past = "Down" +timemachine_go_to_more_past = "Shift-Down" +timemachine_go_to_future = "Up" +timemachine_go_to_more_future = "Shift-Up" +timemachine_go_to_now = "Ctrl-Shift-Up" +timemachine_go_to_oldest = "Ctrl-Shift-Down" + +[color] +background = "white" # Default value is inherit from terminal color. +``` ## What is "viddy" ? diff --git a/config.go b/config.go index 6677bb4..fb623b5 100644 --- a/config.go +++ b/config.go @@ -38,14 +38,16 @@ type runtimeConfig struct { } type general struct { - shell string - shellOptions string - debug bool - bell bool - differences bool - noTitle bool - pty bool - unfold bool + noShell bool + shell string + shellOptions string + debug bool + bell bool + differences bool + skipEmptyDiffs bool + noTitle bool + pty bool + unfold bool } type theme struct { @@ -87,8 +89,10 @@ func NewConfig(v *viper.Viper, args []string) (*config, error) { // general flagSet.BoolP("bell", "b", false, "ring terminal bell changes between updates") flagSet.BoolP("differences", "d", false, "highlight changes between updates") + flagSet.BoolP("skip-empty-diffs", "s", false, "skip snapshots with no changes (+0 -0) in history") flagSet.BoolP("no-title", "t", false, "turn off header") flagSet.Bool("debug", false, "") + flagSet.Bool("no-shell", false, "do not use a shell even if --shell is set") flagSet.String("shell", "", "shell (default \"sh\")") flagSet.String("shell-options", "", "additional shell options") flagSet.Bool("unfold", false, "unfold") @@ -130,6 +134,10 @@ func NewConfig(v *viper.Viper, args []string) (*config, error) { return nil, err } + if err := v.BindPFlag("general.no_shell", flagSet.Lookup("no-shell")); err != nil { + return nil, err + } + if err := v.BindPFlag("general.shell", flagSet.Lookup("shell")); err != nil { return nil, err } @@ -148,6 +156,10 @@ func NewConfig(v *viper.Viper, args []string) (*config, error) { return nil, err } + if err := v.BindPFlag("general.skip_empty_diffs", flagSet.Lookup("skip-empty-diffs")); err != nil { + return nil, err + } + if err := v.BindPFlag("general.no_title", flagSet.Lookup("no-title")); err != nil { return nil, err } @@ -161,10 +173,12 @@ func NewConfig(v *viper.Viper, args []string) (*config, error) { } conf.general.debug = v.GetBool("general.debug") + conf.general.noShell = v.GetBool("general.no_shell") conf.general.shell = v.GetString("general.shell") conf.general.shellOptions = v.GetString("general.shell_options") conf.general.bell, _ = flagSet.GetBool("bell") conf.general.differences, _ = flagSet.GetBool("differences") + conf.general.skipEmptyDiffs, _ = flagSet.GetBool("skip-empty-diffs") conf.general.noTitle, _ = flagSet.GetBool("no-title") conf.general.unfold = v.GetBool("general.unfold") conf.general.pty = v.GetBool("general.pty") @@ -295,7 +309,7 @@ type parseKeyStrokeError struct { } func (e parseKeyStrokeError) Error() string { - return fmt.Sprintf("connot parse key: %q", e.key) + return fmt.Sprintf("cannot parse key: %q", e.key) } // ParseKeyStroke parse string describing key. diff --git a/generator.go b/generator.go index 5c35040..99cbcfb 100644 --- a/generator.go +++ b/generator.go @@ -4,8 +4,10 @@ import "time" type newSnapFunc func(int64, *Snapshot, chan<- struct{}) *Snapshot -func ClockSnapshot(begin int64, newSnap newSnapFunc, interval time.Duration) <-chan *Snapshot { +func ClockSnapshot(begin int64, newSnap newSnapFunc, interval time.Duration) (<-chan *Snapshot, chan<- bool) { c := make(chan *Snapshot) + isSuspended := false + isSuspendedQueue := make(chan bool) go func() { var s *Snapshot @@ -13,6 +15,15 @@ func ClockSnapshot(begin int64, newSnap newSnapFunc, interval time.Duration) <-c t := time.Tick(interval) for now := range t { + select { + case isSuspended = <-isSuspendedQueue: + default: + } + + if isSuspended { + continue + } + finish := make(chan struct{}) id := (now.UnixNano() - begin) / int64(time.Millisecond) s = newSnap(id, s, finish) @@ -20,11 +31,13 @@ func ClockSnapshot(begin int64, newSnap newSnapFunc, interval time.Duration) <-c } }() - return c + return c, isSuspendedQueue } -func PreciseSnapshot(newSnap newSnapFunc, interval time.Duration) <-chan *Snapshot { +func PreciseSnapshot(newSnap newSnapFunc, interval time.Duration) (<-chan *Snapshot, chan<- bool) { c := make(chan *Snapshot) + isSuspended := false + isSuspendedQueue := make(chan bool) go func() { var s *Snapshot @@ -32,6 +45,17 @@ func PreciseSnapshot(newSnap newSnapFunc, interval time.Duration) <-chan *Snapsh begin := time.Now().UnixNano() for { + select { + case isSuspended = <-isSuspendedQueue: + default: + } + + if isSuspended { + time.Sleep(interval) + + continue + } + finish := make(chan struct{}) start := time.Now() id := (start.UnixNano() - begin) / int64(time.Millisecond) @@ -52,11 +76,13 @@ func PreciseSnapshot(newSnap newSnapFunc, interval time.Duration) <-chan *Snapsh } }() - return c + return c, isSuspendedQueue } -func SequentialSnapshot(newSnap newSnapFunc, interval time.Duration) <-chan *Snapshot { +func SequentialSnapshot(newSnap newSnapFunc, interval time.Duration) (<-chan *Snapshot, chan<- bool) { c := make(chan *Snapshot) + isSuspended := false + isSuspendedQueue := make(chan bool) go func() { var s *Snapshot @@ -64,6 +90,17 @@ func SequentialSnapshot(newSnap newSnapFunc, interval time.Duration) <-chan *Sna begin := time.Now().UnixNano() for { + select { + case isSuspended = <-isSuspendedQueue: + default: + } + + if isSuspended { + time.Sleep(interval) + + continue + } + finish := make(chan struct{}) id := (time.Now().UnixNano() - begin) / int64(time.Millisecond) s = newSnap(id, s, finish) @@ -75,5 +112,5 @@ func SequentialSnapshot(newSnap newSnapFunc, interval time.Duration) <-chan *Sna } }() - return c + return c, isSuspendedQueue } diff --git a/go.mod b/go.mod index 006c25b..74f5fbc 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/creack/pty v1.1.15 github.com/fatih/color v1.12.0 github.com/gdamore/tcell/v2 v2.5.1 + github.com/mattn/go-runewidth v0.0.13 github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500 github.com/sergi/go-diff v1.2.0 @@ -30,7 +31,6 @@ require ( github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.12 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/pelletier/go-toml v1.9.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -38,11 +38,11 @@ require ( github.com/spf13/afero v1.6.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect - golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect - golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect - golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index 3b66119..7b019c9 100644 --- a/go.sum +++ b/go.sum @@ -80,9 +80,6 @@ github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWp github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell/v2 v2.4.1-0.20210904044819-ae5116d72813 h1:uqlt4EHPdtAAXKBq6OKc0auHsQP5zfhdHo/BYe6hz2Q= -github.com/gdamore/tcell/v2 v2.4.1-0.20210904044819-ae5116d72813/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= -github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM= github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I= github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= @@ -237,8 +234,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb 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/tview v0.0.0-20211202162923-2a6de950f73b h1:EMgbQ+bOHWkl0Ptano8M0yrzVZkxans+Vfv7ox/EtO8= -github.com/rivo/tview v0.0.0-20211202162923-2a6de950f73b/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500 h1:KvoRB2TMfMqK2NF2mIvZprDT/Ofvsa4RphWLoCmUDag= github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= @@ -246,8 +241,6 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ 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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sachaos/tview v0.0.0-20210909084047-7f6f0b84f61c h1:OYO8SfrdARTchNFYwtSpQf1pY7FeZRY85PuOh0akRmQ= -github.com/sachaos/tview v0.0.0-20210909084047-7f6f0b84f61c/go.mod h1:NpihEwB0BMEMry10psTJveTy9b5l3GMVwBuVpKR3Jo0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -374,8 +367,9 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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= @@ -444,17 +438,14 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810 h1:rHZQSjJdAI4Xf5Qzeh2bBc5YJIkPFVM6oDtMFYmgws0= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.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-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= -golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 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= @@ -463,8 +454,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.6/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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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= @@ -640,8 +632,9 @@ 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.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e69de29 diff --git a/snapshot.go b/snapshot.go index d21993e..9683c70 100644 --- a/snapshot.go +++ b/snapshot.go @@ -24,6 +24,7 @@ type Snapshot struct { command string args []string + noShell bool shell string shellOpts string @@ -50,12 +51,13 @@ type Snapshot struct { // NewSnapshot returns Snapshot object. // //nolint:lll -func NewSnapshot(id int64, command string, args []string, shell string, shellOpts string, before *Snapshot, finish chan<- struct{}) *Snapshot { +func NewSnapshot(id int64, command string, args []string, noShell bool, shell string, shellOpts string, before *Snapshot, finish chan<- struct{}) *Snapshot { return &Snapshot{ id: id, command: command, args: args, + noShell: noShell, shell: shell, shellOpts: shellOpts, @@ -97,22 +99,25 @@ func (s *Snapshot) compareFromBefore() error { return nil } +//nolint:gosec func (s *Snapshot) prepareCommand(commands []string) *exec.Cmd { - var command *exec.Cmd - if runtime.GOOS == "windows" { cmdStr := strings.Join(commands, " ") compSec := os.Getenv("COMSPEC") - command = exec.Command(compSec, "/c", cmdStr) - } else { - var args []string - args = append(args, strings.Fields(s.shellOpts)...) - args = append(args, "-c") - args = append(args, strings.Join(commands, " ")) - command = exec.Command(s.shell, args...) //nolint:gosec + + return exec.Command(compSec, "/c", cmdStr) } - return command + if s.noShell { + return exec.Command(commands[0], commands[1:]...) + } + + var args []string + args = append(args, strings.Fields(s.shellOpts)...) + args = append(args, "-c") + args = append(args, strings.Join(commands, " ")) + + return exec.Command(s.shell, args...) } func isWhiteString(str string) bool { @@ -169,7 +174,7 @@ func DiffPrettyText(diffs []diffmatchpatch.Diff) string { if unicode.IsSpace(c) { _, _ = buff.WriteRune(c) } else { - _, _ = buff.WriteString(color.New(color.BgGreen).Sprintf(string(c))) + _, _ = buff.WriteString(color.New(color.FgBlack).Sprintf(color.New(color.BgGreen).Sprintf(string(c)))) } } case diffmatchpatch.DiffEqual: diff --git a/viddy.go b/viddy.go index 0d67806..2ab5e83 100644 --- a/viddy.go +++ b/viddy.go @@ -9,7 +9,6 @@ import ( "html/template" "io" "os" - "sort" "strconv" "strings" "sync" @@ -45,12 +44,12 @@ type Viddy struct { timeView *tview.TextView historyView *tview.Table historyRows map[int64]*HistoryRow - sync.RWMutex // bWidth store current pty width. bWidth atomic.Value - idList []int64 + // id -> row count (as of just after the snapshot was added). + historyRowCount map[int64]int bodyView *tview.TextView app *tview.Application @@ -59,10 +58,11 @@ type Viddy struct { statusView *tview.TextView queryEditor *tview.InputField - snapshotQueue <-chan *Snapshot - queue chan int64 - finishedQueue chan int64 - diffQueue chan int64 + snapshotQueue <-chan *Snapshot + isSuspendedQueue chan<- bool + queue chan int64 + finishedQueue chan int64 + diffQueue chan int64 currentID int64 latestFinishedID int64 @@ -71,6 +71,7 @@ type Viddy struct { isNoTitle bool isRingBell bool isShowDiff bool + skipEmptyDiffs bool isEditQuery bool unfold bool pty bool @@ -102,18 +103,30 @@ func NewViddy(conf *config) *Viddy { begin := time.Now().UnixNano() newSnap := func(id int64, before *Snapshot, finish chan<- struct{}) *Snapshot { - return NewSnapshot(id, conf.runtime.cmd, conf.runtime.args, conf.general.shell, conf.general.shellOptions, before, finish) - } - - var snapshotQueue <-chan *Snapshot + return NewSnapshot( + id, + conf.runtime.cmd, + conf.runtime.args, + conf.general.noShell, + conf.general.shell, + conf.general.shellOptions, + before, + finish, + ) + } + + var ( + snapshotQueue <-chan *Snapshot + isSuspendedQueue chan<- bool + ) switch conf.runtime.mode { case ViddyIntervalModeClockwork: - snapshotQueue = ClockSnapshot(begin, newSnap, conf.runtime.interval) + snapshotQueue, isSuspendedQueue = ClockSnapshot(begin, newSnap, conf.runtime.interval) case ViddyIntervalModeSequential: - snapshotQueue = SequentialSnapshot(newSnap, conf.runtime.interval) + snapshotQueue, isSuspendedQueue = SequentialSnapshot(newSnap, conf.runtime.interval) case ViddyIntervalModePrecise: - snapshotQueue = PreciseSnapshot(newSnap, conf.runtime.interval) + snapshotQueue, isSuspendedQueue = PreciseSnapshot(newSnap, conf.runtime.interval) } return &Viddy{ @@ -126,17 +139,21 @@ func NewViddy(conf *config) *Viddy { snapshots: sync.Map{}, historyRows: map[int64]*HistoryRow{}, - snapshotQueue: snapshotQueue, - queue: make(chan int64), - finishedQueue: make(chan int64), - diffQueue: make(chan int64, 100), + historyRowCount: map[int64]int{}, - isRingBell: conf.general.bell, - isShowDiff: conf.general.differences, - isNoTitle: conf.general.noTitle, - isDebug: conf.general.debug, - unfold: conf.general.unfold, - pty: conf.general.pty, + snapshotQueue: snapshotQueue, + isSuspendedQueue: isSuspendedQueue, + queue: make(chan int64), + finishedQueue: make(chan int64), + diffQueue: make(chan int64, 100), + + isRingBell: conf.general.bell, + isShowDiff: conf.general.differences, + skipEmptyDiffs: conf.general.skipEmptyDiffs, + isNoTitle: conf.general.noTitle, + isDebug: conf.general.debug, + unfold: conf.general.unfold, + pty: conf.general.pty, currentID: -1, latestFinishedID: -1, @@ -175,14 +192,16 @@ func (v *Viddy) SetIsNoTitle(b bool) { func (v *Viddy) SetIsShowDiff(b bool) { v.isShowDiff = b - v.setSelection(v.currentID) + v.setSelection(v.currentID, -1) v.arrange() } func (v *Viddy) SetIsTimeMachine(b bool) { v.isTimeMachine = b if !v.isTimeMachine { - v.setSelection(v.latestFinishedID) + v.setSelection(v.latestFinishedID, -1) + } else { + v.goToNowOnTimeMachine() } v.arrange() @@ -205,6 +224,26 @@ func (v *Viddy) startRunner() { } } +func (v *Viddy) updateSelection() { + if !v.isTimeMachine { + v.setSelection(v.latestFinishedID, -1) + } else { + v.setSelection(v.currentID, -1) + } +} + +func (v *Viddy) addSnapshotToView(id int64, r *HistoryRow) { + v.historyView.InsertRow(0) + v.historyView.SetCell(0, 0, r.id) + v.historyView.SetCell(0, 1, r.addition) + v.historyView.SetCell(0, 2, r.deletion) + v.historyView.SetCell(0, 3, r.exitCode) + + v.historyRowCount[id] = v.historyView.GetRowCount() + + v.updateSelection() +} + func (v *Viddy) diffQueueHandler() { for { func() { @@ -225,10 +264,12 @@ func (v *Viddy) diffQueueHandler() { return } - if v.isRingBell { - if s.diffAdditionCount > 0 || s.diffDeletionCount > 0 { + if s.diffAdditionCount > 0 || s.diffDeletionCount > 0 { + if v.isRingBell { fmt.Print(string(byte(7))) } + } else if v.skipEmptyDiffs { + return } r, ok := v.historyRows[id] @@ -236,13 +277,19 @@ func (v *Viddy) diffQueueHandler() { return } + // if skipEmptyDiffs is true, queueHandler wouldn't have added the + // snapshot to view, so we need to add it here. + if v.skipEmptyDiffs { + v.addSnapshotToView(id, r) + } + r.addition.SetText("+" + strconv.Itoa(s.diffAdditionCount)) r.deletion.SetText("-" + strconv.Itoa(s.diffDeletionCount)) }() } } -//nolint:funlen,gocognit,cyclop +//nolint:funlen func (v *Viddy) queueHandler() { for { func() { @@ -271,51 +318,50 @@ func (v *Viddy) queueHandler() { ls := v.getSnapShot(v.latestFinishedID) if ls == nil || s.start.After(ls.start) { v.latestFinishedID = id - if !v.isTimeMachine { - v.setSelection(id) - } else { - v.setSelection(v.currentID) - } + v.updateSelection() } case id := <-v.queue: - if v.isSuspend { - return - } - s := v.getSnapShot(id) idCell := tview.NewTableCell(strconv.FormatInt(s.id, 10)).SetTextColor(tview.Styles.SecondaryTextColor) additionCell := tview.NewTableCell("").SetTextColor(tcell.ColorGreen) deletionCell := tview.NewTableCell("").SetTextColor(tcell.ColorRed) exitCodeCell := tview.NewTableCell("").SetTextColor(tcell.ColorYellow) - v.historyRows[s.id] = &HistoryRow{ + r := &HistoryRow{ id: idCell, addition: additionCell, deletion: deletionCell, exitCode: exitCodeCell, } - - v.historyView.InsertRow(0) - v.historyView.SetCell(0, 0, idCell) - v.historyView.SetCell(0, 1, additionCell) - v.historyView.SetCell(0, 2, deletionCell) - v.historyView.SetCell(0, 3, exitCodeCell) - - v.Lock() - v.idList = append(v.idList, id) - v.Unlock() - - if !v.isTimeMachine { - v.setSelection(v.latestFinishedID) - } else { - v.setSelection(v.currentID) + v.historyRows[s.id] = r + + // if skipEmptyDiffs is true, we need to check if the snapshot + // is empty before adding it to the view (in diffQueueHandler). + // + // This means we're trading off two things: + // + // 1. We're not showing the snapshot in history view until the + // command finishes running, which means it's not possible + // to see partial output. + // 2. Order of the snapshots in history view is lost + // (in non-sequential modes), as some commands could finish + // running quicker than others for whatever reason. + // + // It of course is possible to address these issues by adding + // all snapshots to the history view and then removing the empty + // ones but it unnecessarily complicates the implementation. + if !v.skipEmptyDiffs { + v.addSnapshotToView(id, r) } } }() } } -func (v *Viddy) setSelection(id int64) { +// setSelection selects the given row in the history view. If row is -1, it will +// attempt to select the row corresponding to the given id (or default to the +// latest row if id doesn't exist). +func (v *Viddy) setSelection(id int64, row int) { if id == -1 { return } @@ -327,14 +373,11 @@ func (v *Viddy) setSelection(id int64) { v.historyView.SetSelectable(true, false) } - v.RLock() - index := sort.Search(len(v.idList), func(i int) bool { - return v.idList[i] >= id - }) - i := len(v.idList) - index - 1 - v.RUnlock() + if row == -1 { + row = v.historyView.GetRowCount() - v.historyRowCount[id] + } - v.historyView.Select(i, 0) + v.historyView.Select(row, 0) v.currentID = id unix := v.begin + id*int64(time.Millisecond) v.timeView.SetText(time.Unix(unix/int64(time.Second), unix%int64(time.Second)).String()) @@ -433,7 +476,8 @@ func (v *Viddy) arrange() { } // Run is entry point to run viddy. -// nolint: funlen,gocognit,cyclop +// +//nolint:funlen,gocognit,cyclop,gocyclo,maintidx func (v *Viddy) Run() error { b := tview.NewTextView() b.SetDynamicColors(true) @@ -574,20 +618,40 @@ func (v *Viddy) Run() error { any = true } - if event.Key() == tcell.KeyEsc || event.Rune() == 'q' { + if event.Key() == tcell.KeyEsc { v.showHelpView = false v.arrange() } + if event.Rune() == 'q' { + if v.showHelpView { // if it's help mode, just go back + v.showHelpView = false + v.arrange() + } else { // it's not help view, so just quit + v.app.Stop() + os.Exit(0) + } + } + + // quit viddy from any view + if event.Rune() == 'Q' { + v.app.Stop() + os.Exit(0) + } + switch event.Rune() { case 's': v.isSuspend = !v.isSuspend + v.isSuspendedQueue <- v.isSuspend case 'b': v.isRingBell = !v.isRingBell //case 'd': // v.SetIsShowDiff(!v.isShowDiff) case 't': v.SetIsNoTitle(!v.isNoTitle) + case 'u': + b.SetWrap(v.unfold) + v.unfold = !v.unfold case 'x': if v.isDebug { v.ShowLogView(!v.showLogView) @@ -618,8 +682,6 @@ func (v *Viddy) Run() error { v.UpdateStatusView() - app.EnableMouse(true) - v.app = app v.arrange() @@ -632,87 +694,64 @@ func (v *Viddy) Run() error { return app.Run() } -func (v *Viddy) goToPastOnTimeMachine() { - count := v.historyView.GetRowCount() - selection, _ := v.historyView.GetSelection() +func (v *Viddy) goToRow(row int) { + if row < 0 { + row = 0 + } else if count := v.historyView.GetRowCount(); row >= count { + row = count - 1 + } - if selection+1 < count { - cell := v.historyView.GetCell(selection+1, 0) - if id, err := strconv.ParseInt(cell.Text, 10, 64); err == nil { - v.setSelection(id) - } + var ( + cell = v.historyView.GetCell(row, 0) + id, err = strconv.ParseInt(cell.Text, 10, 64) + ) + + if err == nil { // if _no_ error + v.setSelection(id, row) } } +func (v *Viddy) goToPastOnTimeMachine() { + selection, _ := v.historyView.GetSelection() + v.goToRow(selection + 1) +} + func (v *Viddy) goToFutureOnTimeMachine() { selection, _ := v.historyView.GetSelection() - if 0 <= selection-1 { - cell := v.historyView.GetCell(selection-1, 0) - if id, err := strconv.ParseInt(cell.Text, 10, 64); err == nil { - v.setSelection(id) - } - } + v.goToRow(selection - 1) } func (v *Viddy) goToMorePastOnTimeMachine() { - count := v.historyView.GetRowCount() selection, _ := v.historyView.GetSelection() - - if selection+10 < count { - cell := v.historyView.GetCell(selection+10, 0) - if id, err := strconv.ParseInt(cell.Text, 10, 64); err == nil { - v.setSelection(id) - } - } else { - cell := v.historyView.GetCell(count-1, 0) - if id, err := strconv.ParseInt(cell.Text, 10, 64); err == nil { - v.setSelection(id) - } - } + v.goToRow(selection + 10) } func (v *Viddy) goToMoreFutureOnTimeMachine() { selection, _ := v.historyView.GetSelection() - if 0 <= selection-10 { - cell := v.historyView.GetCell(selection-10, 0) - if id, err := strconv.ParseInt(cell.Text, 10, 64); err == nil { - v.setSelection(id) - } - } else { - cell := v.historyView.GetCell(0, 0) - if id, err := strconv.ParseInt(cell.Text, 10, 64); err == nil { - v.setSelection(id) - } - } + v.goToRow(selection - 10) } func (v *Viddy) goToNowOnTimeMachine() { - cell := v.historyView.GetCell(0, 0) - if id, err := strconv.ParseInt(cell.Text, 10, 64); err == nil { - v.setSelection(id) - } + v.goToRow(0) } func (v *Viddy) goToOldestOnTimeMachine() { - count := v.historyView.GetRowCount() - cell := v.historyView.GetCell(count-1, 0) - - if id, err := strconv.ParseInt(cell.Text, 10, 64); err == nil { - v.setSelection(id) - } + v.goToRow(v.historyView.GetRowCount() - 1) } -var helpTemplate = `Press ESC or Q to go back +var helpTemplate = `Press ESC or q to go back [::b]Key Bindings[-:-:-] - [::u]General[-:-:-] + [::u]General[-:-:-] Toggle time machine mode : [yellow]SPACE[-:-:-] Toggle suspend execution : [yellow]s[-:-:-] Toggle ring terminal bell : [yellow]b[-:-:-] Toggle header display : [yellow]t[-:-:-] Toggle help view : [yellow]h[-:-:-], [yellow]?[-:-:-] + Toggle unfold : [yellow]u[-:-:-] + Quit Viddy : [yellow]Q[-:-:-] [::u]Pager[-:-:-]