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