From 23cd8be018f2820302a76876f988e28ace3c395e Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Tue, 18 Jun 2019 14:14:20 +0300
Subject: [PATCH 01/13] fix a bug on selecting empty list and remove useless
code
---
prompt/branch.go | 8 +++++++-
prompt/log.go | 16 ++++++++++++++--
prompt/prompt.go | 24 +++++-------------------
prompt/status.go | 5 +++--
4 files changed, 29 insertions(+), 24 deletions(-)
diff --git a/prompt/branch.go b/prompt/branch.go
index 88cf6f2..9202b97 100644
--- a/prompt/branch.go
+++ b/prompt/branch.go
@@ -1,6 +1,7 @@
package prompt
import (
+ "fmt"
"os/exec"
"github.com/fatih/color"
@@ -39,7 +40,6 @@ func (b *Branch) Start(opts *Options) error {
b.prompt = &prompt{
list: list,
opts: opts,
- layout: branch,
selection: b.onSelect,
keys: b.onKey,
info: b.branchInfo,
@@ -51,6 +51,9 @@ func (b *Branch) Start(opts *Options) error {
func (b *Branch) onSelect() bool {
items, idx := b.prompt.list.Items()
+ if idx == NotFound {
+ return false
+ }
branch := items[idx].(*git.Branch)
args := []string{"checkout", branch.Name}
cmd := exec.Command("git", args...)
@@ -92,6 +95,9 @@ func (b *Branch) branchInfo(item Item) [][]term.Cell {
func (b *Branch) deleteBranch(mode string) error {
items, idx := b.prompt.list.Items()
+ if idx == NotFound {
+ return fmt.Errorf("there is no item to delete")
+ }
branch := items[idx].(*git.Branch)
cmd := exec.Command("git", "branch", "-"+mode, branch.Name)
cmd.Dir = b.Repo.Path()
diff --git a/prompt/log.go b/prompt/log.go
index eef998a..ae01dbe 100644
--- a/prompt/log.go
+++ b/prompt/log.go
@@ -1,6 +1,7 @@
package prompt
import (
+ "fmt"
"strconv"
"strings"
@@ -45,7 +46,6 @@ func (l *Log) Start(opts *Options) error {
l.prompt = &prompt{
list: list,
opts: opts,
- layout: log,
keys: l.onKey,
selection: l.onSelect,
info: l.logInfo,
@@ -59,6 +59,9 @@ func (l *Log) Start(opts *Options) error {
func (l *Log) onSelect() bool {
// s.showDiff()
items, idx := l.prompt.list.Items()
+ if idx == NotFound {
+ return false
+ }
item := items[idx]
switch item.(type) {
case *git.Commit:
@@ -94,7 +97,10 @@ func (l *Log) onSelect() bool {
func (l *Log) onKey(key rune) bool {
items, idx := l.prompt.list.Items()
- item := items[idx]
+ var item Item
+ if idx != NotFound {
+ item = items[idx]
+ }
switch item.(type) {
case *git.Commit:
switch key {
@@ -120,6 +126,9 @@ func (l *Log) onKey(key rune) bool {
func (l *Log) showDiff() error {
items, idx := l.prompt.list.Items()
+ if idx == NotFound {
+ return fmt.Errorf("there is no item to show diff")
+ }
commit := items[idx].(*git.Commit)
args := []string{"show", commit.Hash}
return popGitCommand(l.Repo, args)
@@ -127,6 +136,9 @@ func (l *Log) showDiff() error {
func (l *Log) showStat() error {
items, idx := l.prompt.list.Items()
+ if idx == NotFound {
+ return fmt.Errorf("there is no item to show diff")
+ }
commit := items[idx].(*git.Commit)
args := []string{"show", "--stat", commit.Hash}
return popGitCommand(l.Repo, args)
diff --git a/prompt/prompt.go b/prompt/prompt.go
index 50d8466..978ad1b 100644
--- a/prompt/prompt.go
+++ b/prompt/prompt.go
@@ -12,16 +12,6 @@ import (
"github.com/isacikgoz/gitin/term"
)
-type promptType int
-
-const (
- status promptType = iota
- log
- file
- branch
- stash
-)
-
type keyEvent struct {
ch rune
err error
@@ -48,8 +38,6 @@ type promptState struct {
}
type prompt struct {
- layout promptType
-
list *List
keys onKey
selection onSelect
@@ -112,12 +100,11 @@ func (p *prompt) start() error {
return err
}
- // if p.exitMsg != nil {
for _, cells := range p.exitMsg {
p.writer.WriteCells(cells)
}
p.writer.Flush()
- // }
+
return nil
}
@@ -160,13 +147,15 @@ mainloop:
func (p *prompt) render() {
// lock screen mutex
p.mx.Lock()
- defer p.mx.Unlock()
+ defer func() {
+ p.writer.Flush()
+ p.mx.Unlock()
+ }()
if p.helpMode {
for _, line := range genHelp(p.allControls()) {
p.writer.WriteCells(line)
}
- p.writer.Flush()
return
}
@@ -188,9 +177,6 @@ func (p *prompt) render() {
} else {
p.writer.WriteCells(term.Cprint("Not found.", color.FgRed))
}
-
- // finally, discharge to terminal
- p.writer.Flush()
}
func (p *prompt) assignKey(key rune) bool {
diff --git a/prompt/status.go b/prompt/status.go
index 6538497..48ccb94 100644
--- a/prompt/status.go
+++ b/prompt/status.go
@@ -47,7 +47,6 @@ func (s *Status) Start(opts *Options) error {
s.prompt = &prompt{
list: l,
opts: opts,
- layout: status,
keys: s.onKey,
selection: s.onSelect,
info: s.branchInfo,
@@ -70,7 +69,6 @@ func (s *Status) onSelect() bool {
func (s *Status) onKey(key rune) bool {
var reqReload bool
-
switch key {
case ' ':
reqReload = true
@@ -178,6 +176,9 @@ func (s *Status) hunkStage() error {
// pop git diff
func (s *Status) showDiff() error {
items, idx := s.prompt.list.Items()
+ if idx == NotFound {
+ return fmt.Errorf("there is no item to show diff")
+ }
entry := items[idx].(*git.StatusEntry)
return popGitCommand(s.Repo, fileStatArgs(entry))
}
From 9d533ebfeecda23ad08bdfd57d8fa21c03bfdfb6 Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Tue, 18 Jun 2019 14:20:49 +0300
Subject: [PATCH 02/13] bump version
---
main.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/main.go b/main.go
index 51748e6..93174b3 100644
--- a/main.go
+++ b/main.go
@@ -33,7 +33,7 @@ func evalArgs() string {
pin.Command("status", "Show working-tree status. Also stage and commit changes.")
pin.Command("branch", "Show list of branches.")
- pin.Version("gitin version 0.2.0")
+ pin.Version("gitin version 0.2.1")
pin.UsageTemplate(pin.DefaultUsageTemplate + additionalHelp() + "\n")
pin.CommandLine.HelpFlag.Short('h')
From 786cb77946cca3732cb0b538deb906663934801d Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Tue, 18 Jun 2019 16:46:45 +0300
Subject: [PATCH 03/13] initialize prompt with more readable way
---
prompt/prompt.go | 125 ++++++++++++++++++++++++++++++++++++++---------
1 file changed, 101 insertions(+), 24 deletions(-)
diff --git a/prompt/prompt.go b/prompt/prompt.go
index 978ad1b..096d29b 100644
--- a/prompt/prompt.go
+++ b/prompt/prompt.go
@@ -19,7 +19,7 @@ type keyEvent struct {
type onKey func(rune) bool
type onSelect func() bool
-type grid func(Item) [][]term.Cell
+type genInfo func(Item) [][]term.Cell
// Options is the common options for building a prompt
type Options struct {
@@ -38,25 +38,74 @@ type promptState struct {
}
type prompt struct {
- list *List
+ list *List
+ opts *Options
+
keys onKey
selection onSelect
- info grid
- exitMsg [][]term.Cell
- controls map[string]string
+ info genInfo
+
+ exitMsg [][]term.Cell // to be set on runtime if required
+ controls map[string]string // to be updated if additional controls added
+
inputMode bool
helpMode bool
input string
- reader *term.RuneReader
- writer *term.BufferedWriter
- mx *sync.RWMutex
- opts *Options
+
+ reader *term.RuneReader // initialized by prompt
+ writer *term.BufferedWriter // initialized by prompt
+ mx *sync.RWMutex
events chan keyEvent
quit chan bool
hold bool
}
+type optionalFunc func(*prompt)
+
+func withOnKey(f onKey) optionalFunc {
+ return func(p *prompt) {
+ p.keys = f
+ }
+}
+
+func withSelection(f onSelect) optionalFunc {
+ return func(p *prompt) {
+ p.selection = f
+ }
+}
+
+func withInfo(f genInfo) optionalFunc {
+ return func(p *prompt) {
+ p.info = f
+ }
+}
+
+func create(opts *Options, list *List, fs ...optionalFunc) *prompt {
+ p := &prompt{
+ opts: opts,
+ list: list,
+ }
+
+ p.keys = p.onKey
+ p.selection = p.onSelect
+ p.info = p.genInfo
+
+ var mx sync.RWMutex
+ p.mx = &mx
+
+ p.reader = term.NewRuneReader(os.Stdin)
+ p.writer = term.NewBufferedWriter(os.Stdout)
+
+ p.events = make(chan keyEvent, 20)
+ p.quit = make(chan bool)
+
+ for _, f := range fs {
+ f(p)
+ }
+ return p
+}
+
func (p *prompt) start() error {
var mx sync.RWMutex
p.mx = &mx
@@ -80,21 +129,7 @@ func (p *prompt) start() error {
}
// start input loop
- go func() {
- for {
- select {
- case <-p.quit:
- return
- default:
- time.Sleep(10 * time.Millisecond)
- if p.hold {
- continue
- }
- r, _, err := p.reader.ReadRune()
- p.events <- keyEvent{ch: r, err: err}
- }
- }
- }()
+ go p.spawnEvent()
if err := p.innerRun(); err != nil {
return err
@@ -108,6 +143,22 @@ func (p *prompt) start() error {
return nil
}
+func (p *prompt) spawnEvent() {
+ for {
+ select {
+ case <-p.quit:
+ return
+ default:
+ time.Sleep(10 * time.Millisecond)
+ if p.hold {
+ continue
+ }
+ r, _, err := p.reader.ReadRune()
+ p.events <- keyEvent{ch: r, err: err}
+ }
+ }
+}
+
// this is the main loop for reading input channel
func (p *prompt) innerRun() error {
var err error
@@ -242,3 +293,29 @@ func (p *prompt) allControls() map[string]string {
}
return controls
}
+
+// onKey is the default keybinding function for a prompt
+func (p *prompt) onKey(key rune) bool {
+ switch key {
+ case 'q':
+ p.quit <- true
+ return true
+ default:
+ }
+ return false
+}
+
+// onSelect is the default selection
+func (p *prompt) onSelect() bool {
+ items, idx := p.list.Items()
+ if idx == NotFound {
+ return false
+ }
+ p.writer.WriteCells(term.Cprint(items[idx].String()))
+ return false
+}
+
+// genInfo is the default function to genereate info
+func (p *prompt) genInfo(item Item) [][]term.Cell {
+ return nil
+}
From 4d53128cd60e34ed00bb6ae4688879dc5c2e41c1 Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Tue, 18 Jun 2019 21:08:38 +0300
Subject: [PATCH 04/13] better prompt initialization
---
prompt/branch.go | 19 ++++++++--------
prompt/log.go | 37 ++++++++++++++----------------
prompt/prompt.go | 59 ++++++++++++++++++++++++++----------------------
prompt/status.go | 23 ++++++++++---------
4 files changed, 71 insertions(+), 67 deletions(-)
diff --git a/prompt/branch.go b/prompt/branch.go
index 9202b97..6532611 100644
--- a/prompt/branch.go
+++ b/prompt/branch.go
@@ -37,16 +37,17 @@ func (b *Branch) Start(opts *Options) error {
opts.SearchLabel = "Branches"
- b.prompt = &prompt{
- list: list,
- opts: opts,
- selection: b.onSelect,
- keys: b.onKey,
- info: b.branchInfo,
- controls: controls,
+ b.prompt = create(opts,
+ list,
+ withOnKey(b.onKey),
+ withSelection(b.onSelect),
+ withInfo(b.branchInfo),
+ )
+ b.prompt.controls = controls
+ if err := b.prompt.Run(); err != nil {
+ return err
}
-
- return b.prompt.start()
+ return nil
}
func (b *Branch) onSelect() bool {
diff --git a/prompt/log.go b/prompt/log.go
index ae01dbe..95f18af 100644
--- a/prompt/log.go
+++ b/prompt/log.go
@@ -43,16 +43,17 @@ func (l *Log) Start(opts *Options) error {
opts.SearchLabel = "Commits"
- l.prompt = &prompt{
- list: list,
- opts: opts,
- keys: l.onKey,
- selection: l.onSelect,
- info: l.logInfo,
- controls: controls,
+ l.prompt = create(opts,
+ list,
+ withOnKey(l.onKey),
+ withSelection(l.onSelect),
+ withInfo(l.logInfo),
+ )
+ l.prompt.controls = controls
+ if err := l.prompt.Run(); err != nil {
+ return err
}
-
- return l.prompt.start()
+ return nil
}
// return true to terminate
@@ -76,19 +77,17 @@ func (l *Log) onSelect() bool {
for _, delta := range deltas {
newlist = append(newlist, delta)
}
- l.oldState = &promptState{
- list: l.prompt.list,
- searchMode: l.prompt.inputMode,
- searchStr: l.prompt.input,
- }
+ l.oldState = l.prompt.getState()
list, err := NewList(newlist, 5)
if err != nil {
return false
}
+ l.prompt.setState(&promptState{
+ list: list,
+ searchMode: false,
+ searchStr: "",
+ })
l.prompt.opts.SearchLabel = "Files"
- l.prompt.input = ""
- l.prompt.inputMode = false
- l.prompt.list = list
case *git.DiffDelta:
l.showFileDiff()
}
@@ -115,9 +114,7 @@ func (l *Log) onKey(key rune) bool {
case *git.DiffDelta:
switch key {
case 'q':
- l.prompt.list = l.oldState.list
- l.prompt.inputMode = l.oldState.searchMode
- l.prompt.input = l.oldState.searchStr
+ l.prompt.setState(l.oldState)
l.prompt.opts.SearchLabel = "Commits"
}
}
diff --git a/prompt/prompt.go b/prompt/prompt.go
index 096d29b..6120212 100644
--- a/prompt/prompt.go
+++ b/prompt/prompt.go
@@ -63,24 +63,6 @@ type prompt struct {
type optionalFunc func(*prompt)
-func withOnKey(f onKey) optionalFunc {
- return func(p *prompt) {
- p.keys = f
- }
-}
-
-func withSelection(f onSelect) optionalFunc {
- return func(p *prompt) {
- p.selection = f
- }
-}
-
-func withInfo(f genInfo) optionalFunc {
- return func(p *prompt) {
- p.info = f
- }
-}
-
func create(opts *Options, list *List, fs ...optionalFunc) *prompt {
p := &prompt{
opts: opts,
@@ -106,16 +88,25 @@ func create(opts *Options, list *List, fs ...optionalFunc) *prompt {
return p
}
-func (p *prompt) start() error {
- var mx sync.RWMutex
- p.mx = &mx
+func withOnKey(f onKey) optionalFunc {
+ return func(p *prompt) {
+ p.keys = f
+ }
+}
- p.reader = term.NewRuneReader(os.Stdin)
- p.writer = term.NewBufferedWriter(os.Stdout)
+func withSelection(f onSelect) optionalFunc {
+ return func(p *prompt) {
+ p.selection = f
+ }
+}
- p.events = make(chan keyEvent, 20)
- p.quit = make(chan bool)
+func withInfo(f genInfo) optionalFunc {
+ return func(p *prompt) {
+ p.info = f
+ }
+}
+func (p *prompt) Run() error {
// disable echo and hide cursor
if err := term.Init(os.Stdin, os.Stdout); err != nil {
return err
@@ -131,7 +122,7 @@ func (p *prompt) start() error {
// start input loop
go p.spawnEvent()
- if err := p.innerRun(); err != nil {
+ if err := p.mainloop(); err != nil {
return err
}
@@ -160,7 +151,7 @@ func (p *prompt) spawnEvent() {
}
// this is the main loop for reading input channel
-func (p *prompt) innerRun() error {
+func (p *prompt) mainloop() error {
var err error
sigwinch := make(chan os.Signal, 1)
signal.Notify(sigwinch, syscall.SIGWINCH)
@@ -319,3 +310,17 @@ func (p *prompt) onSelect() bool {
func (p *prompt) genInfo(item Item) [][]term.Cell {
return nil
}
+
+func (p *prompt) getState() *promptState {
+ return &promptState{
+ list: p.list,
+ searchMode: p.inputMode,
+ searchStr: p.input,
+ }
+}
+
+func (p *prompt) setState(state *promptState) {
+ p.list = state.list
+ p.inputMode = state.searchMode
+ p.input = state.searchStr
+}
diff --git a/prompt/status.go b/prompt/status.go
index 48ccb94..a7b4bff 100644
--- a/prompt/status.go
+++ b/prompt/status.go
@@ -28,7 +28,7 @@ func (s *Status) Start(opts *Options) error {
items = append(items, entry)
}
- l, err := NewList(items, opts.Size)
+ list, err := NewList(items, opts.Size)
if err != nil {
return err
}
@@ -44,21 +44,22 @@ func (s *Status) Start(opts *Options) error {
opts.SearchLabel = "Files"
- s.prompt = &prompt{
- list: l,
- opts: opts,
- keys: s.onKey,
- selection: s.onSelect,
- info: s.branchInfo,
- controls: controls,
- }
-
if len(items) == 0 {
s.printClean()
return nil
}
- return s.prompt.start()
+ s.prompt = create(opts,
+ list,
+ withOnKey(s.onKey),
+ withSelection(s.onSelect),
+ withInfo(s.branchInfo),
+ )
+ s.prompt.controls = controls
+ if err := s.prompt.Run(); err != nil {
+ return err
+ }
+ return nil
}
// return true to terminate
From c1a9308a15c7a649671a22f6cdd767e04e5b754d Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Wed, 19 Jun 2019 12:14:54 +0300
Subject: [PATCH 05/13] decreased call stack a little
---
cli/branch.go | 6 ++----
cli/log.go | 6 ++----
cli/status.go | 6 ++----
main.go | 24 ++++++------------------
prompt/prompt.go | 27 ++++++++++++++-------------
5 files changed, 26 insertions(+), 43 deletions(-)
diff --git a/cli/branch.go b/cli/branch.go
index a93de29..7acd3af 100644
--- a/cli/branch.go
+++ b/cli/branch.go
@@ -27,7 +27,7 @@ func BranchPrompt(r *git.Repository, opts *prompt.Options) error {
for _, branch := range branches {
items = append(items, branch)
}
- list, err := prompt.NewList(items, opts.Size)
+ list, err := prompt.NewList(items, opts.LineSize)
if err != nil {
return err
}
@@ -36,10 +36,8 @@ func BranchPrompt(r *git.Repository, opts *prompt.Options) error {
controls["force delete"] = "D"
controls["checkout"] = "enter"
- opts.SearchLabel = "Branches"
b := &Branch{Repo: r}
- b.prompt = prompt.Create(opts,
- list,
+ b.prompt = prompt.Create("Branches", opts, list,
prompt.WithKeyHandler(b.onKey),
prompt.WithSelectionHandler(b.onSelect),
prompt.WithItemRenderer(renderItem),
diff --git a/cli/log.go b/cli/log.go
index 5336502..0a19347 100644
--- a/cli/log.go
+++ b/cli/log.go
@@ -33,7 +33,7 @@ func LogPrompt(r *git.Repository, opts *prompt.Options) error {
for _, commit := range cs {
items = append(items, commit)
}
- list, err := prompt.NewList(items, opts.Size)
+ list, err := prompt.NewList(items, opts.LineSize)
if err != nil {
return err
}
@@ -42,10 +42,8 @@ func LogPrompt(r *git.Repository, opts *prompt.Options) error {
controls["show stat"] = "s"
controls["select"] = "enter"
- opts.SearchLabel = "Commits"
l := &Log{Repo: r}
- l.prompt = prompt.Create(opts,
- list,
+ l.prompt = prompt.Create("Commits", opts, list,
prompt.WithKeyHandler(l.onKey),
prompt.WithSelectionHandler(l.onSelect),
prompt.WithItemRenderer(renderItem),
diff --git a/cli/status.go b/cli/status.go
index a1c19f5..2d93521 100644
--- a/cli/status.go
+++ b/cli/status.go
@@ -29,7 +29,7 @@ func StatusPrompt(r *git.Repository, opts *prompt.Options) error {
items = append(items, entry)
}
- list, err := prompt.NewList(items, opts.Size)
+ list, err := prompt.NewList(items, opts.LineSize)
if err != nil {
return err
}
@@ -43,14 +43,12 @@ func StatusPrompt(r *git.Repository, opts *prompt.Options) error {
controls["amend"] = "m"
controls["discard changes"] = "!"
- opts.SearchLabel = "Files"
-
s := &Status{Repo: r}
if len(items) == 0 {
s.printClean()
return nil
}
- s.prompt = prompt.Create(opts, list,
+ s.prompt = prompt.Create("Files", opts, list,
prompt.WithKeyHandler(s.onKey),
prompt.WithSelectionHandler(s.onSelect),
prompt.WithItemRenderer(renderItem),
diff --git a/main.go b/main.go
index 19b6c94..b5c8f21 100644
--- a/main.go
+++ b/main.go
@@ -12,13 +12,6 @@ import (
pin "gopkg.in/alecthomas/kingpin.v2"
)
-// Config will be passed to screenopts
-type Config struct {
- LineSize int `default:"5"`
- StartSearch bool
- DisableColor bool
-}
-
func main() {
mode := evalArgs()
pwd, _ := os.Getwd()
@@ -28,25 +21,20 @@ func main() {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
- var cfg Config
- err = env.Process("gitin", &cfg)
+ var opts prompt.Options
+ err = env.Process("gitin", &opts)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
- opts := &prompt.Options{
- Size: cfg.LineSize,
- StartInSearch: cfg.StartSearch,
- DisableColor: cfg.DisableColor,
- }
switch mode {
case "status":
- err = cli.StatusPrompt(r, opts)
+ err = cli.StatusPrompt(r, &opts)
case "log":
- err = cli.LogPrompt(r, opts)
+ err = cli.LogPrompt(r, &opts)
case "branch":
- err = cli.BranchPrompt(r, opts)
+ err = cli.BranchPrompt(r, &opts)
}
if err != nil {
@@ -74,7 +62,7 @@ func additionalHelp() string {
return `Environment Variables:
GITIN_LINESIZE=
- GITIN_STARTSEARCH=
+ GITIN_STARTINSEARCH=
GITIN_DISABLECOLOR=
Press ? for controls while application is running.`
diff --git a/prompt/prompt.go b/prompt/prompt.go
index 9a8e360..e32324a 100644
--- a/prompt/prompt.go
+++ b/prompt/prompt.go
@@ -28,9 +28,8 @@ type OptionalFunc func(*Prompt)
// Options is the common options for building a prompt
type Options struct {
- Size int
+ LineSize int `default:"5"`
StartInSearch bool
- SearchLabel string
DisableColor bool
}
@@ -57,9 +56,10 @@ type Prompt struct {
exitMsg [][]term.Cell // to be set on runtime if required
Controls map[string]string // to be updated if additional controls added
- inputMode bool
- helpMode bool
- input string
+ inputMode bool
+ helpMode bool
+ itemsLabel string
+ input string
reader *term.RuneReader // initialized by prompt
writer *term.BufferedWriter // initialized by prompt
@@ -71,10 +71,11 @@ type Prompt struct {
}
// Create returns a pointer to prompt that is ready to Run
-func Create(opts *Options, list *List, fs ...OptionalFunc) *Prompt {
+func Create(label string, opts *Options, list *List, fs ...OptionalFunc) *Prompt {
p := &Prompt{
- opts: opts,
- list: list,
+ opts: opts,
+ list: list,
+ itemsLabel: label,
}
p.keyHandler = p.onKey
@@ -231,7 +232,7 @@ func (p *Prompt) render() {
}
items, idx := p.list.Items()
- p.writer.WriteCells(renderSearch(p.opts.SearchLabel, p.inputMode, p.input))
+ p.writer.WriteCells(renderSearch(p.itemsLabel, p.inputMode, p.input))
for i := range items {
var output []term.Cell
@@ -347,7 +348,7 @@ func (p *Prompt) State() *State {
List: p.list,
SearchMode: p.inputMode,
SearchStr: p.input,
- SearchLabel: p.opts.SearchLabel,
+ SearchLabel: p.itemsLabel,
Cursor: idx,
ListSize: p.list.size,
}
@@ -358,13 +359,13 @@ func (p *Prompt) SetState(state *State) {
p.list = state.List
p.inputMode = state.SearchMode
p.input = state.SearchStr
- p.opts.SearchLabel = state.SearchLabel
+ p.itemsLabel = state.SearchLabel
p.list.SetCursor(state.Cursor)
}
// ListSize returns the size of the items that is renderer each time
func (p *Prompt) ListSize() int {
- return p.opts.Size
+ return p.opts.LineSize
}
// Selection returns the selected item
@@ -376,7 +377,7 @@ func (p *Prompt) Selection() (Item, error) {
return items[idx], nil
}
-// SetExitMsg adds a rendered cell grid to be rendered after prompt is finished
+// SetExitMsg adds a rendered cell grid to be printed after prompt is finished
func (p *Prompt) SetExitMsg(grid [][]term.Cell) {
p.exitMsg = grid
}
From 0f85f451a0a1f12ac709eddef6e35ed050169ae4 Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Wed, 19 Jun 2019 14:41:27 +0300
Subject: [PATCH 06/13] update README
---
README.md | 35 +++++++++++++++++++++++++++--------
1 file changed, 27 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 2b5b634..6a3afe8 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ gitin is a minimalist tool that lets you explore a git repository from the comma
## Features
+
- Fuzzy search (type `/` to start a search after running `gitin `)
- Interactive stage and see the diff of files (`gitin status` then press `enter` to see diff or `space` to stage)
- Commit/amend changes (`gitin status` then press `c` to commit or `m` to amend)
@@ -20,21 +21,26 @@ gitin is a minimalist tool that lets you explore a git repository from the comma
- See more options by running `gitin --help`, also you can get help for individual subcommands (e.g. `gitin log --help`)
## Installation
-- Linux and macOS are supported, haven't tried on Windows.
+
+- Linux and macOS are supported, Windows is not at the moment.
- Download latest release from [here](https://github.com/isacikgoz/gitin/releases)
- **Or**, manually download it with `go get -d github.com/isacikgoz/gitin`
- `cd` into `$GOPATH/src/github.com/isacikgoz/gitin`
+- make would expect a built libgit2 library to make a static link. So, when you run `make` command, you should be able to build libgit2 at your `$GOPATH/pkg/mod/gopkg.in/libgit2/git2go.../vendor/libgit2/build` directory. This issue has been shown up after go modules.
- build with `make install` (`cmake` and `pkg-config` are required)
### Mac/Linux using brew
+
The tap is recently moved to new repo, so if you added the older one (isacikgoz/gitin), consider removing it and adding the new one.
-```
+
+```sh
brew tap isacikgoz/taps
brew install gitin
```
## Usage
-```bash
+
+```sh
usage: gitin [] [ ...]
Flags:
@@ -45,23 +51,33 @@ Commands:
help [...]
Show help.
- branch []
- Checkout, list, or delete branches.
-
- log []
+ log
Show commit logs.
status
- Show working-tree status. Also, stage and commit changes.
+ Show working-tree status. Also stage and commit changes.
+
+ branch
+ Show list of branches.
+
+Environment Variables:
+
+ GITIN_LINESIZE=
+ GITIN_STARTINSEARCH=
+ GITIN_DISABLECOLOR=
+
+Press ? for controls while application is running.
```
## Configure
+
- To set the line size `export GITIN_LINESIZE=5`
- To set always start in search mode `GITIN_STARTSEARCH=true`
- To disable colors `GITIN_DISABLECOLOR=true`
## Development Requirements
+
- Requires gitlib2 v27 and `git2go`. See the project homepages for more information about build instructions. For gitin you can simply;
- macOS:
1. install libgit2 via `brew install libgit2` (consider that libgit2.v27 is required)
@@ -76,12 +92,15 @@ Commands:
- `cd` into `$GOPATH/src/github.com/isacikgoz/gitin` and start hacking
## Contribution
+
- Contributions are welcome. If you like to please refer to [Contribution Guidelines](/CONTRIBUTING.md)
- Bug reports should include descriptive steps to reproduce so that maintainers can easily understand the actual problem
- Feature requests are welcome, ask for anything that seems appropriate
## Credits
+
See the [credits page](https://github.com/isacikgoz/gitin/wiki/Credits)
## License
+
[BSD-3-Clause](/LICENSE)
From a2d60cff8edbe144d11524d4b5cd6c7c828de683 Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Wed, 19 Jun 2019 21:54:29 +0300
Subject: [PATCH 07/13] improve on call stack contd.
---
cli/branch.go | 50 +++++++-------
cli/log.go | 63 +++++++++--------
cli/{common.go => rendering.go} | 0
cli/status.go | 116 ++++++++++++++++----------------
go.mod | 2 +-
main.go | 19 ++++--
prompt/prompt.go | 61 +++++++++--------
7 files changed, 158 insertions(+), 153 deletions(-)
rename cli/{common.go => rendering.go} (100%)
diff --git a/cli/branch.go b/cli/branch.go
index 7acd3af..0288745 100644
--- a/cli/branch.go
+++ b/cli/branch.go
@@ -11,17 +11,17 @@ import (
"github.com/justincampbell/timeago"
)
-// Branch holds a list of items used to fill the terminal screen.
-type Branch struct {
- Repo *git.Repository
- prompt *prompt.Prompt
+// branch holds a list of items used to fill the terminal screen.
+type branch struct {
+ repository *git.Repository
+ prompt *prompt.Prompt
}
// BranchPrompt draws the screen with its list, initializing the cursor to the given position.
-func BranchPrompt(r *git.Repository, opts *prompt.Options) error {
+func BranchPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, error) {
branches, err := r.Branches()
if err != nil {
- return err
+ return nil, fmt.Errorf("could not load branches: %v", err)
}
items := make([]prompt.Item, 0)
for _, branch := range branches {
@@ -29,14 +29,14 @@ func BranchPrompt(r *git.Repository, opts *prompt.Options) error {
}
list, err := prompt.NewList(items, opts.LineSize)
if err != nil {
- return err
+ return nil, fmt.Errorf("could not create list: %v", err)
}
controls := make(map[string]string)
controls["delete branch"] = "d"
controls["force delete"] = "D"
controls["checkout"] = "enter"
- b := &Branch{Repo: r}
+ b := &branch{repository: r}
b.prompt = prompt.Create("Branches", opts, list,
prompt.WithKeyHandler(b.onKey),
prompt.WithSelectionHandler(b.onSelect),
@@ -44,28 +44,27 @@ func BranchPrompt(r *git.Repository, opts *prompt.Options) error {
prompt.WithInformation(b.branchInfo),
)
b.prompt.Controls = controls
- if err := b.prompt.Run(); err != nil {
- return err
- }
- return nil
+
+ return b.prompt, nil
}
-func (b *Branch) onSelect() bool {
+func (b *branch) onSelect() error {
item, err := b.prompt.Selection()
if err != nil {
- return false
+ return nil
}
branch := item.(*git.Branch)
args := []string{"checkout", branch.Name}
cmd := exec.Command("git", args...)
- cmd.Dir = b.Repo.Path()
+ cmd.Dir = b.repository.Path()
if err := cmd.Run(); err != nil {
- return false
+ return nil // possibly dirty branch
}
- return true
+ b.prompt.Stop() // quit after selection
+ return nil
}
-func (b *Branch) onKey(key rune) bool {
+func (b *branch) onKey(key rune) error {
switch key {
case 'd':
b.deleteBranch("d")
@@ -73,12 +72,11 @@ func (b *Branch) onKey(key rune) bool {
b.deleteBranch("D")
case 'q':
b.prompt.Stop()
- return true
}
- return false
+ return nil
}
-func (b *Branch) branchInfo(item prompt.Item) [][]term.Cell {
+func (b *branch) branchInfo(item prompt.Item) [][]term.Cell {
branch := item.(*git.Branch)
target := branch.Target()
grid := make([][]term.Cell, 0)
@@ -94,23 +92,23 @@ func (b *Branch) branchInfo(item prompt.Item) [][]term.Cell {
return grid
}
-func (b *Branch) deleteBranch(mode string) error {
+func (b *branch) deleteBranch(mode string) error {
item, err := b.prompt.Selection()
if err != nil {
return fmt.Errorf("could not delete branch: %v", err)
}
branch := item.(*git.Branch)
cmd := exec.Command("git", "branch", "-"+mode, branch.Name)
- cmd.Dir = b.Repo.Path()
+ cmd.Dir = b.repository.Path()
if err := cmd.Run(); err != nil {
- return err
+ return err // possibly an unmerged branch
}
return b.reloadBranches()
}
// reloads the list
-func (b *Branch) reloadBranches() error {
- branches, err := b.Repo.Branches()
+func (b *branch) reloadBranches() error {
+ branches, err := b.repository.Branches()
if err != nil {
return err
}
diff --git a/cli/log.go b/cli/log.go
index 0a19347..620c07a 100644
--- a/cli/log.go
+++ b/cli/log.go
@@ -12,22 +12,22 @@ import (
"github.com/justincampbell/timeago"
)
-// Log holds a list of items used to fill the terminal screen.
-type Log struct {
- Repo *git.Repository
-
- prompt *prompt.Prompt
- selected *git.Commit
- oldState *prompt.State
+// log holds the repository struct and the prompt pointer. since log and prompt dependent,
+// I found the best wau to associate them with this way
+type log struct {
+ repository *git.Repository
+ prompt *prompt.Prompt
+ selected *git.Commit
+ oldState *prompt.State
}
// LogPrompt draws the screen with its list, initializing the cursor to the given position.
-func LogPrompt(r *git.Repository, opts *prompt.Options) error {
+func LogPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, error) {
cs, err := r.Commits()
if err != nil {
- return err
+ return nil, fmt.Errorf("could not load commits: %v", err)
}
- r.Branches()
+ r.Branches() // to find refs
r.Tags()
items := make([]prompt.Item, 0)
for _, commit := range cs {
@@ -35,14 +35,14 @@ func LogPrompt(r *git.Repository, opts *prompt.Options) error {
}
list, err := prompt.NewList(items, opts.LineSize)
if err != nil {
- return err
+ return nil, fmt.Errorf("could not create list: %v", err)
}
controls := make(map[string]string)
controls["show diff"] = "d"
controls["show stat"] = "s"
controls["select"] = "enter"
- l := &Log{Repo: r}
+ l := &log{repository: r}
l.prompt = prompt.Create("Commits", opts, list,
prompt.WithKeyHandler(l.onKey),
prompt.WithSelectionHandler(l.onSelect),
@@ -50,18 +50,16 @@ func LogPrompt(r *git.Repository, opts *prompt.Options) error {
prompt.WithInformation(l.logInfo),
)
l.prompt.Controls = controls
- if err := l.prompt.Run(); err != nil {
- return err
- }
- return nil
+
+ return l.prompt, nil
}
// return true to terminate
-func (l *Log) onSelect() bool {
+func (l *log) onSelect() error {
item, err := l.prompt.Selection()
if err != nil {
- return false
+ return nil
}
switch item.(type) {
case *git.Commit:
@@ -69,7 +67,7 @@ func (l *Log) onSelect() bool {
l.selected = commit
diff, err := commit.Diff()
if err != nil {
- return false
+ return nil
}
deltas := diff.Deltas()
newlist := make([]prompt.Item, 0)
@@ -79,7 +77,7 @@ func (l *Log) onSelect() bool {
l.oldState = l.prompt.State()
list, err := prompt.NewList(newlist, 5)
if err != nil {
- return false
+ return err
}
l.prompt.SetState(&prompt.State{
List: list,
@@ -91,15 +89,15 @@ func (l *Log) onSelect() bool {
case *git.DiffDelta:
l.showFileDiff()
}
- return false
+ return nil
}
-func (l *Log) onKey(key rune) bool {
+func (l *log) onKey(key rune) error {
var item prompt.Item
var err error
item, err = l.prompt.Selection()
if err != nil {
- return false
+ return err
}
switch item.(type) {
case *git.Commit:
@@ -110,7 +108,6 @@ func (l *Log) onKey(key rune) bool {
l.showDiff()
case 'q':
l.prompt.Stop()
- return true
}
case *git.DiffDelta:
switch key {
@@ -118,30 +115,30 @@ func (l *Log) onKey(key rune) bool {
l.prompt.SetState(l.oldState)
}
}
- return false
+ return nil
}
-func (l *Log) showDiff() error {
+func (l *log) showDiff() error {
item, err := l.prompt.Selection()
if err != nil {
return fmt.Errorf("there is no item to show diff")
}
commit := item.(*git.Commit)
args := []string{"show", commit.Hash}
- return popGitCommand(l.Repo, args)
+ return popGitCommand(l.repository, args)
}
-func (l *Log) showStat() error {
+func (l *log) showStat() error {
item, err := l.prompt.Selection()
if err != nil {
return fmt.Errorf("there is no item to show diff")
}
commit := item.(*git.Commit)
args := []string{"show", "--stat", commit.Hash}
- return popGitCommand(l.Repo, args)
+ return popGitCommand(l.repository, args)
}
-func (l *Log) showFileDiff() error {
+func (l *log) showFileDiff() error {
if l.selected == nil {
return nil
}
@@ -158,10 +155,10 @@ func (l *Log) showFileDiff() error {
}
dd := item.(*git.DiffDelta)
args = append(args, dd.OldFile.Path)
- return popGitCommand(l.Repo, args)
+ return popGitCommand(l.repository, args)
}
-func (l *Log) logInfo(item prompt.Item) [][]term.Cell {
+func (l *log) logInfo(item prompt.Item) [][]term.Cell {
grid := make([][]term.Cell, 0)
if item == nil {
return grid
@@ -175,7 +172,7 @@ func (l *Log) logInfo(item prompt.Item) [][]term.Cell {
cells = term.Cprint("When", color.Faint)
cells = append(cells, term.Cprint(" "+timeago.FromTime(commit.Author.When), color.FgWhite)...)
grid = append(grid, cells)
- grid = append(grid, commitRefs(l.Repo, commit))
+ grid = append(grid, commitRefs(l.repository, commit))
return grid
case *git.DiffDelta:
dd := item.(*git.DiffDelta)
diff --git a/cli/common.go b/cli/rendering.go
similarity index 100%
rename from cli/common.go
rename to cli/rendering.go
diff --git a/cli/status.go b/cli/status.go
index 2d93521..7a5be9f 100644
--- a/cli/status.go
+++ b/cli/status.go
@@ -11,27 +11,29 @@ import (
git "github.com/isacikgoz/libgit2-api"
)
-// Status holds a list of items used to fill the terminal screen.
-type Status struct {
- Repo *git.Repository
-
- prompt *prompt.Prompt
+// status holds the repository struct and the prompt pointer.
+type status struct {
+ repository *git.Repository
+ prompt *prompt.Prompt
}
// StatusPrompt draws the screen with its list, initializing the cursor to the given position.
-func StatusPrompt(r *git.Repository, opts *prompt.Options) error {
+func StatusPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, error) {
st, err := r.LoadStatus()
if err != nil {
- return err
+ return nil, fmt.Errorf("could not load status: %v", err)
}
items := make([]prompt.Item, 0)
for _, entry := range st.Entities {
items = append(items, entry)
}
-
+ if len(items) == 0 {
+ printClean(r)
+ os.Exit(0)
+ }
list, err := prompt.NewList(items, opts.LineSize)
if err != nil {
- return err
+ return nil, fmt.Errorf("could not create list: %v", err)
}
controls := make(map[string]string)
controls["add/reset entry"] = "space"
@@ -43,11 +45,8 @@ func StatusPrompt(r *git.Repository, opts *prompt.Options) error {
controls["amend"] = "m"
controls["discard changes"] = "!"
- s := &Status{Repo: r}
- if len(items) == 0 {
- s.printClean()
- return nil
- }
+ s := &status{repository: r}
+
s.prompt = prompt.Create("Files", opts, list,
prompt.WithKeyHandler(s.onKey),
prompt.WithSelectionHandler(s.onSelect),
@@ -55,19 +54,25 @@ func StatusPrompt(r *git.Repository, opts *prompt.Options) error {
prompt.WithInformation(s.branchInfo),
)
s.prompt.Controls = controls
- if err := s.prompt.Run(); err != nil {
- return err
- }
- return nil
+
+ return s.prompt, nil
}
// return true to terminate
-func (s *Status) onSelect() bool {
- s.showDiff()
- return false
+func (s *status) onSelect() error {
+ item, err := s.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("can't show diff: %v", err)
+ }
+ entry := item.(*git.StatusEntry)
+ if err = popGitCommand(s.repository, fileStatArgs(entry)); err != nil {
+ // return fmt.Errorf("could not run a git command: %v", err)
+ return nil // intentionally ignore errors here
+ }
+ return nil
}
-func (s *Status) onKey(key rune) bool {
+func (s *status) onKey(key rune) error {
var reqReload bool
switch key {
case ' ':
@@ -85,29 +90,29 @@ func (s *Status) onKey(key rune) bool {
case 'a':
reqReload = true
// TODO: check for errors
- addAll(s.Repo)
+ addAll(s.repository)
case 'r':
reqReload = true
- resetAll(s.Repo)
+ resetAll(s.repository)
case '!':
reqReload = true
s.discardChanges()
case 'q':
s.prompt.Stop()
- return true
default:
}
if reqReload {
if err := s.reloadStatus(); err != nil {
- return true
+ return err
}
}
- return false
+ return nil
}
// reloads the list
-func (s *Status) reloadStatus() error {
- status, err := s.Repo.LoadStatus()
+func (s *status) reloadStatus() error {
+ s.repository.LoadHead()
+ status, err := s.repository.LoadStatus()
if err != nil {
return err
}
@@ -118,8 +123,8 @@ func (s *Status) reloadStatus() error {
if len(items) == 0 {
// this is the case when the working tree is cleaned at runtime
s.prompt.Stop()
- s.prompt.SetExitMsg(workingTreeClean(s.Repo.Head))
- return fmt.Errorf("quit")
+ s.prompt.SetExitMsg(workingTreeClean(s.repository.Head))
+ return nil
}
state := s.prompt.State()
list, err := prompt.NewList(items, state.ListSize)
@@ -132,7 +137,7 @@ func (s *Status) reloadStatus() error {
}
// add or reset selected entry
-func (s *Status) addReset() error {
+func (s *status) addReset() error {
item, err := s.prompt.Selection()
if err != nil {
return fmt.Errorf("can't add/reset item: %v", err)
@@ -143,7 +148,7 @@ func (s *Status) addReset() error {
args = []string{"reset", "HEAD", "--", entry.String()}
}
cmd := exec.Command("git", args...)
- cmd.Dir = s.Repo.Path()
+ cmd.Dir = s.repository.Path()
if err := cmd.Run(); err != nil {
return err
}
@@ -151,7 +156,7 @@ func (s *Status) addReset() error {
}
// open hunk stagin ui
-func (s *Status) hunkStage() error {
+func (s *status) hunkStage() error {
// defer s.prompt.writer.HideCursor()
item, err := s.prompt.Selection()
@@ -159,7 +164,7 @@ func (s *Status) hunkStage() error {
return fmt.Errorf("can't hunk stage item: %v", err)
}
entry := item.(*git.StatusEntry)
- file, err := generateDiffFile(s.Repo, entry)
+ file, err := generateDiffFile(s.repository, entry)
if err == nil {
editor, err := editor.NewEditor(file)
if err != nil {
@@ -170,7 +175,7 @@ func (s *Status) hunkStage() error {
return err
}
for _, patch := range patches {
- if err := applyPatchCmd(s.Repo, entry, patch); err != nil {
+ if err := applyPatchCmd(s.repository, entry, patch); err != nil {
return err
}
}
@@ -180,57 +185,50 @@ func (s *Status) hunkStage() error {
return nil
}
-func (s *Status) showDiff() error {
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("can't show diff: %v", err)
- }
- entry := item.(*git.StatusEntry)
- return popGitCommand(s.Repo, fileStatArgs(entry))
-}
-
-func (s *Status) doCommit() error {
+func (s *status) doCommit() error {
// defer s.prompt.writer.HideCursor()
args := []string{"commit", "--edit", "--quiet"}
- err := popGitCommand(s.Repo, args)
+ err := popGitCommand(s.repository, args)
if err != nil {
return err
}
- args, err = lastCommitArgs(s.Repo)
+ s.repository.LoadHead()
+ args, err = lastCommitArgs(s.repository)
if err != nil {
return err
}
- if err := popGitCommand(s.Repo, args); err != nil {
+ if err := popGitCommand(s.repository, args); err != nil {
return err
}
return nil
}
-func (s *Status) doCommitAmend() error {
+func (s *status) doCommitAmend() error {
// defer s.prompt.writer.HideCursor()
args := []string{"commit", "--amend", "--quiet"}
- err := popGitCommand(s.Repo, args)
+ err := popGitCommand(s.repository, args)
if err != nil {
return err
}
- args, err = lastCommitArgs(s.Repo)
+ s.repository.LoadHead()
+ args, err = lastCommitArgs(s.repository)
if err != nil {
return err
}
- if err := popGitCommand(s.Repo, args); err != nil {
+ if err := popGitCommand(s.repository, args); err != nil {
return err
}
return nil
}
-func (s *Status) branchInfo(item prompt.Item) [][]term.Cell {
- b := s.Repo.Head
+func (s *status) branchInfo(item prompt.Item) [][]term.Cell {
+ b := s.repository.Head
return branchInfo(b, true)
}
-func (s *Status) discardChanges() error {
+func (s *status) discardChanges() error {
// defer s.prompt.render()
item, err := s.prompt.Selection()
if err != nil {
@@ -239,16 +237,16 @@ func (s *Status) discardChanges() error {
entry := item.(*git.StatusEntry)
args := []string{"checkout", "--", entry.String()}
cmd := exec.Command("git", args...)
- cmd.Dir = s.Repo.Path()
+ cmd.Dir = s.repository.Path()
if err := cmd.Run(); err != nil {
return err
}
return nil
}
-func (s *Status) printClean() {
+func printClean(r *git.Repository) {
writer := term.NewBufferedWriter(os.Stdout)
- for _, line := range workingTreeClean(s.Repo.Head) {
+ for _, line := range workingTreeClean(r.Head) {
writer.WriteCells(line)
}
writer.Flush()
diff --git a/go.mod b/go.mod
index 9837129..1407622 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
github.com/fatih/color v1.7.0
github.com/isacikgoz/gia v0.2.0
- github.com/isacikgoz/libgit2-api v0.1.3
+ github.com/isacikgoz/libgit2-api v0.1.5
github.com/justincampbell/bigduration v0.0.0-20160531141349-e45bf03c0666 // indirect
github.com/justincampbell/timeago v0.0.0-20160528003754-027f40306f1d
github.com/kelseyhightower/envconfig v1.4.0
diff --git a/main.go b/main.go
index b5c8f21..83c7611 100644
--- a/main.go
+++ b/main.go
@@ -21,26 +21,33 @@ func main() {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
- var opts prompt.Options
- err = env.Process("gitin", &opts)
+ var o prompt.Options
+ err = env.Process("gitin", &o)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
+ var p *prompt.Prompt
+ // cli package is for responsible to create and configure a prompt
switch mode {
case "status":
- err = cli.StatusPrompt(r, &opts)
+ p, err = cli.StatusPrompt(r, &o)
case "log":
- err = cli.LogPrompt(r, &opts)
+ p, err = cli.LogPrompt(r, &o)
case "branch":
- err = cli.BranchPrompt(r, &opts)
+ p, err = cli.BranchPrompt(r, &o)
+ default:
+ return
}
-
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
+ if err := p.Run(); err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
+ }
}
// define the program commands and args
diff --git a/prompt/prompt.go b/prompt/prompt.go
index e32324a..995a9a4 100644
--- a/prompt/prompt.go
+++ b/prompt/prompt.go
@@ -18,8 +18,8 @@ type keyEvent struct {
err error
}
-type keyHandlerFunc func(rune) bool
-type selectionHandlerFunc func() bool
+type keyHandlerFunc func(rune) error
+type selectionHandlerFunc func() error
type itemRendererFunc func(Item, []int, bool) []term.Cell
type informationRendererFunc func(Item) [][]term.Cell
@@ -40,6 +40,7 @@ type State struct {
SearchStr string
SearchLabel string
Cursor int
+ Scroll int
ListSize int
}
@@ -65,9 +66,10 @@ type Prompt struct {
writer *term.BufferedWriter // initialized by prompt
mx *sync.RWMutex
- events chan keyEvent
- quit chan bool
- hold bool
+ events chan keyEvent
+ interrupt chan struct{}
+ quit chan struct{}
+ hold bool
}
// Create returns a pointer to prompt that is ready to Run
@@ -90,7 +92,8 @@ func Create(label string, opts *Options, list *List, fs ...OptionalFunc) *Prompt
p.writer = term.NewBufferedWriter(os.Stdout)
p.events = make(chan keyEvent, 20)
- p.quit = make(chan bool)
+ p.interrupt = make(chan struct{})
+ p.quit = make(chan struct{})
for _, f := range fs {
f(p)
@@ -157,14 +160,16 @@ func (p *Prompt) Run() error {
// Stop sends a quit signal to the main loop of the prompt
func (p *Prompt) Stop() {
- p.quit <- true
+ p.interrupt <- struct{}{}
}
func (p *Prompt) spawnEvents() {
for {
select {
- case <-p.quit:
- return
+ case <-p.interrupt:
+ p.quit <- struct{}{}
+ close(p.events)
+ break
default:
time.Sleep(10 * time.Millisecond)
if p.hold {
@@ -180,12 +185,15 @@ func (p *Prompt) spawnEvents() {
func (p *Prompt) mainloop() error {
var err error
sigwinch := make(chan os.Signal, 1)
+ defer close(sigwinch)
signal.Notify(sigwinch, syscall.SIGWINCH)
p.render()
mainloop:
for {
select {
+ case <-p.quit:
+ break mainloop
case ev := <-p.events:
p.hold = true
if err := ev.err; err != nil {
@@ -195,11 +203,11 @@ mainloop:
case rune(term.KeyCtrlC), rune(term.KeyCtrlD):
break mainloop
case term.Enter, term.NewLine:
- if br := p.selectionHandler(); br {
+ if err = p.selectionHandler(); err != nil {
break mainloop
}
default:
- if br := p.keyBindings(r); br {
+ if err = p.keyBindings(r); err != nil {
break mainloop
}
}
@@ -235,8 +243,7 @@ func (p *Prompt) render() {
p.writer.WriteCells(renderSearch(p.itemsLabel, p.inputMode, p.input))
for i := range items {
- var output []term.Cell
- output = append(output, p.itemRenderer(items[i], p.list.matches[items[i]], (i == idx))...)
+ output := p.itemRenderer(items[i], p.list.matches[items[i]], (i == idx))
p.writer.WriteCells(output)
}
@@ -250,10 +257,10 @@ func (p *Prompt) render() {
}
}
-func (p *Prompt) keyBindings(key rune) bool {
+func (p *Prompt) keyBindings(key rune) error {
if p.helpMode {
p.helpMode = false
- return false
+ return nil
}
switch key {
case term.ArrowUp:
@@ -298,7 +305,7 @@ func (p *Prompt) keyBindings(key rune) bool {
return p.keyHandler(key)
}
}
- return false
+ return nil
}
func (p *Prompt) allControls() map[string]string {
@@ -313,24 +320,23 @@ func (p *Prompt) allControls() map[string]string {
}
// onKey is the default keybinding function for a prompt
-func (p *Prompt) onKey(key rune) bool {
+func (p *Prompt) onKey(key rune) error {
switch key {
case 'q':
- p.quit <- true
- return true
+ p.Stop()
default:
}
- return false
+ return nil
}
// onSelect is the default selection
-func (p *Prompt) onSelect() bool {
+func (p *Prompt) onSelect() error {
items, idx := p.list.Items()
if idx == NotFound {
- return false
+ return fmt.Errorf("could not select an item")
}
p.writer.WriteCells(term.Cprint(items[idx].String()))
- return false
+ return nil
}
// genInfo is the default function to genereate info
@@ -340,16 +346,14 @@ func (p *Prompt) genInfo(item Item) [][]term.Cell {
// State return the current replace-able vars as a struct
func (p *Prompt) State() *State {
- var idx int
- if _, i := p.list.Items(); i != NotFound {
- idx = i
- }
+ scroll := p.list.Start()
return &State{
List: p.list,
SearchMode: p.inputMode,
SearchStr: p.input,
SearchLabel: p.itemsLabel,
- Cursor: idx,
+ Cursor: p.list.cursor,
+ Scroll: scroll,
ListSize: p.list.size,
}
}
@@ -361,6 +365,7 @@ func (p *Prompt) SetState(state *State) {
p.input = state.SearchStr
p.itemsLabel = state.SearchLabel
p.list.SetCursor(state.Cursor)
+ p.list.SetStart(state.Scroll)
}
// ListSize returns the size of the items that is renderer each time
From 570f631b169f9d06fc53e221cb059db9eaa83563 Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Wed, 19 Jun 2019 23:32:31 +0300
Subject: [PATCH 08/13] make following code a little bit easier
---
cli/branch.go | 6 +-
cli/commands.go | 14 ---
cli/log.go | 77 ++++++++---------
cli/status.go | 211 +++++++++++++++++++--------------------------
prompt/renderer.go | 14 +--
5 files changed, 132 insertions(+), 190 deletions(-)
diff --git a/cli/branch.go b/cli/branch.go
index 0288745..7040f59 100644
--- a/cli/branch.go
+++ b/cli/branch.go
@@ -67,9 +67,9 @@ func (b *branch) onSelect() error {
func (b *branch) onKey(key rune) error {
switch key {
case 'd':
- b.deleteBranch("d")
+ return b.deleteBranch("d")
case 'D':
- b.deleteBranch("D")
+ return b.deleteBranch("D")
case 'q':
b.prompt.Stop()
}
@@ -101,7 +101,7 @@ func (b *branch) deleteBranch(mode string) error {
cmd := exec.Command("git", "branch", "-"+mode, branch.Name)
cmd.Dir = b.repository.Path()
if err := cmd.Run(); err != nil {
- return err // possibly an unmerged branch
+ return nil // possibly an unmerged branch, just ignore it
}
return b.reloadBranches()
}
diff --git a/cli/commands.go b/cli/commands.go
index 0c8cbda..254ac60 100644
--- a/cli/commands.go
+++ b/cli/commands.go
@@ -87,17 +87,3 @@ func applyPatchCmd(r *git.Repository, entry *git.StatusEntry, patch string) erro
}
return nil
}
-
-// addAll is the wrapper of "git add ." command
-func addAll(r *git.Repository) error {
- cmd := exec.Command("git", "add", ".")
- cmd.Dir = r.Path()
- return cmd.Run()
-}
-
-// resetAll is the wrapper of "git reset" command
-func resetAll(r *git.Repository) error {
- cmd := exec.Command("git", "reset", "--mixed")
- cmd.Dir = r.Path()
- return cmd.Run()
-}
diff --git a/cli/log.go b/cli/log.go
index 620c07a..3005269 100644
--- a/cli/log.go
+++ b/cli/log.go
@@ -87,7 +87,25 @@ func (l *log) onSelect() error {
})
// l.prompt.opts.SearchLabel = "Files"
case *git.DiffDelta:
- l.showFileDiff()
+ if l.selected == nil {
+ return nil
+ }
+ var args []string
+ pid, err := l.selected.ParentID()
+ if err != nil {
+ args = []string{"show", "--oneline", "--patch"}
+ } else {
+ args = []string{"diff", pid + ".." + l.selected.Hash}
+ }
+ item, err := l.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("there is no item to show diff")
+ }
+ dd := item.(*git.DiffDelta)
+ args = append(args, dd.OldFile.Path)
+ if err := popGitCommand(l.repository, args); err != nil {
+ //no err handling required here
+ }
}
return nil
}
@@ -103,9 +121,22 @@ func (l *log) onKey(key rune) error {
case *git.Commit:
switch key {
case 's':
- l.showStat()
+ item, err := l.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("there is no item to show diff")
+ }
+ commit := item.(*git.Commit)
+ args := []string{"show", "--stat", commit.Hash}
+ return popGitCommand(l.repository, args)
case 'd':
- l.showDiff()
+ item, err := l.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("there is no item to show diff")
+ }
+ commit := item.(*git.Commit)
+ args := []string{"show", commit.Hash}
+ return popGitCommand(l.repository, args)
+
case 'q':
l.prompt.Stop()
}
@@ -118,46 +149,6 @@ func (l *log) onKey(key rune) error {
return nil
}
-func (l *log) showDiff() error {
- item, err := l.prompt.Selection()
- if err != nil {
- return fmt.Errorf("there is no item to show diff")
- }
- commit := item.(*git.Commit)
- args := []string{"show", commit.Hash}
- return popGitCommand(l.repository, args)
-}
-
-func (l *log) showStat() error {
- item, err := l.prompt.Selection()
- if err != nil {
- return fmt.Errorf("there is no item to show diff")
- }
- commit := item.(*git.Commit)
- args := []string{"show", "--stat", commit.Hash}
- return popGitCommand(l.repository, args)
-}
-
-func (l *log) showFileDiff() error {
- if l.selected == nil {
- return nil
- }
- var args []string
- pid, err := l.selected.ParentID()
- if err != nil {
- args = []string{"show", "--oneline", "--patch"}
- } else {
- args = []string{"diff", pid + ".." + l.selected.Hash}
- }
- item, err := l.prompt.Selection()
- if err != nil {
- return fmt.Errorf("there is no item to show diff")
- }
- dd := item.(*git.DiffDelta)
- args = append(args, dd.OldFile.Path)
- return popGitCommand(l.repository, args)
-}
-
func (l *log) logInfo(item prompt.Item) [][]term.Cell {
grid := make([][]term.Cell, 0)
if item == nil {
diff --git a/cli/status.go b/cli/status.go
index 7a5be9f..c51b857 100644
--- a/cli/status.go
+++ b/cli/status.go
@@ -28,7 +28,11 @@ func StatusPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, erro
items = append(items, entry)
}
if len(items) == 0 {
- printClean(r)
+ writer := term.NewBufferedWriter(os.Stdout)
+ for _, line := range workingTreeClean(r.Head) {
+ writer.WriteCells(line)
+ }
+ writer.Flush()
os.Exit(0)
}
list, err := prompt.NewList(items, opts.LineSize)
@@ -72,39 +76,111 @@ func (s *status) onSelect() error {
return nil
}
+// lots of command handling here
func (s *status) onKey(key rune) error {
var reqReload bool
switch key {
case ' ':
reqReload = true
- s.addReset()
+ item, err := s.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("can't add/reset item: %v", err)
+ }
+ entry := item.(*git.StatusEntry)
+ args := []string{"add", "--", entry.String()}
+ if entry.Indexed() {
+ args = []string{"reset", "HEAD", "--", entry.String()}
+ }
+ cmd := exec.Command("git", args...)
+ cmd.Dir = s.repository.Path()
+ if err := cmd.Run(); err != nil {
+ return err
+ }
case 'p':
reqReload = true
- s.hunkStage()
+ // defer s.prompt.writer.HideCursor()
+ item, err := s.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("can't hunk stage item: %v", err)
+ }
+ entry := item.(*git.StatusEntry)
+ file, err := generateDiffFile(s.repository, entry)
+ if err == nil {
+ editor, err := editor.NewEditor(file)
+ if err != nil {
+ return err
+ }
+ patches, err := editor.Run()
+ if err != nil {
+ return err
+ }
+ for _, patch := range patches {
+ if err := applyPatchCmd(s.repository, entry, patch); err != nil {
+ return err
+ }
+ }
+ }
case 'c':
reqReload = true
- s.doCommit()
+ // defer s.prompt.writer.HideCursor()
+ args := []string{"commit", "--edit", "--quiet"}
+ err := popGitCommand(s.repository, args)
+ if err != nil {
+ return err
+ }
+ s.repository.LoadHead()
+ args, err = lastCommitArgs(s.repository)
+ if err != nil {
+ return err
+ }
+ if err := popGitCommand(s.repository, args); err != nil {
+ return fmt.Errorf("failed to commit: %v", err)
+ }
case 'm':
reqReload = true
- s.doCommitAmend()
+ // defer s.prompt.writer.HideCursor()
+ args := []string{"commit", "--amend", "--quiet"}
+ err := popGitCommand(s.repository, args)
+ if err != nil {
+ return err
+ }
+ s.repository.LoadHead()
+ args, err = lastCommitArgs(s.repository)
+ if err != nil {
+ return err
+ }
+ if err := popGitCommand(s.repository, args); err != nil {
+ return fmt.Errorf("failed to commit: %v", err)
+ }
case 'a':
reqReload = true
- // TODO: check for errors
- addAll(s.repository)
+ cmd := exec.Command("git", "add", ".")
+ cmd.Dir = s.repository.Path()
+ cmd.Run()
case 'r':
reqReload = true
- resetAll(s.repository)
+ cmd := exec.Command("git", "reset", "--mixed")
+ cmd.Dir = s.repository.Path()
+ cmd.Run()
case '!':
reqReload = true
- s.discardChanges()
+ item, err := s.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("could not discard changes on item: %v", err)
+ }
+ entry := item.(*git.StatusEntry)
+ args := []string{"checkout", "--", entry.String()}
+ cmd := exec.Command("git", args...)
+ cmd.Dir = s.repository.Path()
+ if err := cmd.Run(); err != nil {
+ return err
+ }
case 'q':
s.prompt.Stop()
default:
}
if reqReload {
- if err := s.reloadStatus(); err != nil {
- return err
- }
+ return s.reloadStatus()
}
return nil
}
@@ -136,118 +212,7 @@ func (s *status) reloadStatus() error {
return nil
}
-// add or reset selected entry
-func (s *status) addReset() error {
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("can't add/reset item: %v", err)
- }
- entry := item.(*git.StatusEntry)
- args := []string{"add", "--", entry.String()}
- if entry.Indexed() {
- args = []string{"reset", "HEAD", "--", entry.String()}
- }
- cmd := exec.Command("git", args...)
- cmd.Dir = s.repository.Path()
- if err := cmd.Run(); err != nil {
- return err
- }
- return nil
-}
-
-// open hunk stagin ui
-func (s *status) hunkStage() error {
- // defer s.prompt.writer.HideCursor()
-
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("can't hunk stage item: %v", err)
- }
- entry := item.(*git.StatusEntry)
- file, err := generateDiffFile(s.repository, entry)
- if err == nil {
- editor, err := editor.NewEditor(file)
- if err != nil {
- return err
- }
- patches, err := editor.Run()
- if err != nil {
- return err
- }
- for _, patch := range patches {
- if err := applyPatchCmd(s.repository, entry, patch); err != nil {
- return err
- }
- }
- } else {
-
- }
- return nil
-}
-
-func (s *status) doCommit() error {
- // defer s.prompt.writer.HideCursor()
-
- args := []string{"commit", "--edit", "--quiet"}
- err := popGitCommand(s.repository, args)
- if err != nil {
- return err
- }
- s.repository.LoadHead()
- args, err = lastCommitArgs(s.repository)
- if err != nil {
- return err
- }
- if err := popGitCommand(s.repository, args); err != nil {
- return err
- }
- return nil
-}
-
-func (s *status) doCommitAmend() error {
- // defer s.prompt.writer.HideCursor()
-
- args := []string{"commit", "--amend", "--quiet"}
- err := popGitCommand(s.repository, args)
- if err != nil {
- return err
- }
- s.repository.LoadHead()
- args, err = lastCommitArgs(s.repository)
- if err != nil {
- return err
- }
- if err := popGitCommand(s.repository, args); err != nil {
- return err
- }
- return nil
-}
-
func (s *status) branchInfo(item prompt.Item) [][]term.Cell {
b := s.repository.Head
return branchInfo(b, true)
}
-
-func (s *status) discardChanges() error {
- // defer s.prompt.render()
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("cant't discard changes on item: %v", err)
- }
- entry := item.(*git.StatusEntry)
- args := []string{"checkout", "--", entry.String()}
- cmd := exec.Command("git", args...)
- cmd.Dir = s.repository.Path()
- if err := cmd.Run(); err != nil {
- return err
- }
- return nil
-}
-
-func printClean(r *git.Repository) {
- writer := term.NewBufferedWriter(os.Stdout)
- for _, line := range workingTreeClean(r.Head) {
- writer.WriteCells(line)
- }
- writer.Flush()
-}
diff --git a/prompt/renderer.go b/prompt/renderer.go
index c66eff1..89b2223 100644
--- a/prompt/renderer.go
+++ b/prompt/renderer.go
@@ -18,22 +18,22 @@ func itemText(item Item, matches []int, selected bool) []term.Cell {
if len(matches) == 0 {
return append(line, term.Cprint(item.String())...)
}
- highligted := make([]term.Cell, 0)
+ highlighted := make([]term.Cell, 0)
for _, r := range item.String() {
- highligted = append(highligted, term.Cell{
+ highlighted = append(highlighted, term.Cell{
Ch: r,
})
}
for _, m := range matches {
- if m > len(highligted)-1 {
+ if m > len(highlighted)-1 {
continue
}
- highligted[m] = term.Cell{
- Ch: highligted[m].Ch,
- Attr: append(highligted[m].Attr, color.Underline),
+ highlighted[m] = term.Cell{
+ Ch: highlighted[m].Ch,
+ Attr: append(highlighted[m].Attr, color.Underline),
}
}
- line = append(line, highligted...)
+ line = append(line, highlighted...)
return line
}
From 64dd287eeb2b06d5790b423fe8be5c745cd77229 Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Thu, 20 Jun 2019 12:28:06 +0300
Subject: [PATCH 09/13] improve interfaces for prompt
---
cli/branch.go | 16 ++------
cli/log.go | 14 +++----
cli/rendering.go | 6 +--
cli/status.go | 25 ++++---------
main.go | 20 +++++-----
prompt/list.go | 83 ++++++++++++++++++------------------------
prompt/prompt.go | 10 ++---
prompt/renderer.go | 7 ++--
term/bufferedwriter.go | 6 +--
9 files changed, 77 insertions(+), 110 deletions(-)
diff --git a/cli/branch.go b/cli/branch.go
index 7040f59..ae16ba5 100644
--- a/cli/branch.go
+++ b/cli/branch.go
@@ -17,17 +17,13 @@ type branch struct {
prompt *prompt.Prompt
}
-// BranchPrompt draws the screen with its list, initializing the cursor to the given position.
+// BranchPrompt configures a prompt to serve as a branch prompt
func BranchPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, error) {
branches, err := r.Branches()
if err != nil {
return nil, fmt.Errorf("could not load branches: %v", err)
}
- items := make([]prompt.Item, 0)
- for _, branch := range branches {
- items = append(items, branch)
- }
- list, err := prompt.NewList(items, opts.LineSize)
+ list, err := prompt.NewList(branches, opts.LineSize)
if err != nil {
return nil, fmt.Errorf("could not create list: %v", err)
}
@@ -76,7 +72,7 @@ func (b *branch) onKey(key rune) error {
return nil
}
-func (b *branch) branchInfo(item prompt.Item) [][]term.Cell {
+func (b *branch) branchInfo(item interface{}) [][]term.Cell {
branch := item.(*git.Branch)
target := branch.Target()
grid := make([][]term.Cell, 0)
@@ -112,12 +108,8 @@ func (b *branch) reloadBranches() error {
if err != nil {
return err
}
- items := make([]prompt.Item, 0)
- for _, branch := range branches {
- items = append(items, branch)
- }
state := b.prompt.State()
- list, err := prompt.NewList(items, state.ListSize)
+ list, err := prompt.NewList(branches, state.ListSize)
if err != nil {
return fmt.Errorf("could not reload branches: %v", err)
}
diff --git a/cli/log.go b/cli/log.go
index 3005269..08bd14d 100644
--- a/cli/log.go
+++ b/cli/log.go
@@ -21,7 +21,7 @@ type log struct {
oldState *prompt.State
}
-// LogPrompt draws the screen with its list, initializing the cursor to the given position.
+// LogPrompt configures a prompt to serve as a commit prompt
func LogPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, error) {
cs, err := r.Commits()
if err != nil {
@@ -29,11 +29,7 @@ func LogPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, error)
}
r.Branches() // to find refs
r.Tags()
- items := make([]prompt.Item, 0)
- for _, commit := range cs {
- items = append(items, commit)
- }
- list, err := prompt.NewList(items, opts.LineSize)
+ list, err := prompt.NewList(cs, opts.LineSize)
if err != nil {
return nil, fmt.Errorf("could not create list: %v", err)
}
@@ -70,7 +66,7 @@ func (l *log) onSelect() error {
return nil
}
deltas := diff.Deltas()
- newlist := make([]prompt.Item, 0)
+ newlist := make([]interface{}, 0)
for _, delta := range deltas {
newlist = append(newlist, delta)
}
@@ -111,7 +107,7 @@ func (l *log) onSelect() error {
}
func (l *log) onKey(key rune) error {
- var item prompt.Item
+ var item interface{}
var err error
item, err = l.prompt.Selection()
if err != nil {
@@ -149,7 +145,7 @@ func (l *log) onKey(key rune) error {
return nil
}
-func (l *log) logInfo(item prompt.Item) [][]term.Cell {
+func (l *log) logInfo(item interface{}) [][]term.Cell {
grid := make([][]term.Cell, 0)
if item == nil {
return grid
diff --git a/cli/rendering.go b/cli/rendering.go
index 84acf7e..ba7d5c4 100644
--- a/cli/rendering.go
+++ b/cli/rendering.go
@@ -1,15 +1,15 @@
package cli
import (
+ "fmt"
"strconv"
"github.com/fatih/color"
- "github.com/isacikgoz/gitin/prompt"
"github.com/isacikgoz/gitin/term"
git "github.com/isacikgoz/libgit2-api"
)
-func renderItem(item prompt.Item, matches []int, selected bool) []term.Cell {
+func renderItem(item interface{}, matches []int, selected bool) []term.Cell {
var line []term.Cell
if selected {
line = append(line, term.Cprint("> ", color.FgCyan)...)
@@ -34,7 +34,7 @@ func renderItem(item prompt.Item, matches []int, selected bool) []term.Cell {
line = append(line, stautsText(dd.DeltaStatusString()[:1])...)
line = append(line, highLightedText(matches, color.FgWhite, dd.String())...)
default:
- line = append(line, highLightedText(matches, color.FgWhite, item.String())...)
+ line = append(line, highLightedText(matches, color.FgWhite, fmt.Sprint(item))...)
}
return line
}
diff --git a/cli/status.go b/cli/status.go
index c51b857..2fc79e0 100644
--- a/cli/status.go
+++ b/cli/status.go
@@ -17,17 +17,13 @@ type status struct {
prompt *prompt.Prompt
}
-// StatusPrompt draws the screen with its list, initializing the cursor to the given position.
+// StatusPrompt configures a prompt to serve as work-dir explorer prompt
func StatusPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, error) {
st, err := r.LoadStatus()
if err != nil {
return nil, fmt.Errorf("could not load status: %v", err)
}
- items := make([]prompt.Item, 0)
- for _, entry := range st.Entities {
- items = append(items, entry)
- }
- if len(items) == 0 {
+ if len(st.Entities) == 0 {
writer := term.NewBufferedWriter(os.Stdout)
for _, line := range workingTreeClean(r.Head) {
writer.WriteCells(line)
@@ -35,7 +31,7 @@ func StatusPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, erro
writer.Flush()
os.Exit(0)
}
- list, err := prompt.NewList(items, opts.LineSize)
+ list, err := prompt.NewList(st.Entities, opts.LineSize)
if err != nil {
return nil, fmt.Errorf("could not create list: %v", err)
}
@@ -55,14 +51,14 @@ func StatusPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, erro
prompt.WithKeyHandler(s.onKey),
prompt.WithSelectionHandler(s.onSelect),
prompt.WithItemRenderer(renderItem),
- prompt.WithInformation(s.branchInfo),
+ prompt.WithInformation(s.info),
)
s.prompt.Controls = controls
return s.prompt, nil
}
-// return true to terminate
+// return err to terminate
func (s *status) onSelect() error {
item, err := s.prompt.Selection()
if err != nil {
@@ -70,7 +66,6 @@ func (s *status) onSelect() error {
}
entry := item.(*git.StatusEntry)
if err = popGitCommand(s.repository, fileStatArgs(entry)); err != nil {
- // return fmt.Errorf("could not run a git command: %v", err)
return nil // intentionally ignore errors here
}
return nil
@@ -192,18 +187,14 @@ func (s *status) reloadStatus() error {
if err != nil {
return err
}
- items := make([]prompt.Item, 0)
- for _, entry := range status.Entities {
- items = append(items, entry)
- }
- if len(items) == 0 {
+ if len(status.Entities) == 0 {
// this is the case when the working tree is cleaned at runtime
s.prompt.Stop()
s.prompt.SetExitMsg(workingTreeClean(s.repository.Head))
return nil
}
state := s.prompt.State()
- list, err := prompt.NewList(items, state.ListSize)
+ list, err := prompt.NewList(status.Entities, state.ListSize)
if err != nil {
return err
}
@@ -212,7 +203,7 @@ func (s *status) reloadStatus() error {
return nil
}
-func (s *status) branchInfo(item prompt.Item) [][]term.Cell {
+func (s *status) info(item interface{}) [][]term.Cell {
b := s.repository.Head
return branchInfo(b, true)
}
diff --git a/main.go b/main.go
index 83c7611..2aa4078 100644
--- a/main.go
+++ b/main.go
@@ -17,16 +17,12 @@ func main() {
pwd, _ := os.Getwd()
r, err := git.Open(pwd)
- if err != nil {
- fmt.Fprintf(os.Stderr, "%v\n", err)
- os.Exit(1)
- }
+ exitIfError(err)
+
var o prompt.Options
err = env.Process("gitin", &o)
- if err != nil {
- fmt.Fprintf(os.Stderr, "%v\n", err)
- os.Exit(1)
- }
+ exitIfError(err)
+
var p *prompt.Prompt
// cli package is for responsible to create and configure a prompt
@@ -40,11 +36,15 @@ func main() {
default:
return
}
- if err != nil {
+ exitIfError(err)
+ if err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
- if err := p.Run(); err != nil {
+}
+
+func exitIfError(err error) {
+ if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
diff --git a/prompt/list.go b/prompt/list.go
index f2cd04f..510c67f 100644
--- a/prompt/list.go
+++ b/prompt/list.go
@@ -1,56 +1,58 @@
// This is a modified version of promptui's list. The original version can
// be found at https://github.com/manifoldco/promptui
-
+// A little copying is better than a little dependency. - Go proverbs.
package prompt
import (
"fmt"
+ "reflect"
"strings"
"github.com/sahilm/fuzzy"
)
-// Item is to create a simple interface for list items
-type Item interface {
- String() string
-}
-
-type interfaceSource []Item
+type interfaceSource []interface{}
-func (is interfaceSource) String(i int) string { return is[i].String() }
+func (is interfaceSource) String(i int) string { return fmt.Sprint(is[i]) }
func (is interfaceSource) Len() int { return len(is) }
-// NotFound is an index returned when no item was selected. This could
-// happen due to a search without results.
+// NotFound is an index returned when no item was selected.
const NotFound = -1
// List holds a collection of items that can be displayed with an N number of
// visible items. The list can be moved up, down by one item of time or an
// entire page (ie: visible size). It keeps track of the current selected item.
type List struct {
- items []Item
- scope []Item
- matches map[Item][]int
+ items []interface{}
+ scope []interface{}
+ matches map[interface{}][]int
cursor int // cursor holds the index of the current selected item
size int // size is the number of visible options
start int
find string
}
-// NewList creates and initializes a list of searchable items. The items attribute must be a slice type with a
-// size greater than 0. Error will be returned if those two conditions are not met.
-func NewList(items []Item, size int) (*List, error) {
+// NewList creates and initializes a list of searchable items. The items attribute must be a slice type.
+func NewList(items interface{}, size int) (*List, error) {
if size < 1 {
return nil, fmt.Errorf("list size %d must be greater than 0", size)
}
+ if items == nil || reflect.TypeOf(items).Kind() != reflect.Slice {
+ return nil, fmt.Errorf("items %v is not a slice", items)
+ }
+
+ slice := reflect.ValueOf(items)
+ values := make([]interface{}, slice.Len())
- return &List{size: size, items: items, scope: items}, nil
+ for i := range values {
+ item := slice.Index(i)
+ values[i] = item.Interface()
+ }
+ return &List{size: size, items: values, scope: values}, nil
}
-// Prev moves the visible list back one item. If the selected item is out of
-// view, the new select item becomes the last visible item. If the list is
-// already at the top, nothing happens.
+// Prev moves the visible list back one item.
func (l *List) Prev() {
if l.cursor > 0 {
l.cursor--
@@ -61,8 +63,7 @@ func (l *List) Prev() {
}
}
-// Search allows the list to be filtered by a given term. The list must
-// implement the searcher function signature for this functionality to work.
+// Search allows the list to be filtered by a given term.
func (l *List) Search(term string) {
term = strings.Trim(term, " ")
l.cursor = 0
@@ -71,8 +72,7 @@ func (l *List) Search(term string) {
l.search(term)
}
-// CancelSearch stops the current search and returns the list to its
-// original order.
+// CancelSearch stops the current search and returns the list to its original order.
func (l *List) CancelSearch() {
l.cursor = 0
l.start = 0
@@ -84,9 +84,9 @@ func (l *List) search(term string) {
l.scope = l.items
return
}
- l.matches = make(map[Item][]int)
+ l.matches = make(map[interface{}][]int)
results := fuzzy.FindFrom(term, interfaceSource(l.items))
- l.scope = make([]Item, 0)
+ l.scope = make([]interface{}, 0)
for _, r := range results {
item := l.items[r.Index]
l.scope = append(l.scope, item)
@@ -99,8 +99,7 @@ func (l *List) Start() int {
return l.start
}
-// SetStart sets the current scroll position. Values out of bounds will be
-// clamped.
+// SetStart sets the current scroll position. Values out of bounds will be clamped.
func (l *List) SetStart(i int) {
if i < 0 {
i = 0
@@ -112,8 +111,8 @@ func (l *List) SetStart(i int) {
}
}
-// SetCursor sets the position of the cursor in the list. Values out of bounds
-// will be clamped.
+// SetCursor sets the position of the cursor in the list. Values out of bounds will
+// be clamped.
func (l *List) SetCursor(i int) {
max := len(l.scope) - 1
if i >= max {
@@ -131,9 +130,7 @@ func (l *List) SetCursor(i int) {
}
}
-// Next moves the visible list forward one item. If the selected item is out of
-// view, the new select item becomes the first visible item. If the list is
-// already at the bottom, nothing happens.
+// Next moves the visible list forward one item.
func (l *List) Next() {
max := len(l.scope) - 1
@@ -147,9 +144,7 @@ func (l *List) Next() {
}
// PageUp moves the visible list backward by x items. Where x is the size of the
-// visible items on the list. The selected item becomes the first visible item.
-// If the list is already at the bottom, the selected item becomes the last
-// visible item.
+// visible items on the list.
func (l *List) PageUp() {
start := l.start - l.size
if start < 0 {
@@ -166,8 +161,7 @@ func (l *List) PageUp() {
}
// PageDown moves the visible list forward by x items. Where x is the size of
-// the visible items on the list. The selected item becomes the first visible
-// item.
+// the visible items on the list.
func (l *List) PageDown() {
start := l.start + l.size
max := len(l.scope) - l.size
@@ -201,10 +195,8 @@ func (l *List) CanPageUp() bool {
return l.start > 0
}
-// Index returns the index of the item currently selected inside the searched list. If no item is selected,
-// the NotFound (-1) index is returned.
+// Index returns the index of the item currently selected inside the searched list.
func (l *List) Index() int {
- // defer recoverFromPanic()
if len(l.scope) <= 0 {
return 0
}
@@ -219,15 +211,10 @@ func (l *List) Index() int {
return NotFound
}
-func recoverFromPanic() {
- if r := recover(); r != nil {
- }
-}
-
// Items returns a slice equal to the size of the list with the current visible
// items and the index of the active item in this list.
-func (l *List) Items() ([]Item, int) {
- var result []Item
+func (l *List) Items() ([]interface{}, int) {
+ var result []interface{}
max := len(l.scope)
end := l.start + l.size
diff --git a/prompt/prompt.go b/prompt/prompt.go
index 995a9a4..c419afd 100644
--- a/prompt/prompt.go
+++ b/prompt/prompt.go
@@ -20,8 +20,8 @@ type keyEvent struct {
type keyHandlerFunc func(rune) error
type selectionHandlerFunc func() error
-type itemRendererFunc func(Item, []int, bool) []term.Cell
-type informationRendererFunc func(Item) [][]term.Cell
+type itemRendererFunc func(interface{}, []int, bool) []term.Cell
+type informationRendererFunc func(interface{}) [][]term.Cell
//OptionalFunc handles functional arguments of the prompt
type OptionalFunc func(*Prompt)
@@ -335,12 +335,12 @@ func (p *Prompt) onSelect() error {
if idx == NotFound {
return fmt.Errorf("could not select an item")
}
- p.writer.WriteCells(term.Cprint(items[idx].String()))
+ p.writer.WriteCells(term.Cprint(fmt.Sprint(items[idx])))
return nil
}
// genInfo is the default function to genereate info
-func (p *Prompt) genInfo(item Item) [][]term.Cell {
+func (p *Prompt) genInfo(item interface{}) [][]term.Cell {
return nil
}
@@ -374,7 +374,7 @@ func (p *Prompt) ListSize() int {
}
// Selection returns the selected item
-func (p *Prompt) Selection() (Item, error) {
+func (p *Prompt) Selection() (interface{}, error) {
items, idx := p.list.Items()
if idx == NotFound {
return nil, fmt.Errorf("there is no item to be selected")
diff --git a/prompt/renderer.go b/prompt/renderer.go
index 89b2223..bd255ff 100644
--- a/prompt/renderer.go
+++ b/prompt/renderer.go
@@ -8,18 +8,19 @@ import (
"github.com/isacikgoz/gitin/term"
)
-func itemText(item Item, matches []int, selected bool) []term.Cell {
+func itemText(item interface{}, matches []int, selected bool) []term.Cell {
var line []term.Cell
+ text := fmt.Sprint(item)
if selected {
line = append(line, term.Cprint("> ", color.FgCyan)...)
} else {
line = append(line, term.Cprint(" ", color.FgWhite)...)
}
if len(matches) == 0 {
- return append(line, term.Cprint(item.String())...)
+ return append(line, term.Cprint(text)...)
}
highlighted := make([]term.Cell, 0)
- for _, r := range item.String() {
+ for _, r := range text {
highlighted = append(highlighted, term.Cell{
Ch: r,
})
diff --git a/term/bufferedwriter.go b/term/bufferedwriter.go
index 390907a..c87d8be 100644
--- a/term/bufferedwriter.go
+++ b/term/bufferedwriter.go
@@ -1,6 +1,6 @@
-// This is a modified version of promptui's screenbuffer. The original version can
-// be found at https://github.com/manifoldco/promptui
-
+// Package term is influenced by https://github.com/AlecAivazis/survey and
+// https://github.com/manifoldco/promptui it might contain some code snippets from those
+// A little copying is better than a little dependency. - Go proverbs.
package term
import (
From 0ab98bb6dd87a02a686da5e13742ee803785680b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C4=B0brahim=20Serdar=20A=C3=A7=C4=B1kg=C3=B6z?=
Date: Thu, 20 Jun 2019 17:27:20 +0300
Subject: [PATCH 10/13] better handle for keybindings
---
cli/commands.go | 67 +---------
cli/log.go | 110 ++++++++++------
cli/status.go | 331 ++++++++++++++++++++++++++++++++----------------
3 files changed, 298 insertions(+), 210 deletions(-)
diff --git a/cli/commands.go b/cli/commands.go
index 254ac60..054db1a 100644
--- a/cli/commands.go
+++ b/cli/commands.go
@@ -1,13 +1,10 @@
package cli
import (
- "fmt"
- "io"
"os"
"os/exec"
git "github.com/isacikgoz/libgit2-api"
- "github.com/waigani/diffparser"
)
func popGitCommand(r *git.Repository, args []string) error {
@@ -27,63 +24,9 @@ func popGitCommand(r *git.Repository, args []string) error {
return nil
}
-// fileStatArgs returns git command args for getting diff
-func fileStatArgs(e *git.StatusEntry) []string {
- var args []string
- if e.Indexed() {
- args = []string{"diff", "--cached", e.String()}
- } else if e.EntryType == git.StatusEntryTypeUntracked {
- args = []string{"diff", "--no-index", "/dev/null", e.String()}
- } else {
- args = []string{"diff", "--", e.String()}
- }
- return args
-}
-
-// lastCommitArgs returns the args for show stat
-func lastCommitArgs(r *git.Repository) ([]string, error) {
- r.LoadStatus()
- head := r.Head
- if head == nil {
- return nil, fmt.Errorf("can't get HEAD")
- }
- hash := string(head.Target().Hash)
- args := []string{"show", "--stat", hash}
- return args, nil
-}
-
-func generateDiffFile(r *git.Repository, entry *git.StatusEntry) (*diffparser.DiffFile, error) {
- args := fileStatArgs(entry)
- cmd := exec.Command("git", args...)
- cmd.Dir = r.Path()
- out, err := cmd.CombinedOutput()
- if err != nil {
- return nil, err
- }
- diff, err := diffparser.Parse(string(out))
- if err != nil {
- return nil, err
- }
- return diff.Files[0], nil
-}
-
-func applyPatchCmd(r *git.Repository, entry *git.StatusEntry, patch string) error {
- mode := []string{"apply", "--cached"}
- if entry.Indexed() {
- mode = []string{"apply", "--cached", "--reverse"}
- }
- cmd := exec.Command("git", mode...)
- cmd.Dir = r.Path()
- stdin, err := cmd.StdinPipe()
- if err != nil {
- return err
- }
- go func() {
- defer stdin.Close()
- io.WriteString(stdin, patch+"\n")
- }()
- if err := cmd.Run(); err != nil {
- return err
- }
- return nil
+type keybinding struct {
+ key rune
+ display string
+ handler func() error
+ desc string
}
diff --git a/cli/log.go b/cli/log.go
index 08bd14d..943fd14 100644
--- a/cli/log.go
+++ b/cli/log.go
@@ -15,10 +15,11 @@ import (
// log holds the repository struct and the prompt pointer. since log and prompt dependent,
// I found the best wau to associate them with this way
type log struct {
- repository *git.Repository
- prompt *prompt.Prompt
- selected *git.Commit
- oldState *prompt.State
+ repository *git.Repository
+ prompt *prompt.Prompt
+ selected *git.Commit
+ oldState *prompt.State
+ keybindings []*keybinding
}
// LogPrompt configures a prompt to serve as a commit prompt
@@ -33,10 +34,6 @@ func LogPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, error)
if err != nil {
return nil, fmt.Errorf("could not create list: %v", err)
}
- controls := make(map[string]string)
- controls["show diff"] = "d"
- controls["show stat"] = "s"
- controls["select"] = "enter"
l := &log{repository: r}
l.prompt = prompt.Create("Commits", opts, list,
@@ -45,7 +42,7 @@ func LogPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, error)
prompt.WithItemRenderer(renderItem),
prompt.WithInformation(l.logInfo),
)
- l.prompt.Controls = controls
+ l.prompt.Controls = l.defineKeybindings()
return l.prompt, nil
}
@@ -81,7 +78,6 @@ func (l *log) onSelect() error {
SearchStr: "",
SearchLabel: "Files",
})
- // l.prompt.opts.SearchLabel = "Files"
case *git.DiffDelta:
if l.selected == nil {
return nil
@@ -107,40 +103,50 @@ func (l *log) onSelect() error {
}
func (l *log) onKey(key rune) error {
- var item interface{}
- var err error
- item, err = l.prompt.Selection()
+ for _, kb := range l.keybindings {
+ if kb.key == key {
+ return kb.handler()
+ }
+ }
+ return nil
+}
+
+func (l *log) commitStat() error {
+ item, err := l.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("there is no item to show diff")
+ }
+ commit, ok := item.(*git.Commit)
+ if !ok {
+ return nil
+ }
+ args := []string{"show", "--stat", commit.Hash}
+ return popGitCommand(l.repository, args)
+}
+
+func (l *log) commitDiff() error {
+ item, err := l.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("there is no item to show diff")
+ }
+ commit, ok := item.(*git.Commit)
+ if !ok {
+ return nil
+ }
+ args := []string{"show", commit.Hash}
+ return popGitCommand(l.repository, args)
+}
+
+func (l *log) quit() error {
+ item, err := l.prompt.Selection()
if err != nil {
return err
}
switch item.(type) {
case *git.Commit:
- switch key {
- case 's':
- item, err := l.prompt.Selection()
- if err != nil {
- return fmt.Errorf("there is no item to show diff")
- }
- commit := item.(*git.Commit)
- args := []string{"show", "--stat", commit.Hash}
- return popGitCommand(l.repository, args)
- case 'd':
- item, err := l.prompt.Selection()
- if err != nil {
- return fmt.Errorf("there is no item to show diff")
- }
- commit := item.(*git.Commit)
- args := []string{"show", commit.Hash}
- return popGitCommand(l.repository, args)
-
- case 'q':
- l.prompt.Stop()
- }
+ l.prompt.Stop()
case *git.DiffDelta:
- switch key {
- case 'q':
- l.prompt.SetState(l.oldState)
- }
+ l.prompt.SetState(l.oldState)
}
return nil
}
@@ -194,6 +200,34 @@ func (l *log) logInfo(item interface{}) [][]term.Cell {
return grid
}
+func (l *log) defineKeybindings() map[string]string {
+ l.keybindings = []*keybinding{
+ &keybinding{
+ key: 's',
+ display: "s",
+ desc: "show stat",
+ handler: l.commitStat,
+ },
+ &keybinding{
+ key: 'd',
+ display: "d",
+ desc: "show diff",
+ handler: l.commitDiff,
+ },
+ &keybinding{
+ key: 'q',
+ display: "q",
+ desc: "quit",
+ handler: l.quit,
+ },
+ }
+ controls := make(map[string]string)
+ for _, kb := range l.keybindings {
+ controls[kb.desc] = kb.display
+ }
+ return controls
+}
+
func commitRefs(r *git.Repository, c *git.Commit) []term.Cell {
var cells []term.Cell
if refs, ok := r.RefMap[c.Hash]; ok {
diff --git a/cli/status.go b/cli/status.go
index 2fc79e0..fdf127c 100644
--- a/cli/status.go
+++ b/cli/status.go
@@ -2,6 +2,7 @@ package cli
import (
"fmt"
+ "io"
"os"
"os/exec"
@@ -9,12 +10,14 @@ import (
"github.com/isacikgoz/gitin/prompt"
"github.com/isacikgoz/gitin/term"
git "github.com/isacikgoz/libgit2-api"
+ "github.com/waigani/diffparser"
)
// status holds the repository struct and the prompt pointer.
type status struct {
- repository *git.Repository
- prompt *prompt.Prompt
+ repository *git.Repository
+ prompt *prompt.Prompt
+ keybindings []*keybinding
}
// StatusPrompt configures a prompt to serve as work-dir explorer prompt
@@ -35,15 +38,6 @@ func StatusPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, erro
if err != nil {
return nil, fmt.Errorf("could not create list: %v", err)
}
- controls := make(map[string]string)
- controls["add/reset entry"] = "space"
- controls["show diff"] = "enter"
- controls["add all"] = "a"
- controls["reset all"] = "r"
- controls["hunk stage"] = "p"
- controls["commit"] = "c"
- controls["amend"] = "m"
- controls["discard changes"] = "!"
s := &status{repository: r}
@@ -53,7 +47,7 @@ func StatusPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, erro
prompt.WithItemRenderer(renderItem),
prompt.WithInformation(s.info),
)
- s.prompt.Controls = controls
+ s.prompt.Controls = s.defineKeybindings()
return s.prompt, nil
}
@@ -71,115 +65,176 @@ func (s *status) onSelect() error {
return nil
}
-// lots of command handling here
+// too much of keybindings
func (s *status) onKey(key rune) error {
- var reqReload bool
- switch key {
- case ' ':
- reqReload = true
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("can't add/reset item: %v", err)
- }
- entry := item.(*git.StatusEntry)
- args := []string{"add", "--", entry.String()}
- if entry.Indexed() {
- args = []string{"reset", "HEAD", "--", entry.String()}
- }
- cmd := exec.Command("git", args...)
- cmd.Dir = s.repository.Path()
- if err := cmd.Run(); err != nil {
- return err
- }
- case 'p':
- reqReload = true
- // defer s.prompt.writer.HideCursor()
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("can't hunk stage item: %v", err)
- }
- entry := item.(*git.StatusEntry)
- file, err := generateDiffFile(s.repository, entry)
- if err == nil {
- editor, err := editor.NewEditor(file)
- if err != nil {
- return err
- }
- patches, err := editor.Run()
- if err != nil {
- return err
- }
- for _, patch := range patches {
- if err := applyPatchCmd(s.repository, entry, patch); err != nil {
- return err
- }
- }
+ for _, kb := range s.keybindings {
+ if kb.key == key {
+ return kb.handler()
}
- case 'c':
- reqReload = true
- // defer s.prompt.writer.HideCursor()
- args := []string{"commit", "--edit", "--quiet"}
- err := popGitCommand(s.repository, args)
- if err != nil {
- return err
- }
- s.repository.LoadHead()
- args, err = lastCommitArgs(s.repository)
- if err != nil {
- return err
- }
- if err := popGitCommand(s.repository, args); err != nil {
- return fmt.Errorf("failed to commit: %v", err)
- }
- case 'm':
- reqReload = true
- // defer s.prompt.writer.HideCursor()
- args := []string{"commit", "--amend", "--quiet"}
- err := popGitCommand(s.repository, args)
+ }
+ return nil
+}
+
+func (s *status) info(item interface{}) [][]term.Cell {
+ b := s.repository.Head
+ return branchInfo(b, true)
+}
+
+func (s *status) defineKeybindings() map[string]string {
+ s.keybindings = []*keybinding{
+ &keybinding{
+ key: ' ',
+ display: "space",
+ desc: "add/reset entry",
+ handler: s.addResetEntry,
+ },
+ &keybinding{
+ key: 'p',
+ display: "p",
+ desc: "hunk stage entry",
+ handler: s.hunkStageEntry,
+ },
+ &keybinding{
+ key: 'c',
+ display: "c",
+ desc: "commit",
+ handler: s.commit,
+ },
+ &keybinding{
+ key: 'm',
+ display: "m",
+ desc: "amend",
+ handler: s.amend,
+ },
+ &keybinding{
+ key: 'a',
+ display: "a",
+ desc: "add all",
+ handler: s.addAllEntries,
+ },
+ &keybinding{
+ key: 'r',
+ display: "r",
+ desc: "reset all",
+ handler: s.resetAllEntries,
+ },
+ &keybinding{
+ key: '!',
+ display: "!",
+ desc: "discard changes",
+ handler: s.checkoutEntry,
+ },
+ &keybinding{
+ key: 'q',
+ display: "q",
+ desc: "quit",
+ handler: s.quit,
+ },
+ }
+ controls := make(map[string]string)
+ for _, kb := range s.keybindings {
+ controls[kb.desc] = kb.display
+ }
+ return controls
+}
+
+func (s *status) addResetEntry() error {
+ item, err := s.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("can't add/reset item: %v", err)
+ }
+ entry := item.(*git.StatusEntry)
+ args := []string{"add", "--", entry.String()}
+ if entry.Indexed() {
+ args = []string{"reset", "HEAD", "--", entry.String()}
+ }
+ return s.runCommandWithArgs(args)
+}
+
+func (s *status) hunkStageEntry() error {
+ item, err := s.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("can't hunk stage item: %v", err)
+ }
+ entry := item.(*git.StatusEntry)
+ file, err := generateDiffFile(s.repository, entry)
+ if err == nil {
+ editor, err := editor.NewEditor(file)
if err != nil {
return err
}
- s.repository.LoadHead()
- args, err = lastCommitArgs(s.repository)
+ patches, err := editor.Run()
if err != nil {
return err
}
- if err := popGitCommand(s.repository, args); err != nil {
- return fmt.Errorf("failed to commit: %v", err)
- }
- case 'a':
- reqReload = true
- cmd := exec.Command("git", "add", ".")
- cmd.Dir = s.repository.Path()
- cmd.Run()
- case 'r':
- reqReload = true
- cmd := exec.Command("git", "reset", "--mixed")
- cmd.Dir = s.repository.Path()
- cmd.Run()
- case '!':
- reqReload = true
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("could not discard changes on item: %v", err)
- }
- entry := item.(*git.StatusEntry)
- args := []string{"checkout", "--", entry.String()}
- cmd := exec.Command("git", args...)
- cmd.Dir = s.repository.Path()
- if err := cmd.Run(); err != nil {
- return err
+ for _, patch := range patches {
+ if err := applyPatchCmd(s.repository, entry, patch); err != nil {
+ return err
+ }
}
- case 'q':
- s.prompt.Stop()
- default:
}
- if reqReload {
- return s.reloadStatus()
+ return s.reloadStatus()
+}
+
+func (s *status) commit() error {
+ return s.bareCommit("--edit")
+}
+
+func (s *status) amend() error {
+ return s.bareCommit("--amend")
+}
+
+func (s *status) bareCommit(arg string) error {
+ args := []string{"commit", arg, "--quiet"}
+ err := popGitCommand(s.repository, args)
+ if err != nil {
+ return err
+ }
+ s.repository.LoadHead()
+ args, err = lastCommitArgs(s.repository)
+ if err != nil {
+ return err
+ }
+ if err := popGitCommand(s.repository, args); err != nil {
+ return fmt.Errorf("failed to commit: %v", err)
}
+ return s.reloadStatus()
+}
+
+func (s *status) addAllEntries() error {
+ args := []string{"add", "."}
+ return s.runCommandWithArgs(args)
+}
+
+func (s *status) resetAllEntries() error {
+ args := []string{"reset", "--mixed"}
+ return s.runCommandWithArgs(args)
+}
+
+func (s *status) checkoutEntry() error {
+ item, err := s.prompt.Selection()
+ if err != nil {
+ return fmt.Errorf("could not discard changes on item: %v", err)
+ }
+ entry := item.(*git.StatusEntry)
+ args := []string{"checkout", "--", entry.String()}
+ return s.runCommandWithArgs(args)
+}
+
+func (s *status) quit() error {
+ s.prompt.Stop()
return nil
}
+func (s *status) runCommandWithArgs(args []string) error {
+ cmd := exec.Command("git", args...)
+ cmd.Dir = s.repository.Path()
+ if err := cmd.Run(); err != nil {
+ return nil //ignore command errors for now
+ }
+ return s.reloadStatus()
+}
+
// reloads the list
func (s *status) reloadStatus() error {
s.repository.LoadHead()
@@ -203,7 +258,63 @@ func (s *status) reloadStatus() error {
return nil
}
-func (s *status) info(item interface{}) [][]term.Cell {
- b := s.repository.Head
- return branchInfo(b, true)
+// fileStatArgs returns git command args for getting diff
+func fileStatArgs(e *git.StatusEntry) []string {
+ var args []string
+ if e.Indexed() {
+ args = []string{"diff", "--cached", e.String()}
+ } else if e.EntryType == git.StatusEntryTypeUntracked {
+ args = []string{"diff", "--no-index", "/dev/null", e.String()}
+ } else {
+ args = []string{"diff", "--", e.String()}
+ }
+ return args
+}
+
+// lastCommitArgs returns the args for show stat
+func lastCommitArgs(r *git.Repository) ([]string, error) {
+ r.LoadStatus()
+ head := r.Head
+ if head == nil {
+ return nil, fmt.Errorf("can't get HEAD")
+ }
+ hash := string(head.Target().Hash)
+ args := []string{"show", "--stat", hash}
+ return args, nil
+}
+
+func generateDiffFile(r *git.Repository, entry *git.StatusEntry) (*diffparser.DiffFile, error) {
+ args := fileStatArgs(entry)
+ cmd := exec.Command("git", args...)
+ cmd.Dir = r.Path()
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return nil, err
+ }
+ diff, err := diffparser.Parse(string(out))
+ if err != nil {
+ return nil, err
+ }
+ return diff.Files[0], nil
+}
+
+func applyPatchCmd(r *git.Repository, entry *git.StatusEntry, patch string) error {
+ mode := []string{"apply", "--cached"}
+ if entry.Indexed() {
+ mode = []string{"apply", "--cached", "--reverse"}
+ }
+ cmd := exec.Command("git", mode...)
+ cmd.Dir = r.Path()
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ return err
+ }
+ go func() {
+ defer stdin.Close()
+ io.WriteString(stdin, patch+"\n")
+ }()
+ if err := cmd.Run(); err != nil {
+ return err
+ }
+ return nil
}
From 895f99366576199f607f87372a53c3fecd7a01b8 Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Thu, 20 Jun 2019 21:04:02 +0300
Subject: [PATCH 11/13] improve selection and key handling
---
cli/branch.go | 65 ++++++++++++--------
cli/commands.go | 7 ---
cli/log.go | 95 ++++++++++-------------------
cli/status.go | 149 +++++++++++++++++++--------------------------
prompt/prompt.go | 102 ++++++++++++++++---------------
prompt/renderer.go | 12 ++--
6 files changed, 197 insertions(+), 233 deletions(-)
diff --git a/cli/branch.go b/cli/branch.go
index ae16ba5..cfbb0ce 100644
--- a/cli/branch.go
+++ b/cli/branch.go
@@ -27,28 +27,19 @@ func BranchPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, erro
if err != nil {
return nil, fmt.Errorf("could not create list: %v", err)
}
- controls := make(map[string]string)
- controls["delete branch"] = "d"
- controls["force delete"] = "D"
- controls["checkout"] = "enter"
b := &branch{repository: r}
b.prompt = prompt.Create("Branches", opts, list,
- prompt.WithKeyHandler(b.onKey),
prompt.WithSelectionHandler(b.onSelect),
prompt.WithItemRenderer(renderItem),
prompt.WithInformation(b.branchInfo),
)
- b.prompt.Controls = controls
+ b.defineKeyBindings()
return b.prompt, nil
}
-func (b *branch) onSelect() error {
- item, err := b.prompt.Selection()
- if err != nil {
- return nil
- }
+func (b *branch) onSelect(item interface{}) error {
branch := item.(*git.Branch)
args := []string{"checkout", branch.Name}
cmd := exec.Command("git", args...)
@@ -60,14 +51,31 @@ func (b *branch) onSelect() error {
return nil
}
-func (b *branch) onKey(key rune) error {
- switch key {
- case 'd':
- return b.deleteBranch("d")
- case 'D':
- return b.deleteBranch("D")
- case 'q':
- b.prompt.Stop()
+func (b *branch) defineKeyBindings() error {
+ keybindings := []*prompt.KeyBinding{
+ &prompt.KeyBinding{
+ Key: 'd',
+ Display: "d",
+ Desc: "delete branch",
+ Handler: b.deleteBranch,
+ },
+ &prompt.KeyBinding{
+ Key: 'D',
+ Display: "D",
+ Desc: "force delete branch",
+ Handler: b.forceDeleteBranch,
+ },
+ &prompt.KeyBinding{
+ Key: 'q',
+ Display: "q",
+ Desc: "quit",
+ Handler: b.quit,
+ },
+ }
+ for _, kb := range keybindings {
+ if err := b.prompt.AddKeyBinding(kb); err != nil {
+ return err
+ }
}
return nil
}
@@ -88,11 +96,15 @@ func (b *branch) branchInfo(item interface{}) [][]term.Cell {
return grid
}
-func (b *branch) deleteBranch(mode string) error {
- item, err := b.prompt.Selection()
- if err != nil {
- return fmt.Errorf("could not delete branch: %v", err)
- }
+func (b *branch) deleteBranch(item interface{}) error {
+ return b.bareDelete(item, "d")
+}
+
+func (b *branch) forceDeleteBranch(item interface{}) error {
+ return b.bareDelete(item, "D")
+}
+
+func (b *branch) bareDelete(item interface{}, mode string) error {
branch := item.(*git.Branch)
cmd := exec.Command("git", "branch", "-"+mode, branch.Name)
cmd.Dir = b.repository.Path()
@@ -102,6 +114,11 @@ func (b *branch) deleteBranch(mode string) error {
return b.reloadBranches()
}
+func (b *branch) quit(item interface{}) error {
+ b.prompt.Stop()
+ return nil
+}
+
// reloads the list
func (b *branch) reloadBranches() error {
branches, err := b.repository.Branches()
diff --git a/cli/commands.go b/cli/commands.go
index 054db1a..3e3f14b 100644
--- a/cli/commands.go
+++ b/cli/commands.go
@@ -23,10 +23,3 @@ func popGitCommand(r *git.Repository, args []string) error {
}
return nil
}
-
-type keybinding struct {
- key rune
- display string
- handler func() error
- desc string
-}
diff --git a/cli/log.go b/cli/log.go
index 943fd14..49ae961 100644
--- a/cli/log.go
+++ b/cli/log.go
@@ -15,11 +15,10 @@ import (
// log holds the repository struct and the prompt pointer. since log and prompt dependent,
// I found the best wau to associate them with this way
type log struct {
- repository *git.Repository
- prompt *prompt.Prompt
- selected *git.Commit
- oldState *prompt.State
- keybindings []*keybinding
+ repository *git.Repository
+ prompt *prompt.Prompt
+ selected *git.Commit
+ oldState *prompt.State
}
// LogPrompt configures a prompt to serve as a commit prompt
@@ -37,23 +36,19 @@ func LogPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, error)
l := &log{repository: r}
l.prompt = prompt.Create("Commits", opts, list,
- prompt.WithKeyHandler(l.onKey),
prompt.WithSelectionHandler(l.onSelect),
prompt.WithItemRenderer(renderItem),
prompt.WithInformation(l.logInfo),
)
- l.prompt.Controls = l.defineKeybindings()
+ if err := l.defineKeybindings(); err != nil {
+ return nil, err
+ }
return l.prompt, nil
}
// return true to terminate
-func (l *log) onSelect() error {
-
- item, err := l.prompt.Selection()
- if err != nil {
- return nil
- }
+func (l *log) onSelect(item interface{}) error {
switch item.(type) {
case *git.Commit:
commit := item.(*git.Commit)
@@ -89,10 +84,6 @@ func (l *log) onSelect() error {
} else {
args = []string{"diff", pid + ".." + l.selected.Hash}
}
- item, err := l.prompt.Selection()
- if err != nil {
- return fmt.Errorf("there is no item to show diff")
- }
dd := item.(*git.DiffDelta)
args = append(args, dd.OldFile.Path)
if err := popGitCommand(l.repository, args); err != nil {
@@ -102,20 +93,7 @@ func (l *log) onSelect() error {
return nil
}
-func (l *log) onKey(key rune) error {
- for _, kb := range l.keybindings {
- if kb.key == key {
- return kb.handler()
- }
- }
- return nil
-}
-
-func (l *log) commitStat() error {
- item, err := l.prompt.Selection()
- if err != nil {
- return fmt.Errorf("there is no item to show diff")
- }
+func (l *log) commitStat(item interface{}) error {
commit, ok := item.(*git.Commit)
if !ok {
return nil
@@ -124,11 +102,7 @@ func (l *log) commitStat() error {
return popGitCommand(l.repository, args)
}
-func (l *log) commitDiff() error {
- item, err := l.prompt.Selection()
- if err != nil {
- return fmt.Errorf("there is no item to show diff")
- }
+func (l *log) commitDiff(item interface{}) error {
commit, ok := item.(*git.Commit)
if !ok {
return nil
@@ -137,11 +111,7 @@ func (l *log) commitDiff() error {
return popGitCommand(l.repository, args)
}
-func (l *log) quit() error {
- item, err := l.prompt.Selection()
- if err != nil {
- return err
- }
+func (l *log) quit(item interface{}) error {
switch item.(type) {
case *git.Commit:
l.prompt.Stop()
@@ -200,32 +170,33 @@ func (l *log) logInfo(item interface{}) [][]term.Cell {
return grid
}
-func (l *log) defineKeybindings() map[string]string {
- l.keybindings = []*keybinding{
- &keybinding{
- key: 's',
- display: "s",
- desc: "show stat",
- handler: l.commitStat,
+func (l *log) defineKeybindings() error {
+ keybindings := []*prompt.KeyBinding{
+ &prompt.KeyBinding{
+ Key: 's',
+ Display: "s",
+ Desc: "show stat",
+ Handler: l.commitStat,
},
- &keybinding{
- key: 'd',
- display: "d",
- desc: "show diff",
- handler: l.commitDiff,
+ &prompt.KeyBinding{
+ Key: 'd',
+ Display: "d",
+ Desc: "show diff",
+ Handler: l.commitDiff,
},
- &keybinding{
- key: 'q',
- display: "q",
- desc: "quit",
- handler: l.quit,
+ &prompt.KeyBinding{
+ Key: 'q',
+ Display: "q",
+ Desc: "quit",
+ Handler: l.quit,
},
}
- controls := make(map[string]string)
- for _, kb := range l.keybindings {
- controls[kb.desc] = kb.display
+ for _, kb := range keybindings {
+ if err := l.prompt.AddKeyBinding(kb); err != nil {
+ return err
+ }
}
- return controls
+ return nil
}
func commitRefs(r *git.Repository, c *git.Commit) []term.Cell {
diff --git a/cli/status.go b/cli/status.go
index fdf127c..e51a34b 100644
--- a/cli/status.go
+++ b/cli/status.go
@@ -15,9 +15,8 @@ import (
// status holds the repository struct and the prompt pointer.
type status struct {
- repository *git.Repository
- prompt *prompt.Prompt
- keybindings []*keybinding
+ repository *git.Repository
+ prompt *prompt.Prompt
}
// StatusPrompt configures a prompt to serve as work-dir explorer prompt
@@ -42,107 +41,91 @@ func StatusPrompt(r *git.Repository, opts *prompt.Options) (*prompt.Prompt, erro
s := &status{repository: r}
s.prompt = prompt.Create("Files", opts, list,
- prompt.WithKeyHandler(s.onKey),
prompt.WithSelectionHandler(s.onSelect),
prompt.WithItemRenderer(renderItem),
prompt.WithInformation(s.info),
)
- s.prompt.Controls = s.defineKeybindings()
+ if err := s.defineKeybindings(); err != nil {
+ return nil, err
+ }
return s.prompt, nil
}
// return err to terminate
-func (s *status) onSelect() error {
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("can't show diff: %v", err)
- }
+func (s *status) onSelect(item interface{}) error {
entry := item.(*git.StatusEntry)
- if err = popGitCommand(s.repository, fileStatArgs(entry)); err != nil {
+ if err := popGitCommand(s.repository, fileStatArgs(entry)); err != nil {
return nil // intentionally ignore errors here
}
return nil
}
-// too much of keybindings
-func (s *status) onKey(key rune) error {
- for _, kb := range s.keybindings {
- if kb.key == key {
- return kb.handler()
- }
- }
- return nil
-}
-
func (s *status) info(item interface{}) [][]term.Cell {
b := s.repository.Head
return branchInfo(b, true)
}
-func (s *status) defineKeybindings() map[string]string {
- s.keybindings = []*keybinding{
- &keybinding{
- key: ' ',
- display: "space",
- desc: "add/reset entry",
- handler: s.addResetEntry,
+func (s *status) defineKeybindings() error {
+ keybindings := []*prompt.KeyBinding{
+ &prompt.KeyBinding{
+ Key: ' ',
+ Display: "space",
+ Desc: "add/reset entry",
+ Handler: s.addResetEntry,
},
- &keybinding{
- key: 'p',
- display: "p",
- desc: "hunk stage entry",
- handler: s.hunkStageEntry,
+ &prompt.KeyBinding{
+ Key: 'p',
+ Display: "p",
+ Desc: "hunk stage entry",
+ Handler: s.hunkStageEntry,
},
- &keybinding{
- key: 'c',
- display: "c",
- desc: "commit",
- handler: s.commit,
+ &prompt.KeyBinding{
+ Key: 'c',
+ Display: "c",
+ Desc: "commit",
+ Handler: s.commit,
},
- &keybinding{
- key: 'm',
- display: "m",
- desc: "amend",
- handler: s.amend,
+ &prompt.KeyBinding{
+ Key: 'm',
+ Display: "m",
+ Desc: "amend",
+ Handler: s.amend,
},
- &keybinding{
- key: 'a',
- display: "a",
- desc: "add all",
- handler: s.addAllEntries,
+ &prompt.KeyBinding{
+ Key: 'a',
+ Display: "a",
+ Desc: "add all",
+ Handler: s.addAllEntries,
},
- &keybinding{
- key: 'r',
- display: "r",
- desc: "reset all",
- handler: s.resetAllEntries,
+ &prompt.KeyBinding{
+ Key: 'r',
+ Display: "r",
+ Desc: "reset all",
+ Handler: s.resetAllEntries,
},
- &keybinding{
- key: '!',
- display: "!",
- desc: "discard changes",
- handler: s.checkoutEntry,
+ &prompt.KeyBinding{
+ Key: '!',
+ Display: "!",
+ Desc: "discard changes",
+ Handler: s.checkoutEntry,
},
- &keybinding{
- key: 'q',
- display: "q",
- desc: "quit",
- handler: s.quit,
+ &prompt.KeyBinding{
+ Key: 'q',
+ Display: "q",
+ Desc: "quit",
+ Handler: s.quit,
},
}
- controls := make(map[string]string)
- for _, kb := range s.keybindings {
- controls[kb.desc] = kb.display
+ for _, kb := range keybindings {
+ if err := s.prompt.AddKeyBinding(kb); err != nil {
+ return err
+ }
}
- return controls
+ return nil
}
-func (s *status) addResetEntry() error {
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("can't add/reset item: %v", err)
- }
+func (s *status) addResetEntry(item interface{}) error {
entry := item.(*git.StatusEntry)
args := []string{"add", "--", entry.String()}
if entry.Indexed() {
@@ -151,11 +134,7 @@ func (s *status) addResetEntry() error {
return s.runCommandWithArgs(args)
}
-func (s *status) hunkStageEntry() error {
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("can't hunk stage item: %v", err)
- }
+func (s *status) hunkStageEntry(item interface{}) error {
entry := item.(*git.StatusEntry)
file, err := generateDiffFile(s.repository, entry)
if err == nil {
@@ -176,11 +155,11 @@ func (s *status) hunkStageEntry() error {
return s.reloadStatus()
}
-func (s *status) commit() error {
+func (s *status) commit(item interface{}) error {
return s.bareCommit("--edit")
}
-func (s *status) amend() error {
+func (s *status) amend(item interface{}) error {
return s.bareCommit("--amend")
}
@@ -201,27 +180,23 @@ func (s *status) bareCommit(arg string) error {
return s.reloadStatus()
}
-func (s *status) addAllEntries() error {
+func (s *status) addAllEntries(item interface{}) error {
args := []string{"add", "."}
return s.runCommandWithArgs(args)
}
-func (s *status) resetAllEntries() error {
+func (s *status) resetAllEntries(item interface{}) error {
args := []string{"reset", "--mixed"}
return s.runCommandWithArgs(args)
}
-func (s *status) checkoutEntry() error {
- item, err := s.prompt.Selection()
- if err != nil {
- return fmt.Errorf("could not discard changes on item: %v", err)
- }
+func (s *status) checkoutEntry(item interface{}) error {
entry := item.(*git.StatusEntry)
args := []string{"checkout", "--", entry.String()}
return s.runCommandWithArgs(args)
}
-func (s *status) quit() error {
+func (s *status) quit(item interface{}) error {
s.prompt.Stop()
return nil
}
diff --git a/prompt/prompt.go b/prompt/prompt.go
index c419afd..99681ff 100644
--- a/prompt/prompt.go
+++ b/prompt/prompt.go
@@ -18,8 +18,16 @@ type keyEvent struct {
err error
}
+// KeyBinding is used for mapping a key to a function
+type KeyBinding struct {
+ Key rune
+ Display string
+ Handler func(interface{}) error
+ Desc string
+}
+
type keyHandlerFunc func(rune) error
-type selectionHandlerFunc func() error
+type selectionHandlerFunc func(interface{}) error
type itemRendererFunc func(interface{}, []int, bool) []term.Cell
type informationRendererFunc func(interface{}) [][]term.Cell
@@ -31,6 +39,7 @@ type Options struct {
LineSize int `default:"5"`
StartInSearch bool
DisableColor bool
+ VimKeys bool `default:"true"`
}
// State holds the changeable vars of the prompt
@@ -46,8 +55,9 @@ type State struct {
// Prompt is a interactive prompt for command-line
type Prompt struct {
- list *List
- opts *Options
+ list *List
+ opts *Options
+ keyBindings []*KeyBinding
keyHandler keyHandlerFunc
selectionHandler selectionHandlerFunc
@@ -57,6 +67,7 @@ type Prompt struct {
exitMsg [][]term.Cell // to be set on runtime if required
Controls map[string]string // to be updated if additional controls added
+ vim bool
inputMode bool
helpMode bool
itemsLabel string
@@ -87,6 +98,7 @@ func Create(label string, opts *Options, list *List, fs ...OptionalFunc) *Prompt
var mx sync.RWMutex
p.mx = &mx
+ p.vim = opts.VimKeys
p.reader = term.NewRuneReader(os.Stdin)
p.writer = term.NewBufferedWriter(os.Stdout)
@@ -203,11 +215,15 @@ mainloop:
case rune(term.KeyCtrlC), rune(term.KeyCtrlD):
break mainloop
case term.Enter, term.NewLine:
- if err = p.selectionHandler(); err != nil {
+ items, idx := p.list.Items()
+ if idx == NotFound {
+ continue
+ }
+ if err = p.selectionHandler(items[idx]); err != nil {
break mainloop
}
default:
- if err = p.keyBindings(r); err != nil {
+ if err = p.onKey(r); err != nil {
break mainloop
}
}
@@ -257,7 +273,14 @@ func (p *Prompt) render() {
}
}
-func (p *Prompt) keyBindings(key rune) error {
+// AddKeyBinding adds a key-function map to prompt
+func (p *Prompt) AddKeyBinding(b *KeyBinding) error {
+ p.keyBindings = append(p.keyBindings, b)
+ return nil
+}
+
+// default key handling function
+func (p *Prompt) onKey(key rune) error {
if p.helpMode {
p.helpMode = false
return nil
@@ -274,7 +297,6 @@ func (p *Prompt) keyBindings(key rune) error {
default:
if key == '/' {
p.inputMode = !p.inputMode
- // p.input = ""
} else if p.inputMode {
switch key {
case term.Backspace, term.Backspace2:
@@ -290,19 +312,24 @@ func (p *Prompt) keyBindings(key rune) error {
p.list.Search(p.input)
} else if key == '?' {
p.helpMode = !p.helpMode
- } else if key == 'h' || key == 'j' || key == 'k' || key == 'l' {
- switch key {
- case 'k':
- p.list.Prev()
- case 'j':
- p.list.Next()
- case 'h':
- p.list.PageDown()
- case 'l':
- p.list.PageUp()
- }
+ } else if p.vim && key == 'k' {
+ p.list.Prev()
+ } else if p.vim && key == 'j' {
+ p.list.Next()
+ } else if p.vim && key == 'h' {
+ p.list.PageDown()
+ } else if p.vim && key == 'l' {
+ p.list.PageUp()
} else {
- return p.keyHandler(key)
+ items, idx := p.list.Items()
+ if idx == NotFound {
+ return nil
+ }
+ for _, kb := range p.keyBindings {
+ if kb.Key == key {
+ return kb.Handler(items[idx])
+ }
+ }
}
}
return nil
@@ -310,32 +337,18 @@ func (p *Prompt) keyBindings(key rune) error {
func (p *Prompt) allControls() map[string]string {
controls := make(map[string]string)
- controls["navigation"] = "← ↓ ↑ → (h,j,k,l)"
- controls["quit app"] = "q"
- controls["toggle search"] = "/"
- for k, v := range p.Controls {
- controls[k] = v
+ controls["← ↓ ↑ → (h,j,k,l)"] = "navigation"
+ controls["/"] = "toggle search"
+ for _, kb := range p.keyBindings {
+ controls[kb.Display] = kb.Desc
}
return controls
}
-// onKey is the default keybinding function for a prompt
-func (p *Prompt) onKey(key rune) error {
- switch key {
- case 'q':
- p.Stop()
- default:
- }
- return nil
-}
-
// onSelect is the default selection
-func (p *Prompt) onSelect() error {
- items, idx := p.list.Items()
- if idx == NotFound {
- return fmt.Errorf("could not select an item")
- }
- p.writer.WriteCells(term.Cprint(fmt.Sprint(items[idx])))
+func (p *Prompt) onSelect(item interface{}) error {
+ p.SetExitMsg([][]term.Cell{[]term.Cell{}, term.Cprint(fmt.Sprint(item))})
+ p.Stop()
return nil
}
@@ -373,15 +386,6 @@ func (p *Prompt) ListSize() int {
return p.opts.LineSize
}
-// Selection returns the selected item
-func (p *Prompt) Selection() (interface{}, error) {
- items, idx := p.list.Items()
- if idx == NotFound {
- return nil, fmt.Errorf("there is no item to be selected")
- }
- return items[idx], nil
-}
-
// SetExitMsg adds a rendered cell grid to be printed after prompt is finished
func (p *Prompt) SetExitMsg(grid [][]term.Cell) {
p.exitMsg = grid
diff --git a/prompt/renderer.go b/prompt/renderer.go
index bd255ff..c81e0ed 100644
--- a/prompt/renderer.go
+++ b/prompt/renderer.go
@@ -41,15 +41,19 @@ func itemText(item interface{}, matches []int, selected bool) []term.Cell {
// returns multiline so the return value will be a 2-d slice
func genHelp(pairs map[string]string) [][]term.Cell {
var grid [][]term.Cell
- // sort keys alphabetically
+ n := map[string][]string{}
+ // sort keys alphabetically, sort by values
keys := make([]string, 0, len(pairs))
- for key := range pairs {
- keys = append(keys, key)
+ for k, v := range pairs {
+ n[v] = append(n[v], k)
+ }
+ for k := range n {
+ keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
grid = append(grid, append(term.Cprint(fmt.Sprintf("%s: ", key), color.Faint),
- term.Cprint(fmt.Sprintf("%s", pairs[key]), color.FgYellow)...))
+ term.Cprint(fmt.Sprintf("%s", n[key][0]), color.FgYellow)...))
}
grid = append(grid, term.Cprint("", 0))
grid = append(grid, term.Cprint("press any key to return.", color.Faint))
From a93f0c37a3aca9e2f1861d518e9a5ab435e3a5f3 Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Fri, 21 Jun 2019 09:54:59 +0300
Subject: [PATCH 12/13] uodate README
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6a3afe8..6972ea7 100644
--- a/README.md
+++ b/README.md
@@ -63,8 +63,9 @@ Commands:
Environment Variables:
GITIN_LINESIZE=
- GITIN_STARTINSEARCH=
+ GITIN_STARTINSEARCH=
+ GITIN_VIMKEYS=
Press ? for controls while application is running.
@@ -75,6 +76,7 @@ Press ? for controls while application is running.
- To set the line size `export GITIN_LINESIZE=5`
- To set always start in search mode `GITIN_STARTSEARCH=true`
- To disable colors `GITIN_DISABLECOLOR=true`
+- To disable h,j,k,l for nav `GITIN_VIMKEYS=false`
## Development Requirements
From 1a98e23c3ff161bbb6afe2fe59d1c1ba55dbbaf2 Mon Sep 17 00:00:00 2001
From: Ibrahim Serdar Acikgoz
Date: Fri, 21 Jun 2019 09:55:34 +0300
Subject: [PATCH 13/13] ignore some errors that breaks the mainloop
unnecessarily
---
cli/status.go | 9 +++++++--
prompt/prompt.go | 2 +-
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/cli/status.go b/cli/status.go
index e51a34b..a54c6ea 100644
--- a/cli/status.go
+++ b/cli/status.go
@@ -156,11 +156,13 @@ func (s *status) hunkStageEntry(item interface{}) error {
}
func (s *status) commit(item interface{}) error {
- return s.bareCommit("--edit")
+ s.bareCommit("--edit") // why ignore err? simply to return status screen
+ return nil
}
func (s *status) amend(item interface{}) error {
- return s.bareCommit("--amend")
+ s.bareCommit("--amend")
+ return nil
}
func (s *status) bareCommit(arg string) error {
@@ -192,6 +194,9 @@ func (s *status) resetAllEntries(item interface{}) error {
func (s *status) checkoutEntry(item interface{}) error {
entry := item.(*git.StatusEntry)
+ if entry.EntryType == git.StatusEntryTypeUntracked {
+ return nil // you can't checkout untracked items
+ }
args := []string{"checkout", "--", entry.String()}
return s.runCommandWithArgs(args)
}
diff --git a/prompt/prompt.go b/prompt/prompt.go
index 99681ff..1f57129 100644
--- a/prompt/prompt.go
+++ b/prompt/prompt.go
@@ -217,7 +217,7 @@ mainloop:
case term.Enter, term.NewLine:
items, idx := p.list.Items()
if idx == NotFound {
- continue
+ break
}
if err = p.selectionHandler(items[idx]); err != nil {
break mainloop