diff --git a/go.mod b/go.mod index e7bc9b5..86d76aa 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/fsnotify/fsnotify v1.5.1 github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 github.com/google/martian/v3 v3.2.1 - github.com/rivo/tview v0.0.0-20211129142845-821b2667c414 - golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 // indirect + github.com/rivo/tview v0.0.0-20220216162559-96063d6082f3 + golang.org/x/net v0.0.0-20220105145211-5b0dc2dfae98 // indirect golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index 5d29724..1fadd0e 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rivo/tview v0.0.0-20211129142845-821b2667c414 h1:8pLxYvjWizid9rNUDyWv9D4gti+/w+TK7P10eXnh+xA= -github.com/rivo/tview v0.0.0-20211129142845-821b2667c414/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= +github.com/rivo/tview v0.0.0-20220216162559-96063d6082f3 h1:crs4rrYnQqsZpz/EtjezHGCu13e+3W9eqj0MzIYXir0= +github.com/rivo/tview v0.0.0-20220216162559-96063d6082f3/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -62,8 +62,8 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 h1:0qxwC5n+ttVOINCBeRHO0nq9X7uy8SDsPoi5OaCdIEI= -golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220105145211-5b0dc2dfae98 h1:+6WJMRLHlD7X7frgp7TUZ36RnQzSf9wVVTNakEp+nqY= +golang.org/x/net v0.0.0-20220105145211-5b0dc2dfae98/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/views/proxyview.go b/views/proxyview.go index 7c6ed00..22da85c 100644 --- a/views/proxyview.go +++ b/views/proxyview.go @@ -29,8 +29,8 @@ import ( type ProxyView struct { Layout *tview.Pages // The main replay view, all others should be underneath Layout Table *tview.Table // the proxy history table - requestBox *tview.TextView // request text box - responseBox *tview.TextView // response text box + requestBox *TextPrimitive // request text box + responseBox *TextPrimitive // response text box Logger *modifier.Logger // the Martian logger filter ViewFilter // filter for the proxy view @@ -77,7 +77,7 @@ func (view *ProxyView) Init(app *tview.Application, replayview *ReplayView, logg view.Table.SetCell(0, 7, tview.NewTableCell("Method").SetTextColor(tcell.ColorMediumPurple).SetSelectable(false)) reqRespFlexView := tview.NewFlex() - view.requestBox = tview.NewTextView().SetWrap(false).SetDynamicColors(true) + view.requestBox = NewTextPrimitive() view.requestBox.SetBorder(true) view.requestBox.SetTitle("Request") view.requestBox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { @@ -113,7 +113,7 @@ func (view *ProxyView) Init(app *tview.Application, replayview *ReplayView, logg return event }) - view.responseBox = tview.NewTextView().SetWrap(false).SetDynamicColors(true) + view.responseBox = NewTextPrimitive() view.responseBox.SetBorder(true) view.responseBox.SetTitle("Response") view.responseBox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { @@ -216,13 +216,7 @@ func (view *ProxyView) Init(app *tview.Application, replayview *ReplayView, logg id = view.Table.GetCell(row, 1).Text if entry := view.Logger.GetEntry(id); entry != nil { if entry.Request != nil { - // Appending a UTF8 braille pattern blank (U+2800) - // to deal with the partial-trailing-utf8-rune logic - // in tview (textview.go) - // this technique seems to make weird artifecats happen depending on the terminal - // some sensible mechanism forResponse escaping data would probably be better... - //fmt.Fprint(view.requestBox, "\u2800") view.writeRequest(entry.Request) view.requestBox.ScrollToBeginning() } diff --git a/views/replayview.go b/views/replayview.go index 7b1b7d0..e8840ce 100644 --- a/views/replayview.go +++ b/views/replayview.go @@ -19,12 +19,12 @@ import ( // ReplayView - struct that holds the main replayview elements type ReplayView struct { - Layout *tview.Pages // The main replay view, all others should be underneath Layout - Table *tview.Table // the main table that lists all the replay items - request *tview.TextView // http request box - response *tview.TextView // http response box - responseMeta *tview.Table // metadata for size recieved and time taken - goButton *tview.Button // send button + Layout *tview.Pages // The main replay view, all others should be underneath Layout + Table *tview.Table // the main table that lists all the replay items + request *TextPrimitive // http request box + response *TextPrimitive // http response box + responseMeta *tview.Table // metadata for size recieved and time taken + goButton *tview.Button // send button host *tview.InputField // host field input port *tview.InputField // port input @@ -216,8 +216,8 @@ func (view *ReplayView) Init(app *tview.Application) { mainLayout := tview.NewFlex() replayFlexView := tview.NewFlex() - view.request = tview.NewTextView() - view.request.SetWrap(false).SetBorder(true).SetTitle("Request") + view.request = NewTextPrimitive() + view.request.SetBorder(true).SetTitle("Request") // go and cancel buttons view.goButton = tview.NewButton("Go") @@ -265,7 +265,7 @@ func (view *ReplayView) Init(app *tview.Application) { view.autoSend.SetLabelColor(tcell.ColorMediumPurple) view.autoSend.SetLabel("AutoSend") - view.response = tview.NewTextView().SetWrap(false) + view.response = NewTextPrimitive() view.response.SetBorder(true).SetTitle("Response") view.goButton.SetSelectedFunc(func() { diff --git a/views/textprimitive.go b/views/textprimitive.go new file mode 100644 index 0000000..8223d66 --- /dev/null +++ b/views/textprimitive.go @@ -0,0 +1,272 @@ +package views + +import ( + "bytes" + "regexp" + "sync" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// TextPrimitive is a basic, line wrapped text view that is designed to replicate a +// severely cut down version of tview's TextView, removing color support, grapheme cluster +// handling, regions and other functionality with the aim of increasing performance +// and being able to handle megabytes of data with wrapping. +// You probably don't want to use this. +// See: https://github.com/rivo/tview/issues/686 +type TextPrimitive struct { + sync.Mutex + *tview.Box + + buffer []string + lineOffset int // the line offset for view scrolling + fitsAll bool // whether or not the entire content of buffer from the lineOffset onwards fits on the screen +} + +var ( + newLineRegex = regexp.MustCompile(`\r?\n`) + TabSize = 4 +) + +func NewTextPrimitive() *TextPrimitive { + var buffer []string + + return &TextPrimitive{ + Box: tview.NewBox(), + buffer: buffer, + lineOffset: 0, + } +} + +// Write lets us implement the io.Writer interface. Tab characters will be +// replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted +// as a new line. +func (t *TextPrimitive) Write(p []byte) (n int, err error) { + t.Lock() + defer t.Unlock() + + newBytes := bytes.Replace(p, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1) + for index, line := range newLineRegex.Split(string(newBytes), -1) { + if index == 0 { + if len(t.buffer) == 0 { + t.buffer = []string{line} + } else { + t.buffer[len(t.buffer)-1] += line + } + } else { + t.buffer = append(t.buffer, line) + } + } + + return len(p), nil +} + +func (t *TextPrimitive) Clear() { + t.Lock() + defer t.Unlock() + t.buffer = nil + t.lineOffset = 0 +} + +func (t *TextPrimitive) ScrollToBeginning() { + t.Lock() + defer t.Unlock() + t.lineOffset = 0 +} + +func (t *TextPrimitive) Draw(screen tcell.Screen) { + t.Lock() + defer t.Unlock() + + t.Box.DrawForSubclass(screen, t) + t.fitsAll = true + x, y, width, height := t.GetInnerRect() + + // loop each str and print + index, offsetindex := 0, 0 + for _, str := range t.buffer { + if index >= height { + t.fitsAll = false + break + } + + if len(str) == 0 { // blank line + if offsetindex < t.lineOffset { + offsetindex++ + } else { + index++ + } + } + + runes := []rune(str) + for len(runes) > 0 { + var extract []rune + if index >= height { + t.fitsAll = false + break + } + + if len(runes) > width { + extract = runes[:width] + } else { + extract = runes + } + + w := len(extract) + for len(string(extract)) > width { + w-- // string width is greater than rune count, yank one out + extract = runes[:w] + } + + runes = runes[len(extract):] + if offsetindex < t.lineOffset { + offsetindex++ + continue + } else { + tview.PrintSimple(screen, string(extract), x, y+index) + index++ + } + } + } +} + +func (t *TextPrimitive) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + + _, _, width, height := t.GetInnerRect() + switch event.Key() { + + case tcell.KeyRune: + switch event.Rune() { + case 'g': // back to the beginning + t.lineOffset = 0 + case 'G': // end + end := 0 + for _, str := range t.buffer { + length := len(str) + end = end + (length / width) + if length%width > 0 { + end++ + } + } + t.lineOffset = end - height + case 'j': + if !t.fitsAll { + max := 0 + for _, str := range t.buffer { + length := len(str) + max = max + (length / width) + if length%width > 0 { + max++ + } + if t.lineOffset < max { + t.lineOffset++ + break + } + } + } + case 'k': + if t.lineOffset > 0 { + t.lineOffset-- + } + } + + case tcell.KeyHome: + t.lineOffset = 0 // back to the beginning + case tcell.KeyEnd: + end := 0 + for _, str := range t.buffer { + length := len(str) + end = end + (length / width) + if length%width > 0 { + end++ + } + } + t.lineOffset = end - height + case tcell.KeyUp: + if t.lineOffset > 0 { + t.lineOffset-- + } + case tcell.KeyDown: + if !t.fitsAll { + max := 0 + for _, str := range t.buffer { + length := len(str) + max = max + (length / width) + if length%width > 0 { + max++ + } + if t.lineOffset < max { + t.lineOffset++ + break + } + } + } + case tcell.KeyPgUp, tcell.KeyCtrlB: + if t.lineOffset > 0 { + t.lineOffset = t.lineOffset - height + if t.lineOffset < 0 { + t.lineOffset = 0 + } + } + + case tcell.KeyPgDn, tcell.KeyCtrlF: + if !t.fitsAll { + max := 0 + for _, str := range t.buffer { + length := len(str) + max = max + (length / width) + if length%width > 0 { + max++ + } + if t.lineOffset < max { + t.lineOffset = t.lineOffset + height + break + } + } + } + } + + }) +} + +func (t *TextPrimitive) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { + return t.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { + x, y := event.Position() + if !t.InRect(x, y) { + return false, nil + } + + _, _, width, _ := t.GetInnerRect() + + switch action { + case tview.MouseLeftClick: + setFocus(t) + consumed = true + case tview.MouseScrollUp: + if t.lineOffset > 0 { + t.lineOffset-- + } + consumed = true + case tview.MouseScrollDown: + if !t.fitsAll { + max := 0 + for _, str := range t.buffer { + length := len(str) + max = max + (length / width) + if length%width > 0 { + max++ + } + if t.lineOffset < max { + t.lineOffset++ + break + } + } + } + consumed = true + } + + return + }) +}