[enh] configurable keybindings ++ help popup

pull/55/head
Adam Tauber 7 years ago
parent 0c6ade0216
commit bf3c5b6bca

@ -0,0 +1,82 @@
package main
import (
"github.com/jroimartin/gocui"
)
type CommandFunc func(*gocui.Gui, *gocui.View) error
var COMMANDS map[string]func(string, *App) CommandFunc = map[string]func(string, *App) CommandFunc{
"submit": func(_ string, a *App) CommandFunc {
return a.SubmitRequest
},
"save": func(_ string, a *App) CommandFunc {
return a.OpenSaveDialog
},
"history": func(_ string, a *App) CommandFunc {
return a.ToggleHistory
},
"quit": func(_ string, _ *App) CommandFunc {
return quit
},
"focus": func(args string, a *App) CommandFunc {
return func(g *gocui.Gui, _ *gocui.View) error {
return a.setViewByName(g, args)
}
},
"nextView": func(_ string, a *App) CommandFunc {
return a.NextView
},
"prevView": func(_ string, a *App) CommandFunc {
return a.PrevView
},
"scrollDown": func(_ string, _ *App) CommandFunc {
return scrollViewDown
},
"scrollUp": func(_ string, _ *App) CommandFunc {
return scrollViewUp
},
"pageDown": func(_ string, _ *App) CommandFunc {
return pageDown
},
"pageUp": func(_ string, _ *App) CommandFunc {
return pageUp
},
}
func scrollView(v *gocui.View, dy int) error {
v.Autoscroll = false
ox, oy := v.Origin()
if oy+dy < 0 {
dy = -oy
}
if _, err := v.Line(dy); dy > 0 && err != nil {
dy = 0
}
v.SetOrigin(ox, oy+dy)
return nil
}
func scrollViewUp(_ *gocui.Gui, v *gocui.View) error {
return scrollView(v, -1)
}
func scrollViewDown(_ *gocui.Gui, v *gocui.View) error {
return scrollView(v, 1)
}
func pageUp(_ *gocui.Gui, v *gocui.View) error {
_, height := v.Size()
scrollView(v, -height*2/3)
return nil
}
func pageDown(_ *gocui.Gui, v *gocui.View) error {
_, height := v.Size()
scrollView(v, height*2/3)
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}

@ -38,6 +38,47 @@ type GeneralOptions struct {
var defaultTimeoutDuration, _ = time.ParseDuration("1m")
var DefaultKeys = map[string]map[string]string{
"global": map[string]string{
"CtrlR": "submit",
"CtrlC": "quit",
"CtrlS": "save",
"Tab": "nextView",
"CtrlJ": "nextView",
"CtrlK": "prevView",
"AltH": "history",
"F2": "focus url",
"F3": "focus get",
"F4": "focus method",
"F5": "focus data",
"F6": "focus headers",
"F7": "focus search",
"F8": "focus response-headers",
"F9": "focus response-body",
},
"url": map[string]string{
"Enter": "submit",
},
"response-headers": map[string]string{
"ArrowUp": "scrollUp",
"ArrowDown": "scrollDown",
"PageUp": "pageUp",
"PageDown": "pageDown",
},
"response-body": map[string]string{
"ArrowUp": "scrollUp",
"ArrowDown": "scrollDown",
"PageUp": "pageUp",
"PageDown": "pageDown",
},
"help": map[string]string{
"ArrowUp": "scrollUp",
"ArrowDown": "scrollDown",
"PageUp": "pageUp",
"PageDown": "pageDown",
},
}
var DefaultConfig = Config{
General: GeneralOptions{
Timeout: Duration{
@ -62,6 +103,24 @@ func LoadConfig(configFile string) (*Config, error) {
return nil, err
}
if conf.Keys == nil {
conf.Keys = DefaultKeys
} else {
// copy default keys
for keyCategory, keys := range DefaultKeys {
confKeys, found := conf.Keys[keyCategory]
if found {
for key, action := range keys {
if _, found := confKeys[key]; !found {
conf.Keys[keyCategory][key] = action
}
}
} else {
conf.Keys[keyCategory] = keys
}
}
}
return &conf, nil
}

@ -0,0 +1,76 @@
package main
import (
"github.com/jroimartin/gocui"
)
var KEYS = map[string]gocui.Key{
"F1": gocui.KeyF1,
"F2": gocui.KeyF2,
"F3": gocui.KeyF3,
"F4": gocui.KeyF4,
"F5": gocui.KeyF5,
"F6": gocui.KeyF6,
"F7": gocui.KeyF7,
"F8": gocui.KeyF8,
"F9": gocui.KeyF9,
"F10": gocui.KeyF10,
"F11": gocui.KeyF11,
"F12": gocui.KeyF12,
"Insert": gocui.KeyInsert,
"Delete": gocui.KeyDelete,
"Home": gocui.KeyHome,
"End": gocui.KeyEnd,
"PageUp": gocui.KeyPgup,
"PageDown": gocui.KeyPgdn,
"ArrowUp": gocui.KeyArrowUp,
"ArrowDown": gocui.KeyArrowDown,
"ArrowLeft": gocui.KeyArrowLeft,
"ArrowRight": gocui.KeyArrowRight,
"CtrlTilde": gocui.KeyCtrlTilde,
"Ctrl2": gocui.KeyCtrl2,
"CtrlSpace": gocui.KeyCtrlSpace,
"CtrlA": gocui.KeyCtrlA,
"CtrlB": gocui.KeyCtrlB,
"CtrlC": gocui.KeyCtrlC,
"CtrlD": gocui.KeyCtrlD,
"CtrlE": gocui.KeyCtrlE,
"CtrlF": gocui.KeyCtrlF,
"CtrlG": gocui.KeyCtrlG,
"Backspace": gocui.KeyBackspace,
"CtrlH": gocui.KeyCtrlH,
"Tab": gocui.KeyTab,
"CtrlI": gocui.KeyCtrlI,
"CtrlJ": gocui.KeyCtrlJ,
"CtrlK": gocui.KeyCtrlK,
"CtrlL": gocui.KeyCtrlL,
"Enter": gocui.KeyEnter,
"CtrlM": gocui.KeyCtrlM,
"CtrlN": gocui.KeyCtrlN,
"CtrlO": gocui.KeyCtrlO,
"CtrlP": gocui.KeyCtrlP,
"CtrlQ": gocui.KeyCtrlQ,
"CtrlR": gocui.KeyCtrlR,
"CtrlS": gocui.KeyCtrlS,
"CtrlT": gocui.KeyCtrlT,
"CtrlU": gocui.KeyCtrlU,
"CtrlV": gocui.KeyCtrlV,
"CtrlW": gocui.KeyCtrlW,
"CtrlX": gocui.KeyCtrlX,
"CtrlY": gocui.KeyCtrlY,
"CtrlZ": gocui.KeyCtrlZ,
"Esc": gocui.KeyEsc,
"CtrlLsqBracket": gocui.KeyCtrlLsqBracket,
"Ctrl3": gocui.KeyCtrl3,
"Ctrl4": gocui.KeyCtrl4,
"CtrlBackslash": gocui.KeyCtrlBackslash,
"Ctrl5": gocui.KeyCtrl5,
"CtrlRsqBracket": gocui.KeyCtrlRsqBracket,
"Ctrl6": gocui.KeyCtrl6,
"Ctrl7": gocui.KeyCtrl7,
"CtrlSlash": gocui.KeyCtrlSlash,
"CtrlUnderscore": gocui.KeyCtrlUnderscore,
"Space": gocui.KeySpace,
"Backspace2": gocui.KeyBackspace2,
"Ctrl8": gocui.KeyCtrl8,
}

@ -6,6 +6,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
@ -40,18 +41,6 @@ var METHODS []string = []string{
http.MethodHead,
}
var SHORTCUTS map[gocui.Key]string = map[gocui.Key]string{
// gocui.KeyF1 reserved for help popup
gocui.KeyF2: "url",
gocui.KeyF3: "get",
gocui.KeyF4: "method",
gocui.KeyF5: "data",
gocui.KeyF6: "headers",
gocui.KeyF7: "search",
gocui.KeyF8: "response-headers",
gocui.KeyF9: "response-body",
}
var CLIENT *http.Client = &http.Client{
Timeout: time.Duration(5 * time.Second),
}
@ -201,7 +190,7 @@ func (a *App) Layout(g *gocui.Gui) error {
return err
}
setViewDefaults(v)
v.Title = "URL (F2) - press ctrl+r to send request"
v.Title = "URL - press F1 for help"
v.Editable = true
v.Overwrite = false
v.Editor = &singlelineEditor{&defaultEditor}
@ -213,7 +202,7 @@ func (a *App) Layout(g *gocui.Gui) error {
}
setViewDefaults(v)
v.Editable = true
v.Title = "URL params (F3)"
v.Title = "URL params"
v.Editor = &defaultEditor
}
if v, err := g.SetView("method", 0, splitY+1, splitX, splitY+3); err != nil {
@ -222,7 +211,7 @@ func (a *App) Layout(g *gocui.Gui) error {
}
setViewDefaults(v)
v.Editable = true
v.Title = "Method (F4)"
v.Title = "Method"
v.Editor = &singlelineEditor{&defaultEditor}
setViewTextAndCursor(v, "GET")
@ -233,7 +222,7 @@ func (a *App) Layout(g *gocui.Gui) error {
}
setViewDefaults(v)
v.Editable = true
v.Title = "Request data (POST/PUT) (F5)"
v.Title = "Request data (POST/PUT)"
v.Editor = &defaultEditor
}
if v, err := g.SetView("headers", 0, 3+(splitY*2), splitX, maxY-2); err != nil {
@ -243,7 +232,7 @@ func (a *App) Layout(g *gocui.Gui) error {
setViewDefaults(v)
v.Wrap = false
v.Editable = true
v.Title = "Request headers (F6)"
v.Title = "Request headers"
v.Editor = &defaultEditor
}
if v, err := g.SetView("response-headers", splitX, 3, maxX-1, splitY+3); err != nil {
@ -251,7 +240,7 @@ func (a *App) Layout(g *gocui.Gui) error {
return err
}
setViewDefaults(v)
v.Title = "Response headers (F8)"
v.Title = "Response headers"
v.Editable = true
v.Editor = &ViewEditor{a, g, false, gocui.EditorFunc(func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
return
@ -262,7 +251,7 @@ func (a *App) Layout(g *gocui.Gui) error {
return err
}
setViewDefaults(v)
v.Title = "Response body (F9)"
v.Title = "Response body"
v.Editable = true
v.Editor = &ViewEditor{a, g, false, gocui.EditorFunc(func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
return
@ -510,7 +499,7 @@ func (a *App) PrintBody(g *gocui.Gui) {
is_binary := strings.Index(req.ContentType, "text") == -1 && strings.Index(req.ContentType, "application") == -1
search_text := getViewValue(g, "search")
if search_text == "" || is_binary {
vrb.Title = "Response body (F9)"
vrb.Title = "Response body"
if is_binary {
vrb.Title += " [binary content]"
fmt.Fprint(vrb, hex.Dump(req.RawResponseBody))
@ -542,52 +531,111 @@ func (a *App) PrintBody(g *gocui.Gui) {
})
}
func (a *App) SetKeys(g *gocui.Gui) {
// global keybindings
g.SetManagerFunc(a.Layout)
func parseKey(k string) (interface{}, gocui.Modifier, error) {
mod := gocui.ModNone
if strings.Index(k, "Alt") == 0 {
mod = gocui.ModAlt
k = k[3:]
}
switch len(k) {
case 0:
return 0, 0, errors.New("Empty key string")
case 1:
if mod != gocui.ModNone {
k = strings.ToLower(k)
}
return rune(k[0]), mod, nil
}
g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit)
key, found := KEYS[k]
if !found {
return 0, 0, fmt.Errorf("Unknown key: %v", k)
}
return key, mod, nil
}
g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, a.NextView)
g.SetKeybinding("", gocui.KeyCtrlJ, gocui.ModNone, a.NextView)
g.SetKeybinding("", gocui.KeyCtrlK, gocui.ModNone, a.PrevView)
g.SetKeybinding("method", gocui.KeyEnter, gocui.ModNone, a.ToggleMethodlist)
func (a *App) setKey(g *gocui.Gui, keyStr, commandStr, viewName string) error {
if commandStr == "" {
return nil
}
key, mod, err := parseKey(keyStr)
if err != nil {
return err
}
commandParts := strings.SplitN(commandStr, " ", 2)
command := commandParts[0]
var commandArgs string
if len(commandParts) == 2 {
commandArgs = commandParts[1]
}
keyFnGen, found := COMMANDS[command]
if !found {
return fmt.Errorf("Unknown command: %v", command)
}
keyFn := keyFnGen(commandArgs, a)
if err := g.SetKeybinding(viewName, key, mod, keyFn); err != nil {
return fmt.Errorf("Failed to set key '%v': %v", keyStr, err)
}
return nil
}
// Cycle for each SHORTCUTS
for key, view := range SHORTCUTS {
handler := func(name string) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, _ *gocui.View) error {
return a.setViewByName(g, name)
}
}
g.SetKeybinding("", key, gocui.ModNone, handler(view))
func (a *App) printViewKeybindings(v io.Writer, viewName string) {
keys, found := a.config.Keys[viewName]
if !found {
return
}
mk := make([]string, len(keys))
i := 0
for k, _ := range keys {
mk[i] = k
i++
}
sort.Strings(mk)
fmt.Fprintf(v, "\n %v\n", viewName)
for _, key := range mk {
fmt.Fprintf(v, " %-15v %v\n", key, keys[key])
}
}
if runtime.GOOS != "windows" {
g.SetKeybinding("", gocui.KeyCtrlH, gocui.ModNone, a.ToggleHistory)
func (a *App) SetKeys(g *gocui.Gui) error {
// load config keybindings
for viewName, keys := range a.config.Keys {
if viewName == "global" {
viewName = ""
}
for keyStr, commandStr := range keys {
if err := a.setKey(g, keyStr, commandStr, viewName); err != nil {
return err
}
}
}
g.SetKeybinding("", 'h', gocui.ModAlt, a.ToggleHistory)
g.SetKeybinding("", gocui.KeyCtrlS, gocui.ModNone, a.OpenSaveDialog)
g.SetKeybinding("", gocui.KeyF1, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
if a.currentPopup == "help" {
a.closePopup(g, "help")
return nil
}
g.SetKeybinding("", gocui.KeyCtrlR, gocui.ModNone, a.SubmitRequest)
g.SetKeybinding("url", gocui.KeyEnter, gocui.ModNone, a.SubmitRequest)
help, err := a.CreatePopupView("help", 60, 40, g)
if err != nil {
return err
}
help.Title = "Help"
help.Highlight = false
fmt.Fprint(help, "Keybindings:\n")
a.printViewKeybindings(help, "global")
for _, viewName := range VIEWS {
if _, found := a.config.Keys[viewName]; !found {
continue
}
a.printViewKeybindings(help, viewName)
}
g.SetViewOnTop("help")
g.SetCurrentView("help")
return nil
})
// responses common keybindings
for _, view := range []string{"response-body", "response-headers"} {
g.SetKeybinding(view, gocui.KeyArrowUp, gocui.ModNone, scrollViewUp)
g.SetKeybinding(view, gocui.KeyArrowDown, gocui.ModNone, scrollViewDown)
g.SetKeybinding(view, gocui.KeyPgup, gocui.ModNone, func(_ *gocui.Gui, v *gocui.View) error {
_, height := v.Size()
scrollView(v, -height*2/3)
return nil
})
g.SetKeybinding(view, gocui.KeyPgdn, gocui.ModNone, func(_ *gocui.Gui, v *gocui.View) error {
_, height := v.Size()
scrollView(v, height*2/3)
return nil
})
}
g.SetKeybinding("method", gocui.KeyEnter, gocui.ModNone, a.ToggleMethodlist)
cursDown := func(g *gocui.Gui, v *gocui.View) error {
cx, cy := v.Cursor()
@ -698,6 +746,7 @@ func (a *App) SetKeys(g *gocui.Gui) {
a.closePopup(g, "save-result")
return nil
})
return nil
}
func (a *App) closePopup(g *gocui.Gui, viewname string) {
@ -717,8 +766,8 @@ func (a *App) CreatePopupView(name string, width, height int, g *gocui.Gui) (v *
g.Cursor = false
maxX, maxY := g.Size()
if height > maxY-1 {
height = maxY - 1
if height > maxY-4 {
height = maxY - 4
}
if width > maxX-4 {
width = maxX - 4
@ -864,12 +913,14 @@ func (a *App) LoadConfig(configPath string) error {
// If the config file doesn't exist, load the default config
if _, err := os.Stat(configPath); os.IsNotExist(err) {
a.config = &config.DefaultConfig
a.config.Keys = config.DefaultKeys
return nil
}
conf, err := config.LoadConfig(configPath)
if err != nil {
a.config = &config.DefaultConfig
a.config.Keys = config.DefaultKeys
return err
}
@ -982,7 +1033,7 @@ func initApp(a *App, g *gocui.Gui) {
g.InputEsc = false
g.BgColor = gocui.ColorDefault
g.FgColor = gocui.ColorDefault
a.SetKeys(g)
g.SetManagerFunc(a.Layout)
}
func getViewValue(g *gocui.Gui, name string) string {
@ -993,27 +1044,6 @@ func getViewValue(g *gocui.Gui, name string) string {
return strings.TrimSpace(v.Buffer())
}
func scrollView(v *gocui.View, dy int) error {
v.Autoscroll = false
ox, oy := v.Origin()
if oy+dy < 0 {
dy = -oy
}
if _, err := v.Line(dy); dy > 0 && err != nil {
dy = 0
}
v.SetOrigin(ox, oy+dy)
return nil
}
func scrollViewUp(_ *gocui.Gui, v *gocui.View) error {
return scrollView(v, -1)
}
func scrollViewDown(_ *gocui.Gui, v *gocui.View) error {
return scrollView(v, 1)
}
func setViewDefaults(v *gocui.View) {
v.Frame = true
v.Wrap = true
@ -1025,10 +1055,6 @@ func setViewTextAndCursor(v *gocui.View, s string) {
v.SetCursor(len(s), 0)
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
func help() {
fmt.Println(`wuzz - Interactive cli tool for HTTP inspection
@ -1108,6 +1134,14 @@ func main() {
os.Exit(1)
}
err = app.SetKeys(g)
if err != nil {
g.Close()
fmt.Println("Error!", err)
os.Exit(1)
}
defer g.Close()
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {

Loading…
Cancel
Save