Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b3916d0
feat: initial code for a simple TUI ww
jtyers Jun 28, 2020
b0e0472
feat: refactored to use channels rather than direct writes
jtyers Jun 28, 2020
7ea126f
fix: various fixes; better layout, and interval support
jtyers Jun 28, 2020
663f9da
fix: layout and clipping fix
jtyers Jun 28, 2020
16664e1
feat: use default term color rather than white for output
jtyers Jun 28, 2020
dbb2327
feat: added -s / --shell for wrapping cmd in a shell (derived from
jtyers Jun 28, 2020
be36c34
feat: allow triggers to feed back status updates before they fire
jtyers Jun 28, 2020
bd69cf3
feat: moved to a better CLI args parser
jtyers Jun 28, 2020
a633640
fix: trigger test fixes
jtyers Jun 28, 2020
8a9127f
feat: set background color of top row rather than ust text
jtyers Jun 28, 2020
23e766a
feat: added highlighting
jtyers Jun 29, 2020
4ee7ad9
fix: changed -H and --highlight to --color, --colour, -c to match cur…
jtyers Jun 29, 2020
cafa11a
feat: added parser for WW_DEFAULT_ARGS
jtyers Jun 29, 2020
889663d
feat: add WW_DEFAULT_ARGS support into ww
jtyers Jun 29, 2020
b1aeea5
feat: move to flaggy for args parsing to get sne behaviour on -h and …
jtyers Jun 29, 2020
0fcacf5
feat: moved from channels to callbacks in ww
jtyers Jul 1, 2020
82cd224
fix: remove config printinng
jtyers Jul 1, 2020
cba35cf
feat: re-jigged scanning logic so we detect EOFs correctly, and wait …
jtyers Jul 1, 2020
f9e7776
tidy: refactor status callback using a custom struct for readability,
jtyers Jul 2, 2020
d67cf58
fix: use channels to synchronise stdout/stderr scanners
jtyers Jul 2, 2020
6e3a830
feat: added fsnotify (file system watch) support, and refactored ever…
jtyers Jul 4, 2020
e400b43
feat: add support for watch exclusions
jtyers Jul 4, 2020
8c7a896
fix: ignore .git by default for --watch
jtyers Jul 4, 2020
839124e
fix: do not re-use watchers, re-create every time;
jtyers Jul 4, 2020
f246887
fix: fix update of text view on command re-run
jtyers Jul 6, 2020
85af840
feat: refactor tview out of WW into abstract WWDisplay
jtyers Jul 10, 2020
fc7d6ab
feat: add new UiLiveDisplay which does not go full screen (on by defa…
jtyers Jul 10, 2020
105d334
feat: make shell processing the default, and can be disabled with -S
jtyers Jul 11, 2020
892dde2
feat: add color for uilive display
jtyers Jul 11, 2020
b5bab64
tidy: go mod tidy
jtyers Jul 11, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
packaging/dist
packaging/*.tar.gz
ww
115 changes: 115 additions & 0 deletions cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package main

//go:generate slice -dir slice -type string -package slice

import (
"fmt"
"os"
"strings"
"time"

"github.com/integrii/flaggy"
"github.com/jtyers/ww/slice"
"github.com/jtyers/ww/trigger/fsnotify"
"github.com/jtyers/ww/trigger/interval"
)

func parseArgs() (WWConfig, WWDisplay) {
config := WWConfig{}

flWatch := false
flInterval := 2
flFullscreen := false
flNoShell := false
flHighlights := []string{}
flWatchExcludes := []string{".git"} // default value

flaggy.Bool(&flFullscreen, "f", "fullscreen", "Run command in an ncurses-like full screen view")
flaggy.Int(&flInterval, "n", "interval", "Run command every X seconds")
flaggy.Bool(&flNoShell, "S", "no-shell", "Do not run command inside a shell")
flaggy.StringSlice(&flHighlights, "c", "color", "Colour (highlight) the given string in output (can be specified multiple times, case-insensitive)")
flaggy.Bool(&flWatch, "w", "watch", "Watch current directory for changes")
flaggy.StringSlice(&flWatchExcludes, "x", "exclude", "Exclude files/directories with the given name")

flaggy.DefaultParser.ShowVersionWithVersionFlag = true
flaggy.DefaultParser.ShowHelpOnUnexpected = true
flaggy.DefaultParser.ShowHelpWithHFlag = true

// Prepend $WW_DEFAULT_ARGS to our args list then process them all as normal
args := slice.NewStringSlice(GetArgsFromEnvironment()).
Concat(os.Args[1:]).
Value()
flaggy.ParseArgs(args)

if len(flaggy.TrailingArguments) == 0 {
flaggy.ShowHelpAndExit("command required")
}

config.Command = flaggy.TrailingArguments[0]
config.Args = flaggy.TrailingArguments[1:]

if flWatch {
wd, err := os.Getwd()
if err != nil {
die(nil, "getwd: %v", err)
}

fsnotifyTrigger, err := fsnotify.NewFsNotifyTrigger(wd, flWatchExcludes)
if err != nil {
die(nil, "error creating fsnotify trigger: %v", err)
}

config.Trigger = fsnotifyTrigger

} else if flInterval > 0 {
i, err := time.ParseDuration(fmt.Sprintf("%ds", flInterval))
if err != nil {
die(nil, "invalid --interval: %v", err)
}

config.Trigger = &interval.IntervalWWTrigger{Interval: i}
}

if !flNoShell {
currentShell, ok := os.LookupEnv("SHELL")
if !ok {
currentShell = "/bin/sh"
}

newArgs := []string{
"-c",
strings.Join(
slice.ConcatString([]string{config.Command}, slice.MapString(config.Args, func(s string, n int) string {
// for every arg, if it contains whitespace, enclose it in quotes
if strings.Contains(s, " ") || strings.Contains(s, "\t") {
return "\"" + s + "\""
}

return s
})),
" ",
),
}

config.Command = currentShell
config.Args = newArgs
}

highlights := map[string]string{}
if len(flHighlights) > 0 {
for _, highlight := range flHighlights {
highlights[highlight] = "[red]"
}
}

config.Highlighter = NewHighlighter(highlights) // always set this so it is not nil

var display WWDisplay
if flFullscreen {
display = &TviewDisplay{config: config}
} else {
display = &UiLiveDisplay{config: config}
}

return config, display
}
20 changes: 20 additions & 0 deletions display.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package main

import ()

// WWDisplay represents a type of display for WW.
type WWDisplay interface {
// Init should perform whatever initialisation is needed for the display, including initial render.
Init(config WWConfig) error

Stop() error

// Called when process state changes
UpdateStatus(status Status, header string, cmdNameAndArgs string)

// Called when we receive data on stdout
OnStdout(data string)

// Called when we receive data on stder
OnStderr(data string)
}
26 changes: 26 additions & 0 deletions env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"os"
"regexp"
)

var DefaultArgsEnvKey = "WW_DEFAULT_ARGS"
var WordSplit = regexp.MustCompile("\"([^\"]*)\"|([^\\s]+)")

// GetArgsFromEnvironment reads WW_DEFAULT_ARGS and produces a string slice containing those args.
func GetArgsFromEnvironment() []string {
v := os.Getenv(DefaultArgsEnvKey)
result := []string{}
for _, match := range WordSplit.FindAllStringSubmatch(v, -1) {
if match[1] != "" {
result = append(result, match[1])

} else {
// NOTE we also hit this if the first group matched, but the quote were empty (""); this is valid
result = append(result, match[2])
}
}

return result
}
51 changes: 51 additions & 0 deletions env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

import (
"os"
"testing"

"github.com/stretchr/testify/require"
)

func TestGetArgsFromEnvironment(t *testing.T) {
var tests = []struct {
name string
input string
output []string
}{
{
"should split on spaces",
"-c word -s --colour hello",
[]string{"-c", "word", "-s", "--colour", "hello"},
},
{
"should split on tabs",
"-c word\t-s --colour\thello",
[]string{"-c", "word", "-s", "--colour", "hello"},
},
{
"should consider quoted args together",
"-c word\t-s --colour \"hello world\" -c \"foo\" -c \"another.js\"",
[]string{"-c", "word", "-s", "--colour", "hello world", "-c", "foo", "-c", "another.js"},
},
{
"should convert empty quote marks to empty string",
"-c \"\"",
[]string{"-c", ""},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// given
os.Setenv(DefaultArgsEnvKey, test.input)

// when
result := GetArgsFromEnvironment()

// then

require.Equal(t, test.output, result)
})
}
}
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module github.com/jtyers/ww

go 1.14

require (
github.com/fsnotify/fsnotify v1.4.9
github.com/gdamore/tcell v1.3.0
github.com/gosuri/uilive v0.0.4
github.com/integrii/flaggy v1.4.4
github.com/karrick/godirwalk v1.15.6
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/rivo/tview v0.0.0-20200528200248-fe953220389f
github.com/stretchr/testify v1.6.1
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect
)
49 changes: 49 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
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 v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI=
github.com/integrii/flaggy v1.4.4 h1:8fGyiC14o0kxhTqm2VBoN19fDKPZsKipP7yggreTMDc=
github.com/integrii/flaggy v1.4.4/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/karrick/godirwalk v1.15.6 h1:Yf2mmR8TJy+8Fa0SuQVto5SYap6IF7lNVX4Jdl8G1qA=
github.com/karrick/godirwalk v1.15.6/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
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/rivo/tview v0.0.0-20200528200248-fe953220389f h1:tRx/LLIP2PSA7johw9xhf+6NUCLC4BbMhpGdm110MGI=
github.com/rivo/tview v0.0.0-20200528200248-fe953220389f/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
44 changes: 44 additions & 0 deletions highlighter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"regexp"
)

// tview colour tag which resets foreground/background/flags.
var RESET = "[-:-:-]"

type highlightItem struct {
Regexp *regexp.Regexp
ColorTag string
}

type Highlighter struct {
highlightItems []highlightItem
}

// NewHighlighter creates a new Highlighter with the passed-in map of
// strings to highlight. The key is the value to highlight, and the value
// is the colour to change to. The value should be a full tcell tag (e.g. "[red]").
func NewHighlighter(highlights map[string]string) *Highlighter {
highlightItems := []highlightItem{}

if highlights != nil {
for k, v := range highlights {
highlightItems = append(highlightItems, highlightItem{
// Prepend '(?i)' to our regex, which sets the case-insensitive flag.
Regexp: regexp.MustCompile("(?i)" + regexp.QuoteMeta(k)),
ColorTag: v,
})
}
}

return &Highlighter{highlightItems}
}

func (h *Highlighter) Highlight(input string) string {
for _, item := range h.highlightItems {
input = item.Regexp.ReplaceAllString(input, item.ColorTag+"${0}"+RESET)
}

return input
}
65 changes: 65 additions & 0 deletions highlighter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestHighlight(t *testing.T) {
sample := `The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.`

var tests = []struct {
name string
highlights map[string]string
input string
output string
}{
{
"should not change input when highlights is empty",
map[string]string{},
sample,
sample,
},
{
"should handle nil highlights",
nil,
sample,
sample,
},
{
"should change input for specified highlights",
map[string]string{
"word": "[red]",
"this": "[green]",
},
"this word or this word",
"[green]this[-:-:-] [red]word[-:-:-] or [green]this[-:-:-] [red]word[-:-:-]",
},
{
"should change input for specified highlights ignoring case",
map[string]string{
"word": "[red]",
},
"this word or this WORD or this Word or this wORD",
"this [red]word[-:-:-] or this [red]WORD[-:-:-] or this [red]Word[-:-:-] or this [red]wORD[-:-:-]",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// given
highlighter := NewHighlighter(test.highlights)

// when
result := highlighter.Highlight(test.input)

// then

require.Equal(t, test.output, result)
})
}
}
Loading