summaryrefslogtreecommitdiff
path: root/gui
diff options
context:
space:
mode:
authorIbrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>2019-01-04 18:22:50 +0300
committerGitHub <noreply@github.com>2019-01-04 18:22:50 +0300
commit8a7a258de22432c5927ccfd2c9d5c41fea275b19 (patch)
tree1bd6738d5016aecbc944f04f0391197541cbd7c3 /gui
parentMinor checks and changes throughout files (#53) (diff)
parentcleanup before version increase (diff)
downloadgitbatch-8a7a258de22432c5927ccfd2c9d5c41fea275b19.tar.gz
Merge pull request #54 from isacikgoz/develop
Develop (0.3.0)
Diffstat (limited to 'gui')
-rw-r--r--gui/authenticationview.go188
-rw-r--r--gui/commitview.go189
-rw-r--r--gui/controlsview.go36
-rw-r--r--gui/diffview.go127
-rw-r--r--gui/errorview.go36
-rw-r--r--gui/extensions.go198
-rw-r--r--gui/gui.go245
-rw-r--r--gui/keybindings.go675
-rw-r--r--gui/mainview.go302
-rw-r--r--gui/sideviews.go232
-rw-r--r--gui/stagedview.go73
-rw-r--r--gui/stashview.go84
-rw-r--r--gui/statusview.go162
-rw-r--r--gui/text-renderer.go191
-rw-r--r--gui/unstagedview.go66
15 files changed, 2804 insertions, 0 deletions
diff --git a/gui/authenticationview.go b/gui/authenticationview.go
new file mode 100644
index 0000000..2faa3b7
--- /dev/null
+++ b/gui/authenticationview.go
@@ -0,0 +1,188 @@
+package gui
+
+import (
+ "fmt"
+ "regexp"
+
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/isacikgoz/gitbatch/core/job"
+ "github.com/jroimartin/gocui"
+ log "github.com/sirupsen/logrus"
+)
+
+var (
+ // this is required so we can know where we can return
+ authenticationReturnView string
+
+ // these views used as a label for git repository address and credential views
+ authenticationViewFeature = viewFeature{Name: "authentication", Title: " Authentication "}
+ authUserLabelFeature = viewFeature{Name: "authuserlabel", Title: " User: "}
+ authPswdLabelViewFeature = viewFeature{Name: "authpasswdlabel", Title: " Password: "}
+
+ // these views used as a input for the credentials
+ authUserFeature = viewFeature{Name: "authuser", Title: " User "}
+ authPasswordViewFeature = viewFeature{Name: "authpasswd", Title: " Password "}
+
+ // these are the view groups, so that we can assign common keybindings
+ authViews = []viewFeature{authUserFeature, authPasswordViewFeature}
+ authLabels = []viewFeature{authenticationViewFeature, authUserLabelFeature, authPswdLabelViewFeature}
+
+ // we can hold the job that is required to authenticate
+ jobRequiresAuth *job.Job
+)
+
+// open an auth view to get user credentials
+func (gui *Gui) openAuthenticationView(g *gocui.Gui, jobQueue *job.JobQueue, job *job.Job, returnViewName string) error {
+ maxX, maxY := g.Size()
+ // lets add this job since it is removed from the queue
+ // also it is already unsuccessfully finished
+ if err := jobQueue.AddJob(job); err != nil {
+ return err
+ }
+ jobRequiresAuth = job
+ if job.Repository.WorkStatus() != git.Fail {
+ if err := jobQueue.RemoveFromQueue(job.Repository); err != nil {
+ log.Fatal(err.Error())
+ return err
+ }
+ }
+ authenticationReturnView = returnViewName
+ v, err := g.SetView(authenticationViewFeature.Name, maxX/2-30, maxY/2-2, maxX/2+30, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(v, keySymbol+selectionIndicator+red.Sprint(jobRequiresAuth.Repository.State.Remote.URL[0]))
+ }
+ g.Cursor = true
+ if err := gui.openUserView(g); err != nil {
+ return err
+ }
+ return gui.openPasswordView(g)
+}
+
+// close the opened auth views
+func (gui *Gui) closeAuthenticationView(g *gocui.Gui, v *gocui.View) error {
+ g.Cursor = false
+ for _, vf := range authLabels {
+ if err := g.DeleteView(vf.Name); err != nil {
+ return nil
+ }
+ }
+ for _, vf := range authViews {
+ if err := g.DeleteView(vf.Name); err != nil {
+ return nil
+ }
+ }
+ return gui.closeViewCleanup(authenticationReturnView)
+}
+
+// close the opened auth views and submit the credentials
+func (gui *Gui) submitAuthenticationView(g *gocui.Gui, v *gocui.View) error {
+ g.Cursor = false
+
+ // in order to read buffer of the views, first we need to find'em
+ vUser, err := g.View(authUserFeature.Name)
+ if err != nil {
+ log.Errorln("error while retrieving user from view:", err)
+ return err // should return??
+ }
+
+ vPswd, err := g.View(authPasswordViewFeature.Name)
+ if err != nil {
+ log.Errorln("error while retrieving password from view:", err)
+ return err // should return??
+ }
+
+ // the return string of the views contain trailing new lines
+ re := regexp.MustCompile(`\r?\n`)
+ creduser := re.ReplaceAllString(vUser.ViewBuffer(), "")
+ credpswd := re.ReplaceAllString(vPswd.ViewBuffer(), "")
+
+ // since the git ops require different types of options we better switch
+ switch mode := jobRequiresAuth.JobType; mode {
+ case job.FetchJob:
+ jobRequiresAuth.Options = command.FetchOptions{
+ RemoteName: jobRequiresAuth.Repository.State.Remote.Name,
+ Credentials: git.Credentials{
+ User: creduser,
+ Password: credpswd,
+ },
+ }
+ case job.PullJob:
+ // we handle pull as fetch&merge so same rule applies
+ jobRequiresAuth.Options = command.PullOptions{
+ RemoteName: jobRequiresAuth.Repository.State.Remote.Name,
+ Credentials: git.Credentials{
+ User: creduser,
+ Password: credpswd,
+ },
+ }
+ }
+ jobRequiresAuth.Repository.SetWorkStatus(git.Queued)
+
+ // add this job to the last of the queue
+ if err := gui.State.Queue.AddJob(jobRequiresAuth); err != nil {
+ return err
+ }
+
+ return gui.closeAuthenticationView(g, v)
+}
+
+// open an error view to inform user with a message and a useful note
+func (gui *Gui) openUserView(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+ // first, create the label for user
+ vlabel, err := g.SetView(authUserLabelFeature.Name, maxX/2-30, maxY/2-1, maxX/2-19, maxY/2+1)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(vlabel, authUserLabelFeature.Title)
+ vlabel.Frame = false
+ }
+ // second, crete the user input
+ v, err := g.SetView(authUserFeature.Name, maxX/2-18, maxY/2-1, maxX/2+29, maxY/2+1)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = authUserFeature.Title
+ v.Editable = true
+ v.Frame = false
+ }
+ return gui.focusToView(authUserFeature.Name)
+}
+
+// open an error view to inform user with a message and a useful note
+func (gui *Gui) openPasswordView(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+ // first, create the label for password
+ vlabel, err := g.SetView(authPswdLabelViewFeature.Name, maxX/2-30, maxY/2, maxX/2-19, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(vlabel, authPswdLabelViewFeature.Title)
+ vlabel.Frame = false
+ }
+ // second, crete the masked password input
+ v, err := g.SetView(authPasswordViewFeature.Name, maxX/2-18, maxY/2, maxX/2+29, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = authPasswordViewFeature.Title
+ v.Editable = true
+ v.Mask ^= '*'
+ v.Frame = false
+ }
+ return nil
+}
+
+// focus to next view
+func (gui *Gui) nextAuthView(g *gocui.Gui, v *gocui.View) error {
+ err := gui.nextViewOfGroup(g, v, authViews)
+ return err
+}
diff --git a/gui/commitview.go b/gui/commitview.go
new file mode 100644
index 0000000..7acc5ca
--- /dev/null
+++ b/gui/commitview.go
@@ -0,0 +1,189 @@
+package gui
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/jroimartin/gocui"
+)
+
+var (
+ commitFrameViewFeature = viewFeature{Name: "commitframe", Title: " Frame "}
+ commitUserNameLabelFeature = viewFeature{Name: "commitusernamelabel", Title: " Name: "}
+ commitUserEmailLabelViewFeature = viewFeature{Name: "commituseremaillabel", Title: " E-Mail: "}
+
+ // these views used as a input for the credentials
+ commitMessageViewFeature = viewFeature{Name: "commitmessage", Title: " Commit Mesage "}
+ commitUserUserViewFeature = viewFeature{Name: "commitusername", Title: " Name "}
+ commitUserEmailViewFeature = viewFeature{Name: "commituseremail", Title: " E-Mail "}
+
+ commitViews = []viewFeature{commitMessageViewFeature, commitUserUserViewFeature, commitUserEmailViewFeature}
+ commitLabelViews = []viewFeature{commitFrameViewFeature, commitUserNameLabelFeature, commitUserEmailLabelViewFeature}
+)
+
+// open the commit message views
+func (gui *Gui) openCommitMessageView(g *gocui.Gui, v *gocui.View) error {
+ maxX, maxY := g.Size()
+ commitMesageReturnView = v.Name()
+ vFrame, err := g.SetView(commitFrameViewFeature.Name, maxX/2-30, maxY/2-4, maxX/2+30, maxY/2+3)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ vFrame.Frame = true
+ fmt.Fprintln(vFrame, " Enter your commit message:")
+ }
+ v, err = g.SetView(commitMessageViewFeature.Name, maxX/2-29, maxY/2-3, maxX/2+29, maxY/2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Frame = false
+ v.Wrap = true
+ v.Editable = true
+ v.Editor = gocui.DefaultEditor
+ g.Cursor = true
+ }
+ if err := gui.openCommitUserNameView(g); err != nil {
+ return err
+ }
+ if err := gui.openCommitUserEmailView(g); err != nil {
+ return err
+ }
+ return gui.focusToView(commitMessageViewFeature.Name)
+}
+
+// open an error view to inform user with a message and a useful note
+func (gui *Gui) openCommitUserNameView(g *gocui.Gui) error {
+ r := gui.getSelectedRepository()
+ maxX, maxY := g.Size()
+ // first, create the label for user
+ vlabel, err := g.SetView(commitUserNameLabelFeature.Name, maxX/2-30, maxY/2, maxX/2-19, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(vlabel, commitUserNameLabelFeature.Title)
+ vlabel.Frame = false
+ }
+ // second, crete the user input
+ v, err := g.SetView(commitUserUserViewFeature.Name, maxX/2-18, maxY/2, maxX/2+29, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ name, err := command.Config(r, command.ConfigOptions{
+ Section: "user",
+ Option: "name",
+ })
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(v, name)
+ v.Editable = true
+ v.Frame = false
+ }
+ return nil
+}
+
+// open an error view to inform user with a message and a useful note
+func (gui *Gui) openCommitUserEmailView(g *gocui.Gui) error {
+ r := gui.getSelectedRepository()
+ maxX, maxY := g.Size()
+ // first, create the label for password
+ vlabel, err := g.SetView(commitUserEmailLabelViewFeature.Name, maxX/2-30, maxY/2+1, maxX/2-19, maxY/2+3)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(vlabel, commitUserEmailLabelViewFeature.Title)
+ vlabel.Frame = false
+ }
+ // second, crete the masked password input
+ v, err := g.SetView(commitUserEmailViewFeature.Name, maxX/2-18, maxY/2+1, maxX/2+29, maxY/2+3)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ email, err := command.Config(r, command.ConfigOptions{
+ Section: "user",
+ Option: "email",
+ })
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(v, email)
+ v.Editable = true
+ v.Frame = false
+ }
+ return nil
+}
+
+// close the opened commite mesage view
+func (gui *Gui) submitCommitMessageView(g *gocui.Gui, v *gocui.View) error {
+ r := gui.getSelectedRepository()
+
+ // in order to read buffer of the views, first we need to find'em
+ vMsg, err := g.View(commitMessageViewFeature.Name)
+ if err != nil {
+ return err // should return??
+ }
+
+ vName, err := g.View(commitUserUserViewFeature.Name)
+ if err != nil {
+ return err // should return??
+ }
+
+ vEmail, err := g.View(commitUserEmailViewFeature.Name)
+ if err != nil {
+ return err // should return??
+ }
+
+ // the return string of the views contain trailing new lines
+ re := regexp.MustCompile(`\r?\n`)
+ // TODO: maybe intentionally added new lines?
+ msg := re.ReplaceAllString(vMsg.ViewBuffer(), "")
+ name := re.ReplaceAllString(vName.ViewBuffer(), "")
+ email := re.ReplaceAllString(vEmail.ViewBuffer(), "")
+ if len(email) <= 0 {
+ return errors.New("User email needs to be provided")
+ }
+
+ err = command.CommitCommand(r, command.CommitOptions{
+ CommitMsg: msg,
+ User: name,
+ Email: email,
+ })
+ if err != nil {
+ return err
+ }
+
+ return gui.closeCommitMessageView(g, v)
+}
+
+// focus to next view
+func (gui *Gui) nextCommitView(g *gocui.Gui, v *gocui.View) error {
+ return gui.nextViewOfGroup(g, v, commitViews)
+}
+
+// close the opened commite mesage view
+func (gui *Gui) closeCommitMessageView(g *gocui.Gui, v *gocui.View) error {
+ r := gui.getSelectedRepository()
+ g.Cursor = false
+ for _, view := range commitViews {
+ if err := g.DeleteView(view.Name); err != nil {
+ return err
+ }
+ }
+ for _, view := range commitLabelViews {
+ if err := g.DeleteView(view.Name); err != nil {
+ return err
+ }
+ }
+ if err := refreshAllStatusView(g, r, true); err != nil {
+ return err
+ }
+ return gui.closeViewCleanup(commitMesageReturnView)
+}
diff --git a/gui/controlsview.go b/gui/controlsview.go
new file mode 100644
index 0000000..4b2f97e
--- /dev/null
+++ b/gui/controlsview.go
@@ -0,0 +1,36 @@
+package gui
+
+import (
+ "fmt"
+
+ "github.com/jroimartin/gocui"
+)
+
+// open the application controls
+// TODO: view size can handled better for such situations like too small
+// application area
+func (gui *Gui) openCheatSheetView(g *gocui.Gui, v *gocui.View) error {
+ maxX, maxY := g.Size()
+ v, err := g.SetView(cheatSheetViewFeature.Name, maxX/2-25, maxY/2-10, maxX/2+25, maxY/2+10)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = cheatSheetViewFeature.Title
+ for _, k := range gui.KeyBindings {
+ if k.View == mainViewFeature.Name || k.View == "" {
+ binding := " " + cyan.Sprint(k.Display) + ": " + k.Description
+ fmt.Fprintln(v, binding)
+ }
+ }
+ }
+ return gui.focusToView(cheatSheetViewFeature.Name)
+}
+
+// close the application controls and do the clean job
+func (gui *Gui) closeCheatSheetView(g *gocui.Gui, v *gocui.View) error {
+ if err := g.DeleteView(v.Name()); err != nil {
+ return nil
+ }
+ return gui.closeViewCleanup(mainViewFeature.Name)
+}
diff --git a/gui/diffview.go b/gui/diffview.go
new file mode 100644
index 0000000..9f49456
--- /dev/null
+++ b/gui/diffview.go
@@ -0,0 +1,127 @@
+package gui
+
+import (
+ "fmt"
+
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/jroimartin/gocui"
+)
+
+var diffReturnView string
+
+// renders the diff view
+func (gui *Gui) prepareDiffView(g *gocui.Gui, v *gocui.View, display []string) (out *gocui.View, err error) {
+ maxX, maxY := g.Size()
+ diffReturnView = v.Name()
+ out, err = g.SetView(diffViewFeature.Name, 5, 3, maxX-5, maxY-3)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return out, err
+ }
+ }
+ out.Title = diffViewFeature.Title
+ out.Overwrite = true
+ out.Wrap = true
+ if err = gui.focusToView(diffViewFeature.Name); err != nil {
+ return out, err
+ }
+ for _, line := range display {
+ fmt.Fprintln(out, line)
+ }
+ return out, err
+}
+
+// open diff view for the selcted commit
+// called from commitview, so initial view is commitview
+func (gui *Gui) openCommitDiffView(g *gocui.Gui, v *gocui.View) (err error) {
+ r := gui.getSelectedRepository()
+ commit := r.State.Commit
+ commitDetail := []string{("Hash: " + cyan.Sprint(commit.Hash) + "\n" + "Author: " + commit.Author +
+ "\n" + commit.Time + "\n" + "\n" + "\t\t" + commit.Message + "\n")}
+ diff, err := command.Diff(r, r.State.Commit.Hash)
+ if err != nil {
+ return err
+ }
+ colorized := colorizeDiff(diff)
+ commitDetail = append(commitDetail, colorized...)
+ out, err := gui.prepareDiffView(g, v, commitDetail)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ }
+ out.Title = " Commit Detail "
+ return nil
+}
+
+// called from status, so initial view may be stagedview or unstaged view
+func (gui *Gui) openFileDiffView(g *gocui.Gui, v *gocui.View) (err error) {
+
+ _, cy := v.Cursor()
+ _, oy := v.Origin()
+ var files []*command.File
+ switch v.Name() {
+ case unstageViewFeature.Name:
+ files = unstagedFiles
+ case stageViewFeature.Name:
+ files = stagedFiles
+ }
+
+ if len(files) <= 0 {
+ return nil
+ }
+ output, err := files[cy+oy].Diff()
+ if err != nil || len(output) <= 0 {
+ return nil
+ }
+ if err != nil {
+ if err = gui.openErrorView(g, output,
+ "You should manually resolve this issue",
+ diffReturnView); err != nil {
+ return err
+ }
+ }
+ colorized := colorizeDiff(output)
+ _, err = gui.prepareDiffView(g, v, colorized)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ }
+ return nil
+}
+
+// called from stashview, so initial view is stashview
+func (gui *Gui) showStash(g *gocui.Gui, v *gocui.View) (err error) {
+ r := gui.getSelectedRepository()
+ _, oy := v.Origin()
+ _, cy := v.Cursor()
+ if len(r.Stasheds) <= 0 {
+ return nil
+ }
+ stashedItem := r.Stasheds[oy+cy]
+ output, err := stashedItem.Show()
+ if err != nil {
+ if err = gui.openErrorView(g, output,
+ "You should manually resolve this issue",
+ stashViewFeature.Name); err != nil {
+ return err
+ }
+ }
+ colorized := colorizeDiff(output)
+ _, err = gui.prepareDiffView(g, v, colorized)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ }
+ return nil
+}
+
+// close the opened diff view
+func (gui *Gui) closeCommitDiffView(g *gocui.Gui, v *gocui.View) error {
+ if err := g.DeleteView(v.Name()); err != nil {
+ return nil
+ }
+ return gui.closeViewCleanup(diffReturnView)
+}
diff --git a/gui/errorview.go b/gui/errorview.go
new file mode 100644
index 0000000..f679b54
--- /dev/null
+++ b/gui/errorview.go
@@ -0,0 +1,36 @@
+package gui
+
+import (
+ "fmt"
+
+ "github.com/jroimartin/gocui"
+)
+
+var errorReturnView string
+
+// open an error view to inform user with a message and a useful note
+func (gui *Gui) openErrorView(g *gocui.Gui, message, note, returnViewName string) error {
+ maxX, maxY := g.Size()
+ errorReturnView = returnViewName
+ v, err := g.SetView(errorViewFeature.Name, maxX/2-30, maxY/2-3, maxX/2+30, maxY/2+3)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = errorViewFeature.Title
+ v.Wrap = true
+ ps := red.Sprint("Note:") + " " + note
+ fmt.Fprintln(v, message)
+ fmt.Fprintln(v, ps)
+ }
+ return gui.focusToView(errorViewFeature.Name)
+}
+
+// close the opened error view
+func (gui *Gui) closeErrorView(g *gocui.Gui, v *gocui.View) error {
+
+ if err := g.DeleteView(v.Name()); err != nil {
+ return nil
+ }
+ return gui.closeViewCleanup(errorReturnView)
+}
diff --git a/gui/extensions.go b/gui/extensions.go
new file mode 100644
index 0000000..b25848c
--- /dev/null
+++ b/gui/extensions.go
@@ -0,0 +1,198 @@
+package gui
+
+import (
+ "github.com/jroimartin/gocui"
+ log "github.com/sirupsen/logrus"
+)
+
+// focus to next view
+func (gui *Gui) nextViewOfGroup(g *gocui.Gui, v *gocui.View, group []viewFeature) error {
+ var focusedViewName string
+ if v == nil || v.Name() == group[len(group)-1].Name {
+ focusedViewName = group[0].Name
+ } else {
+ for i := range group {
+ if v.Name() == group[i].Name {
+ focusedViewName = group[i+1].Name
+ break
+ }
+ if i == len(group)-1 {
+ return nil
+ }
+ }
+ }
+ if _, err := g.SetCurrentView(focusedViewName); err != nil {
+ log.WithFields(log.Fields{
+ "view": focusedViewName,
+ }).Warn("View cannot be focused.")
+ return nil
+ }
+
+ return gui.updateKeyBindingsView(g, focusedViewName)
+}
+
+// focus to previous view
+func (gui *Gui) previousViewOfGroup(g *gocui.Gui, v *gocui.View, group []viewFeature) error {
+ var focusedViewName string
+ if v == nil || v.Name() == group[0].Name {
+ focusedViewName = group[len(group)-1].Name
+ } else {
+ for i := range group {
+ if v.Name() == group[i].Name {
+ focusedViewName = group[i-1].Name
+ break
+ }
+ if i == len(group)-1 {
+ return nil
+ }
+ }
+ }
+ if _, err := g.SetCurrentView(focusedViewName); err != nil {
+ log.WithFields(log.Fields{
+ "view": focusedViewName,
+ }).Warn("View cannot be focused.")
+ return nil
+ }
+
+ return gui.updateKeyBindingsView(g, focusedViewName)
+}
+
+// siwtch the app's mode to fetch
+func (gui *Gui) switchToFetchMode(g *gocui.Gui, v *gocui.View) error {
+ gui.State.Mode = fetchMode
+ return gui.updateKeyBindingsView(g, mainViewFeature.Name)
+}
+
+// siwtch the app's mode to pull
+func (gui *Gui) switchToPullMode(g *gocui.Gui, v *gocui.View) error {
+ gui.State.Mode = pullMode
+ return gui.updateKeyBindingsView(g, mainViewFeature.Name)
+}
+
+// siwtch the app's mode to merge
+func (gui *Gui) switchToMergeMode(g *gocui.Gui, v *gocui.View) error {
+ gui.State.Mode = mergeMode
+ return gui.updateKeyBindingsView(g, mainViewFeature.Name)
+}
+
+// bring the view on the top by its name
+func (gui *Gui) setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) {
+ if _, err := g.SetCurrentView(name); err != nil {
+ return nil, err
+ }
+ return g.SetViewOnTop(name)
+}
+
+// if the cursor down past the last item, move it to the last line
+func (gui *Gui) correctCursor(v *gocui.View) error {
+ cx, cy := v.Cursor()
+ ox, oy := v.Origin()
+ width, height := v.Size()
+ maxY := height - 1
+ ly := width - 1
+ if oy+cy <= ly {
+ return nil
+ }
+ newCy := min(ly, maxY)
+ if err := v.SetCursor(cx, newCy); err != nil {
+ return err
+ }
+ err := v.SetOrigin(ox, ly-newCy)
+ return err
+}
+
+// min finds the minimum value of two int
+func min(x, y int) int {
+ if x < y {
+ return x
+ }
+ return y
+}
+
+// this function handles the iteration of a side view and set its origin point
+// so that the selected line can be in the middle of the view
+func (gui *Gui) smartAnchorRelativeToLine(v *gocui.View, currentindex, totallines int) error {
+ _, y := v.Size()
+ if currentindex >= int(0.5*float32(y)) && totallines-currentindex+int(0.5*float32(y)) >= y {
+ if err := v.SetOrigin(0, currentindex-int(0.5*float32(y))); err != nil {
+ return err
+ }
+ } else if totallines-currentindex < y && totallines > y {
+ if err := v.SetOrigin(0, totallines-y); err != nil {
+ return err
+ }
+ } else if totallines-currentindex <= int(0.5*float32(y)) && totallines > y-1 && currentindex > y {
+ if err := v.SetOrigin(0, currentindex-int(0.5*float32(y))); err != nil {
+ return err
+ }
+ } else {
+ if err := v.SetOrigin(0, 0); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// this function writes the given text to rgiht hand side of the view
+// cx and cy values are important to get the cursor to its old position
+func writeRightHandSide(v *gocui.View, text string, cx, cy int) error {
+ runes := []rune(text)
+ tl := len(runes)
+ lx, _ := v.Size()
+ v.MoveCursor(lx-tl, cy-1, true)
+ for i := tl - 1; i >= 0; i-- {
+ v.EditDelete(true)
+ v.EditWrite(runes[i])
+ }
+ v.SetCursor(cx, cy)
+ return nil
+}
+
+// cursor down acts like half-page down for faster scrolling
+func (gui *Gui) fastCursorDown(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, oy := v.Origin()
+ _, vy := v.Size()
+ if len(v.BufferLines())+len(v.ViewBufferLines()) <= vy+oy || len(v.ViewBufferLines()) < vy {
+ return nil
+ }
+ // TODO: do something when it hits bottom
+ if err := v.SetOrigin(ox, oy+vy/2); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// cursor up acts like half-page up for faster scrolling
+func (gui *Gui) fastCursorUp(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, oy := v.Origin()
+ _, vy := v.Size()
+
+ if oy-vy/2 > 0 {
+ if err := v.SetOrigin(ox, oy-vy/2); err != nil {
+ return err
+ }
+ } else if oy-vy/2 <= 0 {
+ if err := v.SetOrigin(0, 0); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// closeViewCleanup both updates the keybidings view and focuses to returning view
+func (gui *Gui) closeViewCleanup(returningViewName string) (err error) {
+ if _, err = gui.g.SetCurrentView(returningViewName); err != nil {
+ return err
+ }
+ err = gui.updateKeyBindingsView(gui.g, returningViewName)
+ return err
+}
+
+// focus to view same as closeViewCleanup but its just a wrapper for easy reading
+func (gui *Gui) focusToView(viewName string) (err error) {
+ return gui.closeViewCleanup(viewName)
+}
diff --git a/gui/gui.go b/gui/gui.go
new file mode 100644
index 0000000..767bf16
--- /dev/null
+++ b/gui/gui.go
@@ -0,0 +1,245 @@
+package gui
+
+import (
+ "fmt"
+ "sort"
+ "sync"
+
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/isacikgoz/gitbatch/core/job"
+ "github.com/isacikgoz/gitbatch/core/load"
+ "github.com/jroimartin/gocui"
+ log "github.com/sirupsen/logrus"
+)
+
+// Gui struct hold the gocui struct along with the gui's state, also keybindings
+// are tied with this struct in order to render those in different occasions
+type Gui struct {
+ g *gocui.Gui
+ KeyBindings []*KeyBinding
+ State guiState
+ mutex *sync.Mutex
+}
+
+// guiState struct holds the repositories, directiories, mode and queue of the
+// gui object. These values are not static
+type guiState struct {
+ Repositories []*git.Repository
+ Directories []string
+ Mode mode
+ Queue *job.JobQueue
+ FailoverQueue *job.JobQueue
+}
+
+// this struct encapsulates the name and title of a view. the name of a view is
+// passed around so much it is added so that I don't need to wirte names again
+type viewFeature struct {
+ Name string
+ Title string
+}
+
+// mode of the gui
+type mode struct {
+ ModeID ModeID
+ DisplayString string
+ CommandString string
+}
+
+// ModeID is the mode indicator for the gui
+type ModeID string
+
+const (
+ // FetchMode puts the gui in fetch state
+ FetchMode ModeID = "fetch"
+ // PullMode puts the gui in pull state
+ PullMode ModeID = "pull"
+ // MergeMode puts the gui in merge state
+ MergeMode ModeID = "merge"
+)
+
+var (
+ mainViewFeature = viewFeature{Name: "main", Title: " Matched Repositories "}
+ loadingViewFeature = viewFeature{Name: "loading", Title: " Loading in Progress "}
+ branchViewFeature = viewFeature{Name: "branch", Title: " Local Branches "}
+ remoteViewFeature = viewFeature{Name: "remotes", Title: " Remotes "}
+ remoteBranchViewFeature = viewFeature{Name: "remotebranches", Title: " Remote Branches "}
+ commitViewFeature = viewFeature{Name: "commits", Title: " Commits "}
+ scheduleViewFeature = viewFeature{Name: "schedule", Title: " Schedule "}
+ keybindingsViewFeature = viewFeature{Name: "keybindings", Title: " Keybindings "}
+ diffViewFeature = viewFeature{Name: "diff", Title: " Diff Detail "}
+ cheatSheetViewFeature = viewFeature{Name: "cheatsheet", Title: " Application Controls "}
+ errorViewFeature = viewFeature{Name: "error", Title: " Error "}
+
+ fetchMode = mode{ModeID: FetchMode, DisplayString: "Fetch", CommandString: "fetch"}
+ pullMode = mode{ModeID: PullMode, DisplayString: "Pull", CommandString: "pull"}
+ mergeMode = mode{ModeID: MergeMode, DisplayString: "Merge", CommandString: "merge"}
+
+ mainViews = []viewFeature{mainViewFeature, remoteViewFeature, remoteBranchViewFeature, branchViewFeature, commitViewFeature}
+ modes = []mode{fetchMode, pullMode, mergeMode}
+
+ loaded = make(chan bool)
+)
+
+// NewGui creates a Gui opject and fill it's state related entites
+func NewGui(mode string, directoies []string) (*Gui, error) {
+ initialState := guiState{
+ Directories: directoies,
+ Mode: fetchMode,
+ Queue: job.CreateJobQueue(),
+ FailoverQueue: job.CreateJobQueue(),
+ }
+ gui := &Gui{
+ State: initialState,
+ mutex: &sync.Mutex{},
+ }
+ for _, m := range modes {
+ if string(m.ModeID) == mode {
+ gui.State.Mode = m
+ break
+ }
+ }
+ return gui, nil
+}
+
+// Run function runs the main loop with initial values
+func (gui *Gui) Run() error {
+ g, err := gocui.NewGui(gocui.OutputNormal)
+ if err != nil {
+ return err
+ }
+ defer g.Close()
+
+ gui.g = g
+ g.Highlight = true
+ g.SelFgColor = gocui.ColorGreen
+
+ g.InputEsc = true
+ g.SetManagerFunc(gui.layout)
+
+ // load repositories in background asynchronously
+ go load.AsyncLoad(gui.State.Directories, gui.loadRepository, loaded)
+
+ if err := gui.generateKeybindings(); err != nil {
+ log.Error("Keybindings could not be created.")
+ return err
+ }
+ if err := gui.keybindings(g); err != nil {
+ log.Error("Keybindings could not be set.")
+ return err
+ }
+ if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
+ log.Error("Error in the main loop. " + err.Error())
+ return err
+ }
+ return nil
+}
+
+func (gui *Gui) loadRepository(r *git.Repository) {
+ rs := gui.State.Repositories
+
+ // insertion sort implementation
+ index := sort.Search(len(rs), func(i int) bool { return git.Less(r, rs[i]) })
+ rs = append(rs, &git.Repository{})
+ copy(rs[index+1:], rs[index:])
+ rs[index] = r
+ // add listener
+ r.On(git.RepositoryUpdated, gui.repositoryUpdated)
+ // update gui
+ gui.repositoryUpdated(nil)
+ gui.renderTitle()
+ // take pointer back
+ gui.State.Repositories = rs
+ go func() {
+ if <-loaded {
+ v, err := gui.g.View(mainViewFeature.Name)
+ if err != nil {
+ log.Warn(err.Error())
+ return
+ }
+ v.Title = mainViewFeature.Title + fmt.Sprintf("(%d) ", len(gui.State.Repositories))
+ }
+ }()
+}
+
+func (gui *Gui) renderTitle() error {
+ v, err := gui.g.View(mainViewFeature.Name)
+ if err != nil {
+ log.Warn(err.Error())
+ return err
+ }
+ v.Title = mainViewFeature.Title + fmt.Sprintf("(%d/%d) ", len(gui.State.Repositories), len(gui.State.Directories))
+ return nil
+}
+
+// set the layout and create views with their default size, name etc. values
+// TODO: window sizes can be handled better
+func (gui *Gui) layout(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+ if v, err := g.SetView(mainViewFeature.Name, 0, 0, int(0.55*float32(maxX))-1, maxY-2); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = mainViewFeature.Title
+ v.Overwrite = true
+ if _, err := gui.setCurrentViewOnTop(g, mainViewFeature.Name); err != nil {
+ return err
+ }
+ }
+ if v, err := g.SetView(remoteViewFeature.Name, int(0.55*float32(maxX)), 0, maxX-1, int(0.10*float32(maxY))); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = remoteViewFeature.Title
+ v.Wrap = false
+ v.Autoscroll = false
+ }
+ if v, err := g.SetView(remoteBranchViewFeature.Name, int(0.55*float32(maxX)), int(0.10*float32(maxY))+1, maxX-1, int(0.35*float32(maxY))); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = remoteBranchViewFeature.Title
+ v.Wrap = false
+ v.Overwrite = false
+ }
+ if v, err := g.SetView(branchViewFeature.Name, int(0.55*float32(maxX)), int(0.35*float32(maxY))+1, maxX-1, int(0.60*float32(maxY))); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = branchViewFeature.Title
+ v.Wrap = false
+ v.Autoscroll = false
+ }
+ if v, err := g.SetView(commitViewFeature.Name, int(0.55*float32(maxX)), int(0.60*float32(maxY))+1, maxX-1, maxY-2); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = commitViewFeature.Title
+ v.Wrap = false
+ v.Autoscroll = false
+ }
+ if v, err := g.SetView(keybindingsViewFeature.Name, -1, maxY-2, maxX, maxY); err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.BgColor = gocui.ColorWhite
+ v.FgColor = gocui.ColorBlack
+ v.Frame = false
+ gui.updateKeyBindingsView(g, mainViewFeature.Name)
+ }
+ return nil
+}
+
+// focus to next view
+func (gui *Gui) nextMainView(g *gocui.Gui, v *gocui.View) error {
+ return gui.nextViewOfGroup(g, v, mainViews)
+}
+
+// focus to previous view
+func (gui *Gui) previousMainView(g *gocui.Gui, v *gocui.View) error {
+ return gui.previousViewOfGroup(g, v, mainViews)
+}
+
+// quit from the gui and end its loop
+func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
+ return gocui.ErrQuit
+}
diff --git a/gui/keybindings.go b/gui/keybindings.go
new file mode 100644
index 0000000..668fe90
--- /dev/null
+++ b/gui/keybindings.go
@@ -0,0 +1,675 @@
+package gui
+
+import (
+ "fmt"
+
+ "github.com/jroimartin/gocui"
+)
+
+// KeyBinding structs is helpful for not re-writinh the same function over and
+// over again. it hold useful values to generate a controls view
+type KeyBinding struct {
+ View string
+ Handler func(*gocui.Gui, *gocui.View) error
+ Key interface{}
+ Modifier gocui.Modifier
+ Display string
+ Description string
+ Vital bool
+}
+
+// generate the gui's controls a.k.a. keybindings
+func (gui *Gui) generateKeybindings() error {
+ // Mainviews common keybindings
+ for _, view := range mainViews {
+ mainKeybindings := []*KeyBinding{
+ {
+ View: view.Name,
+ Key: 'q',
+ Modifier: gocui.ModNone,
+ Handler: gui.quit,
+ Display: "q",
+ Description: "Quit",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: 'f',
+ Modifier: gocui.ModNone,
+ Handler: gui.switchToFetchMode,
+ Display: "f",
+ Description: "Fetch mode",
+ Vital: false,
+ }, {
+ View: view.Name,
+ Key: 'p',
+ Modifier: gocui.ModNone,
+ Handler: gui.switchToPullMode,
+ Display: "p",
+ Description: "Pull mode",
+ Vital: false,
+ }, {
+ View: view.Name,
+ Key: 'm',
+ Modifier: gocui.ModNone,
+ Handler: gui.switchToMergeMode,
+ Display: "m",
+ Description: "Merge mode",
+ Vital: false,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyTab,
+ Modifier: gocui.ModNone,
+ Handler: gui.nextMainView,
+ Display: "tab",
+ Description: "Next Panel",
+ Vital: false,
+ },
+ }
+ gui.KeyBindings = append(gui.KeyBindings, mainKeybindings...)
+ }
+ for _, view := range sideViews {
+ sideViewKeybindings := []*KeyBinding{
+ {
+ View: view.Name,
+ Key: gocui.KeyArrowDown,
+ Modifier: gocui.ModNone,
+ Handler: gui.sideViewsNextItem,
+ Display: "↓",
+ Description: "Iterate over branches",
+ Vital: false,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyArrowUp,
+ Modifier: gocui.ModNone,
+ Handler: gui.sideViewsPreviousItem,
+ Display: "↑",
+ Description: "Iterate over branches",
+ Vital: false,
+ }, {
+ View: view.Name,
+ Key: 'j',
+ Modifier: gocui.ModNone,
+ Handler: gui.sideViewsNextItem,
+ Display: "j",
+ Description: "Down",
+ Vital: false,
+ }, {
+ View: view.Name,
+ Key: 'k',
+ Modifier: gocui.ModNone,
+ Handler: gui.sideViewsPreviousItem,
+ Display: "k",
+ Description: "Up",
+ Vital: false,
+ },
+ }
+ gui.KeyBindings = append(gui.KeyBindings, sideViewKeybindings...)
+ }
+ // Statusviews common keybindings
+ for _, view := range statusViews {
+ statusKeybindings := []*KeyBinding{
+ {
+ View: view.Name,
+ Key: 'q',
+ Modifier: gocui.ModNone,
+ Handler: gui.closeStatusView,
+ Display: "q",
+ Description: "Close/Cancel",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyTab,
+ Modifier: gocui.ModNone,
+ Handler: gui.nextStatusView,
+ Display: "tab",
+ Description: "Next Panel",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyArrowUp,
+ Modifier: gocui.ModNone,
+ Handler: gui.statusCursorUp,
+ Display: "↑",
+ Description: "Up",
+ Vital: false,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyArrowDown,
+ Modifier: gocui.ModNone,
+ Handler: gui.statusCursorDown,
+ Display: "↓",
+ Description: "Down",
+ Vital: false,
+ }, {
+ View: view.Name,
+ Key: 'k',
+ Modifier: gocui.ModNone,
+ Handler: gui.statusCursorUp,
+ Display: "k",
+ Description: "Up",
+ Vital: false,
+ }, {
+ View: view.Name,
+ Key: 'j',
+ Modifier: gocui.ModNone,
+ Handler: gui.statusCursorDown,
+ Display: "j",
+ Description: "Down",
+ Vital: false,
+ }, {
+ View: view.Name,
+ Key: 't',
+ Modifier: gocui.ModNone,
+ Handler: gui.stashChanges,
+ Display: "t",
+ Description: "Save to Stash",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: 'm',
+ Modifier: gocui.ModNone,
+ Handler: gui.openCommitMessageView,
+ Display: "m",
+ Description: "Commit Changes",
+ Vital: true,
+ },
+ }
+ gui.KeyBindings = append(gui.KeyBindings, statusKeybindings...)
+ }
+ for _, view := range authViews {
+ authKeybindings := []*KeyBinding{
+ {
+ View: view.Name,
+ Key: gocui.KeyEsc,
+ Modifier: gocui.ModNone,
+ Handler: gui.closeAuthenticationView,
+ Display: "esc",
+ Description: "Close/Cancel",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyTab,
+ Modifier: gocui.ModNone,
+ Handler: gui.nextAuthView,
+ Display: "tab",
+ Description: "Next Panel",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyEnter,
+ Modifier: gocui.ModNone,
+ Handler: gui.submitAuthenticationView,
+ Display: "enter",
+ Description: "Submit",
+ Vital: true,
+ },
+ }
+ gui.KeyBindings = append(gui.KeyBindings, authKeybindings...)
+ }
+ for _, view := range commitViews {
+ commitKeybindings := []*KeyBinding{
+ {
+ View: view.Name,
+ Key: gocui.KeyEsc,
+ Modifier: gocui.ModNone,
+ Handler: gui.closeCommitMessageView,
+ Display: "esc",
+ Description: "Close/Cancel",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyTab,
+ Modifier: gocui.ModNone,
+ Handler: gui.nextCommitView,
+ Display: "tab",
+ Description: "Next Panel",
+ Vital: true,
+ }, {
+ View: view.Name,
+ Key: gocui.KeyEnter,
+ Modifier: gocui.ModNone,
+ Handler: gui.submitCommitMessageView,
+ Display: "enter",
+ Description: "Submit",
+ Vital: true,
+ },
+ }
+ gui.KeyBindings = append(gui.KeyBindings, commitKeybindings...)
+ }
+ individualKeybindings := []*KeyBinding{
+ // stash view
+ {
+ View: stashViewFeature.Name,
+ Key: 'p',
+ Modifier: gocui.ModNone,
+ Handler: gui.popStash,
+ Display: "p",
+ Description: "Pop Item",
+ Vital: true,
+ }, {
+ View: stashViewFeature.Name,
+ Key: 'd',
+ Modifier: gocui.ModNone,
+ Handler: gui.showStash,
+ Display: "d",
+ Description: "Show diff",
+ Vital: true,
+ },
+ // staged view
+ {
+ View: stageViewFeature.Name,
+ Key: 'r',
+ Modifier: gocui.ModNone,
+ Handler: gui.resetChanges,
+ Display: "r",
+ Description: "Reset Item",
+ Vital: true,
+ }, {
+ View: stageViewFeature.Name,
+ Key: gocui.KeyCtrlR,
+ Modifier: gocui.ModNone,
+ Handler: gui.resetAllChanges,
+ Display: "ctrl+r",
+ Description: "Reset All Items",
+ Vital: true,
+ }, {
+ View: stageViewFeature.Name,
+ Key: 'd',
+ Modifier: gocui.ModNone,
+ Handler: gui.openFileDiffView,
+ Display: "d",
+ Description: "Show diff",
+ Vital: true,
+ },
+ // unstaged view
+ {
+ View: unstageViewFeature.Name,
+ Key: 'a',
+ Modifier: gocui.ModNone,
+ Handler: gui.addChanges,
+ Display: "a",
+ Description: "Add Item",
+ Vital: true,
+ }, {
+ View: unstageViewFeature.Name,
+ Key: gocui.KeyCtrlA,
+ Modifier: gocui.ModNone,
+ Handler: gui.addAllChanges,
+ Display: "ctrl+a",
+ Description: "Add All Items",
+ Vital: true,
+ }, {
+ View: unstageViewFeature.Name,
+ Key: 'd',
+ Modifier: gocui.ModNone,
+ Handler: gui.openFileDiffView,
+ Display: "d",
+ Description: "Show diff",
+ Vital: true,
+ },
+ // Main view controls
+ {
+ View: mainViewFeature.Name,
+ Key: 'u',
+ Modifier: gocui.ModNone,
+ Handler: gui.submitCredentials,
+ Display: "u",
+ Description: "Submit Credentials",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyArrowUp,
+ Modifier: gocui.ModNone,
+ Handler: gui.cursorUp,
+ Display: "↑",
+ Description: "Up",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyPgup,
+ Modifier: gocui.ModNone,
+ Handler: gui.pageUp,
+ Display: "page up",
+ Description: "Page up",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyHome,
+ Modifier: gocui.ModNone,
+ Handler: gui.cursorTop,
+ Display: "home",
+ Description: "Home",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyPgdn,
+ Modifier: gocui.ModNone,
+ Handler: gui.pageDown,
+ Display: "page down",
+ Description: "Page Down",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyEnd,
+ Modifier: gocui.ModNone,
+ Handler: gui.cursorEnd,
+ Display: "end",
+ Description: "End",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyArrowDown,
+ Modifier: gocui.ModNone,
+ Handler: gui.cursorDown,
+ Display: "↓",
+ Description: "Down",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: 'k',
+ Modifier: gocui.ModNone,
+ Handler: gui.cursorUp,
+ Display: "k",
+ Description: "Up",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: 'j',
+ Modifier: gocui.ModNone,
+ Handler: gui.cursorDown,
+ Display: "j",
+ Description: "Down",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeySpace,
+ Modifier: gocui.ModNone,
+ Handler: gui.markRepository,
+ Display: "space",
+ Description: "Select",
+ Vital: true,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyEnter,
+ Modifier: gocui.ModNone,
+ Handler: gui.startQueue,
+ Display: "enter",
+ Description: "Start",
+ Vital: true,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyCtrlSpace,
+ Modifier: gocui.ModNone,
+ Handler: gui.markAllRepositories,
+ Display: "ctrl + space",
+ Description: "Select All",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: gocui.KeyBackspace2,
+ Modifier: gocui.ModNone,
+ Handler: gui.unmarkAllRepositories,
+ Display: "backspace",
+ Description: "Deselect All",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: 'c',
+ Modifier: gocui.ModNone,
+ Handler: gui.openCheatSheetView,
+ Display: "c",
+ Description: "Controls",
+ Vital: true,
+ }, {
+ View: mainViewFeature.Name,
+ Key: 'n',
+ Modifier: gocui.ModNone,
+ Handler: gui.sortByName,
+ Display: "n",
+ Description: "Sort repositories by Name",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: 'r',
+ Modifier: gocui.ModNone,
+ Handler: gui.sortByMod,
+ Display: "r",
+ Description: "Sort repositories by Modification date",
+ Vital: false,
+ }, {
+ View: mainViewFeature.Name,
+ Key: 's',
+ Modifier: gocui.ModNone,
+ Handler: gui.openStatusView,
+ Display: "s",
+ Description: "Open Status",
+ Vital: true,
+ }, {
+ View: "",
+ Key: gocui.KeyCtrlC,
+ Modifier: gocui.ModNone,
+ Handler: gui.quit,
+ Display: "ctrl + c",
+ Description: "Force application to quit",
+ Vital: false,
+ }, {
+ View: remoteBranchViewFeature.Name,
+ Key: 's',
+ Modifier: gocui.ModNone,
+ Handler: gui.syncRemoteBranch,
+ Display: "s",
+ Description: "Synch with Remote",
+ Vital: true,
+ }, {
+ View: branchViewFeature.Name,
+ Key: 'u',
+ Modifier: gocui.ModNone,
+ Handler: gui.setUpstreamToBranch,
+ Display: "u",
+ Description: "Set Upstream",
+ Vital: true,
+ }, {
+ View: commitViewFeature.Name,
+ Key: 'd',
+ Modifier: gocui.ModNone,
+ Handler: gui.openCommitDiffView,
+ Display: "d",
+ Description: "Show commit diff",
+ Vital: true,
+ },
+ // upstream confirmation
+ {
+ View: confirmationViewFeature.Name,
+ Key: 'q',
+ Modifier: gocui.ModNone,
+ Handler: gui.closeConfirmationView,
+ Display: "q",
+ Description: "Close/Cancel",
+ Vital: true,
+ }, {
+ View: confirmationViewFeature.Name,
+ Key: gocui.KeyEnter,
+ Modifier: gocui.ModNone,
+ Handler: gui.confirmSetUpstreamToBranch,
+ Display: "enter",
+ Description: "Set Upstream",
+ Vital: true,
+ },
+ // Diff View Controls
+ {
+ View: diffViewFeature.Name,
+ Key: 'q',
+ Modifier: gocui.ModNone,
+ Handler: gui.closeCommitDiffView,
+ Display: "q",
+ Description: "Close/Cancel",
+ Vital: true,
+ }, {
+ View: diffViewFeature.Name,
+ Key: gocui.KeyArrowUp,
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorUp,
+ Display: "↑",
+ Description: "Page up",
+ Vital: true,
+ }, {
+ View: diffViewFeature.Name,
+ Key: gocui.KeyArrowDown,
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorDown,
+ Display: "↓",
+ Description: "Page down",
+ Vital: true,
+ }, {
+ View: diffViewFeature.Name,
+ Key: 'k',
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorUp,
+ Display: "k",
+ Description: "Page up",
+ Vital: false,
+ }, {
+ View: diffViewFeature.Name,
+ Key: 'j',
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorDown,
+ Display: "j",
+ Description: "Page down",
+ Vital: false,
+ },
+ // Application Controls
+ {
+ View: cheatSheetViewFeature.Name,
+ Key: 'q',
+ Modifier: gocui.ModNone,
+ Handler: gui.closeCheatSheetView,
+ Display: "q",
+ Description: "Close/Cancel",
+ Vital: true,
+ }, {
+ View: cheatSheetViewFeature.Name,
+ Key: gocui.KeyArrowUp,
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorUp,
+ Display: "↑",
+ Description: "Up",
+ Vital: true,
+ }, {
+ View: cheatSheetViewFeature.Name,
+ Key: gocui.KeyArrowDown,
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorDown,
+ Display: "↓",
+ Description: "Down",
+ Vital: true,
+ }, {
+ View: cheatSheetViewFeature.Name,
+ Key: 'k',
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorUp,
+ Display: "k",
+ Description: "Up",
+ Vital: false,
+ }, {
+ View: cheatSheetViewFeature.Name,
+ Key: 'j',
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorDown,
+ Display: "j",
+ Description: "Down",
+ Vital: false,
+ },
+ // Error View
+ {
+ View: errorViewFeature.Name,
+ Key: 'q',
+ Modifier: gocui.ModNone,
+ Handler: gui.closeErrorView,
+ Display: "q",
+ Description: "Close/Cancel",
+ Vital: true,
+ }, {
+ View: errorViewFeature.Name,
+ Key: gocui.KeyArrowUp,
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorUp,
+ Display: "↑",
+ Description: "Up",
+ Vital: true,
+ }, {
+ View: errorViewFeature.Name,
+ Key: gocui.KeyArrowDown,
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorDown,
+ Display: "↓",
+ Description: "Down",
+ Vital: true,
+ }, {
+ View: errorViewFeature.Name,
+ Key: 'k',
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorUp,
+ Display: "k",
+ Description: "Up",
+ Vital: false,
+ }, {
+ View: errorViewFeature.Name,
+ Key: 'j',
+ Modifier: gocui.ModNone,
+ Handler: gui.fastCursorDown,
+ Display: "j",
+ Description: "Down",
+ Vital: false,
+ },
+ }
+ gui.KeyBindings = append(gui.KeyBindings, individualKeybindings...)
+ return nil
+}
+
+// set the guis by iterating over a slice of the gui's keybindings struct
+func (gui *Gui) keybindings(g *gocui.Gui) error {
+ for _, k := range gui.KeyBindings {
+ if err := g.SetKeybinding(k.View, k.Key, k.Modifier, k.Handler); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// the bottom line of the gui is mode indicator and keybindings view. Only the
+// important controls (marked as vital) are shown
+func (gui *Gui) updateKeyBindingsView(g *gocui.Gui, viewName string) error {
+ v, err := g.View(keybindingsViewFeature.Name)
+ if err != nil {
+ return err
+ }
+ v.Clear()
+ v.BgColor = gocui.ColorWhite
+ v.FgColor = gocui.ColorBlack
+ v.Frame = false
+ fmt.Fprint(v, ws)
+ modeLabel := ""
+ switch mode := gui.State.Mode.ModeID; mode {
+ case FetchMode:
+ v.BgColor = gocui.ColorBlue
+ modeLabel = fetchSymbol + ws + "FETCH"
+ case PullMode:
+ v.BgColor = gocui.ColorMagenta
+ modeLabel = pullSymbol + ws + "PULL"
+ case MergeMode:
+ v.BgColor = gocui.ColorCyan
+ modeLabel = mergeSymbol + ws + "MERGE"
+ default:
+ modeLabel = "No mode selected"
+ }
+
+ fmt.Fprint(v, ws+modeLabel+ws+modeSeperator)
+
+ for _, k := range gui.KeyBindings {
+ if k.View == viewName && k.Vital {
+ binding := keyBindingSeperator + ws + k.Display + ":" + ws + k.Description + ws
+ fmt.Fprint(v, binding)
+ }
+ }
+ return nil
+}
diff --git a/gui/mainview.go b/gui/mainview.go
new file mode 100644
index 0000000..6cdaf61
--- /dev/null
+++ b/gui/mainview.go
@@ -0,0 +1,302 @@
+package gui
+
+import (
+ "fmt"
+ "sort"
+
+ gerr "github.com/isacikgoz/gitbatch/core/errors"
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/isacikgoz/gitbatch/core/job"
+ "github.com/jroimartin/gocui"
+ log "github.com/sirupsen/logrus"
+)
+
+// refresh the main view and re-render the repository representations
+func (gui *Gui) renderMain() error {
+ gui.mutex.Lock()
+ defer gui.mutex.Unlock()
+
+ mainView, err := gui.g.View(mainViewFeature.Name)
+ if err != nil {
+ return err
+ }
+ mainView.Clear()
+ for _, r := range gui.State.Repositories {
+ fmt.Fprintln(mainView, gui.repositoryLabel(r))
+ }
+ // while refreshing, refresh sideViews for selected entity, something may
+ // be changed?
+ return gui.renderSideViews(gui.getSelectedRepository())
+}
+
+// listens the event -> "repository.updated"
+func (gui *Gui) repositoryUpdated(event *git.RepositoryEvent) error {
+ gui.g.Update(func(g *gocui.Gui) error {
+ return gui.renderMain()
+ })
+ return nil
+}
+
+// moves the cursor downwards for the main view and if it goes to bottom it
+// prevents from going further
+func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ cx, cy := v.Cursor()
+ ox, oy := v.Origin()
+ ly := len(gui.State.Repositories) - 1
+
+ // if we are at the end we just return
+ if cy+oy == ly {
+ return nil
+ }
+ if err := v.SetCursor(cx, cy+1); err != nil {
+ if err := v.SetOrigin(ox, oy+1); err != nil {
+ return err
+ }
+ }
+ }
+ return gui.renderMain()
+}
+
+// moves the cursor upwards for the main view
+func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, oy := v.Origin()
+ cx, cy := v.Cursor()
+ if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
+ if err := v.SetOrigin(ox, oy-1); err != nil {
+ return err
+ }
+ }
+ }
+ return gui.renderMain()
+}
+
+// moves cursor to the top
+func (gui *Gui) cursorTop(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, _ := v.Origin()
+ cx, _ := v.Cursor()
+ if err := v.SetOrigin(ox, 0); err != nil {
+ return err
+ }
+ if err := v.SetCursor(cx, 0); err != nil {
+ return err
+ }
+ }
+ return gui.renderMain()
+}
+
+// moves cursor to the end
+func (gui *Gui) cursorEnd(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, _ := v.Origin()
+ cx, _ := v.Cursor()
+ _, vy := v.Size()
+ lr := len(gui.State.Repositories)
+ if lr <= vy {
+ if err := v.SetCursor(cx, lr-1); err != nil {
+ return err
+ }
+ return gui.renderMain()
+ }
+ if err := v.SetOrigin(ox, lr-vy); err != nil {
+ return err
+ }
+ if err := v.SetCursor(cx, vy-1); err != nil {
+ return err
+ }
+ }
+ return gui.renderMain()
+}
+
+// moves cursor down for a page size
+func (gui *Gui) pageDown(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, oy := v.Origin()
+ cx, _ := v.Cursor()
+ _, vy := v.Size()
+ lr := len(gui.State.Repositories)
+ if lr < vy {
+ return nil
+ }
+ if oy+vy >= lr-vy {
+ if err := v.SetOrigin(ox, lr-vy); err != nil {
+ return err
+ }
+ } else if err := v.SetOrigin(ox, oy+vy); err != nil {
+ return err
+ }
+ if err := v.SetCursor(cx, 0); err != nil {
+ return err
+ }
+ }
+ return gui.renderMain()
+}
+
+// moves cursor up for a page size
+func (gui *Gui) pageUp(g *gocui.Gui, v *gocui.View) error {
+ if v != nil {
+ ox, oy := v.Origin()
+ cx, cy := v.Cursor()
+ _, vy := v.Size()
+ if oy == 0 || oy+cy < vy {
+ if err := v.SetOrigin(ox, 0); err != nil {
+ return err
+ }
+ } else if oy <= vy {
+ if err := v.SetOrigin(ox, oy+cy-vy); err != nil {
+ return err
+ }
+ } else if err := v.SetOrigin(ox, oy-vy); err != nil {
+ return err
+ }
+ if err := v.SetCursor(cx, 0); err != nil {
+ return err
+ }
+ }
+ return gui.renderMain()
+}
+
+// returns the entity at cursors position by taking its position in the gui's
+// slice of repositories. Since it is not a %100 percent safe methodology it may
+// rrequire a better implementation or the slice's order must be synchronized
+// with the views lines
+func (gui *Gui) getSelectedRepository() *git.Repository {
+ if len(gui.State.Repositories) == 0 {
+ return nil
+ }
+ v, _ := gui.g.View(mainViewFeature.Name)
+ _, oy := v.Origin()
+ _, cy := v.Cursor()
+ return gui.State.Repositories[cy+oy]
+}
+
+// adds given entity to job queue
+func (gui *Gui) addToQueue(r *git.Repository) error {
+ var jt job.JobType
+ switch mode := gui.State.Mode.ModeID; mode {
+ case FetchMode:
+ jt = job.FetchJob
+ case PullMode:
+ jt = job.PullJob
+ case MergeMode:
+ jt = job.MergeJob
+ default:
+ return nil
+ }
+ err := gui.State.Queue.AddJob(&job.Job{
+ JobType: jt,
+ Repository: r,
+ })
+ if err != nil {
+ return err
+ }
+ r.SetWorkStatus(git.Queued)
+ return nil
+}
+
+// removes given entity from job queue
+func (gui *Gui) removeFromQueue(r *git.Repository) error {
+ err := gui.State.Queue.RemoveFromQueue(r)
+ if err != nil {
+ return err
+ }
+ r.SetWorkStatus(git.Available)
+ return nil
+}
+
+// this function starts the queue and updates the gui with the result of an
+// operation
+func (gui *Gui) startQueue(g *gocui.Gui, v *gocui.View) error {
+ go func(gui_go *Gui) {
+ fails := gui_go.State.Queue.StartJobsAsync()
+ gui_go.State.Queue = job.CreateJobQueue()
+ for j, err := range fails {
+ if err == gerr.ErrAuthenticationRequired {
+ j.Repository.SetWorkStatus(git.Paused)
+ gui_go.State.FailoverQueue.AddJob(j)
+ }
+ }
+ }(gui)
+ return nil
+}
+
+func (gui *Gui) submitCredentials(g *gocui.Gui, v *gocui.View) error {
+ if is, j := gui.State.FailoverQueue.IsInTheQueue(gui.getSelectedRepository()); is {
+ if j.Repository.WorkStatus() == git.Paused {
+ gui.State.FailoverQueue.RemoveFromQueue(j.Repository)
+ err := gui.openAuthenticationView(g, gui.State.Queue, j, v.Name())
+ if err != nil {
+ log.Warn(err.Error())
+ return err
+ }
+ if isnt, _ := gui.State.Queue.IsInTheQueue(j.Repository); !isnt {
+ gui.State.FailoverQueue.AddJob(j)
+ }
+ }
+ }
+ return nil
+}
+
+// marking repository is simply adding the repostirory into the queue. the
+// function does take its current state into account before adding it
+func (gui *Gui) markRepository(g *gocui.Gui, v *gocui.View) error {
+ r := gui.getSelectedRepository()
+ // maybe, failed entities may be added to queue again
+ if r.WorkStatus().Ready {
+ if err := gui.addToQueue(r); err != nil {
+ return err
+ }
+ } else if r.WorkStatus() == git.Queued {
+ if err := gui.removeFromQueue(r); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// add all remaining repositories into the queue. the function does take its
+// current state into account before adding it
+func (gui *Gui) markAllRepositories(g *gocui.Gui, v *gocui.View) error {
+ for _, r := range gui.State.Repositories {
+ if r.WorkStatus().Ready {
+ if err := gui.addToQueue(r); err != nil {
+ return err
+ }
+ } else {
+ continue
+ }
+ }
+ return nil
+}
+
+// remove all repositories from the queue. the function does take its
+// current state into account before removing it
+func (gui *Gui) unmarkAllRepositories(g *gocui.Gui, v *gocui.View) error {
+ for _, r := range gui.State.Repositories {
+ if r.WorkStatus() == git.Queued {
+ if err := gui.removeFromQueue(r); err != nil {
+ return err
+ }
+ } else {
+ continue
+ }
+ }
+ return nil
+}
+
+// sortByName sorts the repositories by A to Z order
+func (gui *Gui) sortByName(g *gocui.Gui, v *gocui.View) error {
+ sort.Sort(git.Alphabetical(gui.State.Repositories))
+ gui.renderMain()
+ return nil
+}
+
+// sortByMod sorts the repositories according to last modifed date
+// the top element will be the last modified
+func (gui *Gui) sortByMod(g *gocui.Gui, v *gocui.View) error {
+ sort.Sort(git.LastModified(gui.State.Repositories))
+ gui.renderMain()
+ return nil
+}
diff --git a/gui/sideviews.go b/gui/sideviews.go
new file mode 100644
index 0000000..1a910cf
--- /dev/null
+++ b/gui/sideviews.go
@@ -0,0 +1,232 @@
+package gui
+
+import (
+ "fmt"
+
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/jroimartin/gocui"
+)
+
+var (
+ confirmationViewFeature = viewFeature{Name: "confirmation", Title: " Confirmation "}
+ sideViews = []viewFeature{remoteViewFeature, remoteBranchViewFeature, branchViewFeature, commitViewFeature}
+)
+
+// refreshes the side views of the application for given repository.Repository struct
+func (gui *Gui) renderSideViews(r *git.Repository) error {
+ if r == nil {
+ return nil
+ }
+
+ if err := gui.renderRemotes(r); err != nil {
+ return err
+ }
+ if err := gui.renderBranch(r); err != nil {
+ return err
+ }
+ if err := gui.renderRemoteBranches(r); err != nil {
+ return err
+ }
+ if err := gui.renderCommits(r); err != nil {
+ return err
+ }
+ return nil
+}
+
+// updates the remotesview for given entity
+func (gui *Gui) renderRemotes(r *git.Repository) error {
+ var err error
+ out, err := gui.g.View(remoteViewFeature.Name)
+ if err != nil {
+ return err
+ }
+ out.Clear()
+ currentindex := 0
+ totalRemotes := len(r.Remotes)
+ if totalRemotes > 0 {
+ for i, rm := range r.Remotes {
+ _, shortURL := trimRemoteURL(rm.URL[0])
+ if rm.Name == r.State.Remote.Name {
+ currentindex = i
+ fmt.Fprintln(out, selectionIndicator+rm.Name+": "+shortURL)
+ continue
+ }
+ fmt.Fprintln(out, tab+rm.Name+": "+shortURL)
+ }
+ if err = gui.smartAnchorRelativeToLine(out, currentindex, totalRemotes); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// updates the remotebranchview for given entity
+func (gui *Gui) renderRemoteBranches(r *git.Repository) error {
+ var err error
+ out, err := gui.g.View(remoteBranchViewFeature.Name)
+ if err != nil {
+ return err
+ }
+ out.Clear()
+ currentindex := 0
+ trb := len(r.State.Remote.Branches)
+ if trb > 0 {
+ for i, rm := range r.State.Remote.Branches {
+ if rm.Name == r.State.Remote.Branch.Name {
+ currentindex = i
+ fmt.Fprintln(out, selectionIndicator+rm.Name)
+ continue
+ }
+ fmt.Fprintln(out, tab+rm.Name)
+ }
+ if err = gui.smartAnchorRelativeToLine(out, currentindex, trb); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// updates the branchview for given entity
+func (gui *Gui) renderBranch(r *git.Repository) error {
+ var err error
+ out, err := gui.g.View(branchViewFeature.Name)
+ if err != nil {
+ return err
+ }
+ out.Clear()
+ currentindex := 0
+ totalbranches := len(r.Branches)
+ for i, b := range r.Branches {
+ if b.Name == r.State.Branch.Name {
+ currentindex = i
+ fmt.Fprintln(out, selectionIndicator+b.Name)
+ continue
+ }
+ fmt.Fprintln(out, tab+b.Name)
+ }
+
+ return gui.smartAnchorRelativeToLine(out, currentindex, totalbranches)
+}
+
+// updates the commitsview for given entity
+func (gui *Gui) renderCommits(r *git.Repository) error {
+ var err error
+ out, err := gui.g.View(commitViewFeature.Name)
+ if err != nil {
+ return err
+ }
+ out.Clear()
+ currentindex := 0
+ totalcommits := len(r.Commits)
+ for i, c := range r.Commits {
+ if c.Hash == r.State.Commit.Hash {
+ currentindex = i
+ fmt.Fprintln(out, selectionIndicator+commitLabel(c))
+ continue
+ }
+ fmt.Fprintln(out, tab+commitLabel(c))
+ }
+ return gui.smartAnchorRelativeToLine(out, currentindex, totalcommits)
+}
+
+// cursor down variant for sideviews
+func (gui *Gui) sideViewsNextItem(g *gocui.Gui, v *gocui.View) error {
+ var err error
+ r := gui.getSelectedRepository()
+ switch viewName := v.Name(); viewName {
+ case remoteBranchViewFeature.Name:
+ return r.State.Remote.NextRemoteBranch(r)
+ case remoteViewFeature.Name:
+ return r.NextRemote()
+ case branchViewFeature.Name:
+ if err = r.Checkout(r.NextBranch()); err != nil {
+ err = gui.openErrorView(g, err.Error(),
+ "You should manually resolve this issue",
+ branchViewFeature.Name)
+ return err
+ }
+ case commitViewFeature.Name:
+ r.NextCommit()
+ return gui.renderCommits(r)
+ }
+ return err
+}
+
+// cursor up variant for sideviews
+func (gui *Gui) sideViewsPreviousItem(g *gocui.Gui, v *gocui.View) error {
+ var err error
+ r := gui.getSelectedRepository()
+ switch viewName := v.Name(); viewName {
+ case remoteBranchViewFeature.Name:
+ return r.State.Remote.PreviousRemoteBranch(r)
+ case remoteViewFeature.Name:
+ return r.PreviousRemote()
+ case branchViewFeature.Name:
+ if err = r.Checkout(r.PreviousBranch()); err != nil {
+ err = gui.openErrorView(g, err.Error(),
+ "You should manually resolve this issue",
+ branchViewFeature.Name)
+ return err
+ }
+ case commitViewFeature.Name:
+ r.PreviousCommit()
+ return gui.renderCommits(r)
+ }
+ return err
+}
+
+// basically does fetch --prune
+func (gui *Gui) syncRemoteBranch(g *gocui.Gui, v *gocui.View) error {
+ r := gui.getSelectedRepository()
+ return command.Fetch(r, command.FetchOptions{
+ RemoteName: r.State.Remote.Name,
+ Prune: true,
+ })
+}
+
+// opens a confirmation view for setting default merge branch
+func (gui *Gui) setUpstreamToBranch(g *gocui.Gui, v *gocui.View) error {
+ maxX, maxY := g.Size()
+
+ r := gui.getSelectedRepository()
+ v, err := g.SetView(confirmationViewFeature.Name, maxX/2-30, maxY/2-2, maxX/2+30, maxY/2+2)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(v, "branch."+r.State.Branch.Name+"."+"remote"+"="+r.State.Remote.Name)
+ fmt.Fprintln(v, "branch."+r.State.Branch.Name+"."+"merge"+"="+r.State.Branch.Reference.Name().String())
+ }
+ return gui.focusToView(confirmationViewFeature.Name)
+}
+
+// add config for upstream merge
+func (gui *Gui) confirmSetUpstreamToBranch(g *gocui.Gui, v *gocui.View) error {
+ var err error
+ r := gui.getSelectedRepository()
+ if err = command.AddConfig(r, command.ConfigOptions{
+ Section: "branch." + r.State.Branch.Name,
+ Option: "remote",
+ Site: command.ConfigSiteLocal,
+ }, r.State.Remote.Name); err != nil {
+ return err
+ }
+ if err = command.AddConfig(r, command.ConfigOptions{
+ Section: "branch." + r.State.Branch.Name,
+ Option: "merge",
+ Site: command.ConfigSiteLocal,
+ }, r.State.Branch.Reference.Name().String()); err != nil {
+ return err
+ }
+ r.Refresh()
+ return gui.closeConfirmationView(g, v)
+}
+
+// close confirmation view
+func (gui *Gui) closeConfirmationView(g *gocui.Gui, v *gocui.View) error {
+ if err := g.DeleteView(v.Name()); err != nil {
+ return err
+ }
+ return gui.closeViewCleanup(branchViewFeature.Name)
+}
diff --git a/gui/stagedview.go b/gui/stagedview.go
new file mode 100644
index 0000000..95b474a
--- /dev/null
+++ b/gui/stagedview.go
@@ -0,0 +1,73 @@
+package gui
+
+import (
+ "fmt"
+
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/jroimartin/gocui"
+)
+
+// staged view
+func (gui *Gui) openStageView(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+
+ v, err := g.SetView(stageViewFeature.Name, 6, 5, maxX/2-1, int(0.75*float32(maxY))-1)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = stageViewFeature.Title
+ }
+ if err := refreshStagedView(g); err != nil {
+ return err
+ }
+ return gui.focusToView(stageViewFeature.Name)
+}
+
+func (gui *Gui) resetChanges(g *gocui.Gui, v *gocui.View) error {
+ r := gui.getSelectedRepository()
+
+ _, cy := v.Cursor()
+ _, oy := v.Origin()
+ if len(stagedFiles) <= 0 || len(stagedFiles) <= cy+oy {
+ return nil
+ }
+ if err := command.Reset(r, stagedFiles[cy+oy], command.ResetOptions{}); err != nil {
+ return err
+ }
+ return refreshAllStatusView(g, r, true)
+}
+
+func (gui *Gui) resetAllChanges(g *gocui.Gui, v *gocui.View) error {
+ r := gui.getSelectedRepository()
+ ref, err := r.Repo.Head()
+ if err != nil {
+ return err
+ }
+ if err := command.ResetAll(r, command.ResetOptions{
+ Hash: ref.Hash().String(),
+ Rtype: command.ResetMixed,
+ }); err != nil {
+ return err
+ }
+ return refreshAllStatusView(g, r, true)
+}
+
+// refresh the main view and re-render the repository representations
+func refreshStagedView(g *gocui.Gui) error {
+ stageView, err := g.View(stageViewFeature.Name)
+ if err != nil {
+ return err
+ }
+ stageView.Clear()
+ _, cy := stageView.Cursor()
+ _, oy := stageView.Origin()
+ for i, file := range stagedFiles {
+ var prefix string
+ if i == cy+oy {
+ prefix = prefix + selectionIndicator
+ }
+ fmt.Fprintf(stageView, "%s%s%s %s\n", prefix, green.Sprint(string(file.X)), red.Sprint(string(file.Y)), file.Name)
+ }
+ return nil
+}
diff --git a/gui/stashview.go b/gui/stashview.go
new file mode 100644
index 0000000..f1ecc48
--- /dev/null
+++ b/gui/stashview.go
@@ -0,0 +1,84 @@
+package gui
+
+import (
+ "fmt"
+
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/jroimartin/gocui"
+)
+
+// stash view
+func (gui *Gui) openStashView(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+
+ v, err := g.SetView(stashViewFeature.Name, 6, int(0.75*float32(maxY)), maxX-6, maxY-3)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = stashViewFeature.Title
+ }
+ r := gui.getSelectedRepository()
+ err = refreshStashView(g, r)
+ return err
+}
+
+//
+func (gui *Gui) stashChanges(g *gocui.Gui, v *gocui.View) error {
+ r := gui.getSelectedRepository()
+ output, err := r.Stash()
+ if err != nil {
+ if err = gui.openErrorView(g, output,
+ "You should manually resolve this issue",
+ stashViewFeature.Name); err != nil {
+ return err
+ }
+ }
+ err = refreshAllStatusView(g, r, true)
+ return err
+}
+
+//
+func (gui *Gui) popStash(g *gocui.Gui, v *gocui.View) error {
+ r := gui.getSelectedRepository()
+ _, oy := v.Origin()
+ _, cy := v.Cursor()
+ if len(r.Stasheds) <= 0 {
+ return nil
+ }
+ stashedItem := r.Stasheds[oy+cy]
+ output, err := stashedItem.Pop()
+ if err != nil {
+ if err = gui.openErrorView(g, output,
+ "You should manually resolve this issue",
+ stashViewFeature.Name); err != nil {
+ return err
+ }
+ }
+ // since the pop is a func of stashed item, we need to refresh entity here
+ if err := r.Refresh(); err != nil {
+ return err
+ }
+
+ return refreshAllStatusView(g, r, true)
+}
+
+// refresh the main view and re-render the repository representations
+func refreshStashView(g *gocui.Gui, r *git.Repository) error {
+ stashView, err := g.View(stashViewFeature.Name)
+ if err != nil {
+ return err
+ }
+ stashView.Clear()
+ _, cy := stashView.Cursor()
+ _, oy := stashView.Origin()
+ stashedItems := r.Stasheds
+ for i, stashedItem := range stashedItems {
+ var prefix string
+ if i == cy+oy {
+ prefix = prefix + selectionIndicator
+ }
+ fmt.Fprintf(stashView, "%s%d %s: %s (%s)\n", prefix, stashedItem.StashID, cyan.Sprint(stashedItem.BranchName), stashedItem.Description, cyan.Sprint(stashedItem.Hash))
+ }
+ return nil
+}
diff --git a/gui/statusview.go b/gui/statusview.go
new file mode 100644
index 0000000..7b92e6a
--- /dev/null
+++ b/gui/statusview.go
@@ -0,0 +1,162 @@
+package gui
+
+import (
+ "fmt"
+
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/jroimartin/gocui"
+)
+
+var (
+ statusHeaderViewFeature = viewFeature{Name: "status-header", Title: " Status Header "}
+ stageViewFeature = viewFeature{Name: "staged", Title: " Staged "}
+ unstageViewFeature = viewFeature{Name: "unstaged", Title: " Not Staged "}
+ stashViewFeature = viewFeature{Name: "stash", Title: " Stash "}
+
+ statusViews = []viewFeature{stageViewFeature, unstageViewFeature, stashViewFeature}
+
+ commitMesageReturnView string
+ stagedFiles []*command.File
+ unstagedFiles []*command.File
+)
+
+// open the status layout
+func (gui *Gui) openStatusView(g *gocui.Gui, v *gocui.View) error {
+ if err := populateFileLists(gui.getSelectedRepository()); err != nil {
+ return err
+ }
+ gui.openStatusHeaderView(g)
+ gui.openStageView(g)
+ gui.openUnStagedView(g)
+ gui.openStashView(g)
+ return nil
+}
+
+// focus to next view
+func (gui *Gui) nextStatusView(g *gocui.Gui, v *gocui.View) error {
+ return gui.nextViewOfGroup(g, v, statusViews)
+}
+
+// focus to previous view
+func (gui *Gui) previousStatusView(g *gocui.Gui, v *gocui.View) error {
+ return gui.previousViewOfGroup(g, v, statusViews)
+}
+
+// moves the cursor downwards for the main view and if it goes to bottom it
+// prevents from going further
+func (gui *Gui) statusCursorDown(g *gocui.Gui, v *gocui.View) error {
+ if v == nil {
+ return nil
+ }
+
+ cx, cy := v.Cursor()
+ ox, oy := v.Origin()
+ ly := len(v.BufferLines()) - 2 // why magic number? have no idea
+
+ // if we are at the end we just return
+ if cy+oy == ly {
+ return nil
+ }
+ if err := v.SetCursor(cx, cy+1); err != nil {
+
+ if err := v.SetOrigin(ox, oy+1); err != nil {
+ return err
+ }
+ }
+ r := gui.getSelectedRepository()
+ return refreshStatusView(v.Name(), g, r, false)
+}
+
+// moves the cursor upwards for the main view
+func (gui *Gui) statusCursorUp(g *gocui.Gui, v *gocui.View) error {
+ if v == nil {
+ return nil
+ }
+
+ ox, oy := v.Origin()
+ cx, cy := v.Cursor()
+ if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
+ if err := v.SetOrigin(ox, oy-1); err != nil {
+ return err
+ }
+ }
+ r := gui.getSelectedRepository()
+ return refreshStatusView(v.Name(), g, r, false)
+}
+
+// header og the status layout
+func (gui *Gui) openStatusHeaderView(g *gocui.Gui) error {
+ maxX, _ := g.Size()
+ r := gui.getSelectedRepository()
+ v, err := g.SetView(statusHeaderViewFeature.Name, 6, 2, maxX-6, 4)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ fmt.Fprintln(v, r.AbsPath)
+ // v.Frame = false
+ v.Wrap = true
+ }
+ return nil
+}
+
+// close the opened stat views
+func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error {
+ for _, view := range statusViews {
+ if err := g.DeleteView(view.Name); err != nil {
+ return err
+ }
+ }
+ if err := g.DeleteView(statusHeaderViewFeature.Name); err != nil {
+ return err
+ }
+ stagedFiles = make([]*command.File, 0)
+ unstagedFiles = make([]*command.File, 0)
+
+ return gui.closeViewCleanup(mainViewFeature.Name)
+}
+
+// generate file lists by git status command
+func populateFileLists(r *git.Repository) error {
+ files, err := command.Status(r)
+ if err != nil {
+ return err
+ }
+ stagedFiles = make([]*command.File, 0)
+ unstagedFiles = make([]*command.File, 0)
+ for _, file := range files {
+ if file.X != command.StatusNotupdated && file.X != command.StatusUntracked && file.X != command.StatusIgnored && file.X != command.StatusUpdated {
+ stagedFiles = append(stagedFiles, file)
+ }
+ if file.Y != command.StatusNotupdated {
+ unstagedFiles = append(unstagedFiles, file)
+ }
+ }
+ return err
+}
+
+func refreshStatusView(viewName string, g *gocui.Gui, r *git.Repository, reload bool) error {
+ if reload {
+ populateFileLists(r)
+ }
+ var err error
+ switch viewName {
+ case stageViewFeature.Name:
+ err = refreshStagedView(g)
+ case unstageViewFeature.Name:
+ err = refreshUnstagedView(g)
+ case stashViewFeature.Name:
+ err = refreshStashView(g, r)
+ }
+ return err
+}
+
+func refreshAllStatusView(g *gocui.Gui, r *git.Repository, reload bool) error {
+ for _, v := range statusViews {
+ if err := refreshStatusView(v.Name, g, r, reload); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/gui/text-renderer.go b/gui/text-renderer.go
new file mode 100644
index 0000000..d699acf
--- /dev/null
+++ b/gui/text-renderer.go
@@ -0,0 +1,191 @@
+package gui
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/fatih/color"
+ "github.com/isacikgoz/gitbatch/core/git"
+ "github.com/isacikgoz/gitbatch/core/job"
+)
+
+var (
+ black = color.New(color.FgBlack)
+ blue = color.New(color.FgBlue)
+ green = color.New(color.FgGreen)
+ red = color.New(color.FgRed)
+ cyan = color.New(color.FgCyan)
+ yellow = color.New(color.FgYellow)
+ white = color.New(color.FgWhite)
+ magenta = color.New(color.FgMagenta)
+
+ bold = color.New(color.Bold)
+
+ maxBranchLength = 15
+ maxRepositoryLength = 20
+ hashLength = 7
+
+ ws = " "
+ pushable = string(blue.Sprint("↖"))
+ pullable = string(blue.Sprint("↘"))
+ dirty = string(yellow.Sprint("✗"))
+
+ queuedSymbol = "•"
+ workingSymbol = "•"
+ successSymbol = "✔"
+ pauseSymbol = "॥"
+ failSymbol = "✗"
+
+ fetchSymbol = "↓"
+ pullSymbol = "↓↳"
+ mergeSymbol = "↳"
+
+ keySymbol = ws + yellow.Sprint("🔑") + ws
+
+ modeSeperator = ""
+ keyBindingSeperator = "░"
+
+ selectionIndicator = ws + string(green.Sprint("→")) + ws
+ tab = ws
+)
+
+// this function handles the render and representation of the repository
+// TODO: cleanup is required, right now it looks too complicated
+func (gui *Gui) repositoryLabel(r *git.Repository) string {
+
+ var prefix string
+ b := r.State.Branch
+ if b.Pushables != "?" {
+ prefix = prefix + pushable + ws + b.Pushables +
+ ws + pullable + ws + b.Pullables
+ } else {
+ prefix = prefix + pushable + ws + yellow.Sprint(b.Pushables) +
+ ws + pullable + ws + yellow.Sprint(b.Pullables)
+ }
+
+ var repoName string
+ sr := gui.getSelectedRepository()
+ if sr == r {
+ prefix = prefix + selectionIndicator
+ repoName = green.Sprint(r.Name)
+ } else {
+ prefix = prefix + ws
+ repoName = r.Name
+ }
+ // some branch names can be really long, in that times I hope the first
+ // characters are important and meaningful
+ branch := adjustTextLength(b.Name, maxBranchLength)
+ prefix = prefix + string(cyan.Sprint(branch))
+
+ if !b.Clean {
+ prefix = prefix + ws + dirty + ws
+ } else {
+ prefix = prefix + ws
+ }
+
+ var suffix string
+ // rendering the satus according to repository's state
+ if r.WorkStatus() == git.Queued {
+ if inQueue, j := gui.State.Queue.IsInTheQueue(r); inQueue {
+ switch mode := j.JobType; mode {
+ case job.FetchJob:
+ suffix = blue.Sprint(queuedSymbol)
+ case job.PullJob:
+ suffix = magenta.Sprint(queuedSymbol)
+ case job.MergeJob:
+ suffix = cyan.Sprint(queuedSymbol)
+ default:
+ suffix = green.Sprint(queuedSymbol)
+ }
+ }
+ return prefix + repoName + ws + suffix
+ } else if r.WorkStatus() == git.Working {
+ // TODO: maybe the type of the job can be written while its working?
+ return prefix + repoName + ws + green.Sprint(workingSymbol)
+ } else if r.WorkStatus() == git.Success {
+ return prefix + repoName + ws + green.Sprint(successSymbol)
+ } else if r.WorkStatus() == git.Paused {
+ return prefix + repoName + ws + yellow.Sprint("authentication required (u)")
+ } else if r.WorkStatus() == git.Fail {
+ return prefix + repoName + ws + red.Sprint(failSymbol) + ws + red.Sprint(r.State.Message)
+ }
+ return prefix + repoName
+}
+
+func commitLabel(c *git.Commit) string {
+ var body string
+ switch c.CommitType {
+ case git.EvenCommit:
+ body = cyan.Sprint(c.Hash[:hashLength]) + " " + c.Message
+ case git.LocalCommit:
+ body = blue.Sprint(c.Hash[:hashLength]) + " " + c.Message
+ case git.RemoteCommit:
+ if len(c.Hash) > hashLength {
+ body = yellow.Sprint(c.Hash[:hashLength]) + " " + c.Message
+ } else {
+ body = yellow.Sprint(c.Hash[:len(c.Hash)]) + " " + c.Message
+ }
+ default:
+ body = c.Hash[:hashLength] + " " + c.Message
+ }
+ return body
+}
+
+// limit the text length for visual concerns
+func adjustTextLength(text string, maxLength int) string {
+ if len(text) > maxLength {
+ return text[:maxLength-2] + ".."
+ }
+ return text
+}
+
+// colorize the plain diff text collected from system output
+// the style is near to original diff command
+func colorizeDiff(original string) (colorized []string) {
+ colorized = strings.Split(original, "\n")
+ re := regexp.MustCompile(`@@ .+ @@`)
+ for i, line := range colorized {
+ if len(line) > 0 {
+ if line[0] == '-' {
+ colorized[i] = red.Sprint(line)
+ } else if line[0] == '+' {
+ colorized[i] = green.Sprint(line)
+ } else if re.MatchString(line) {
+ s := re.FindString(line)
+ colorized[i] = cyan.Sprint(s) + line[len(s):]
+ } else {
+ continue
+ }
+ } else {
+ continue
+ }
+ }
+ return colorized
+}
+
+// the remote link can be too verbose sometimes, so it is good to trim it
+func trimRemoteURL(url string) (urltype string, shorturl string) {
+ // lets trim the unnecessary .git extension of the url
+ regit := regexp.MustCompile(`.git`)
+ if regit.MatchString(url[len(url)-4:]) {
+ url = url[:len(url)-4]
+ }
+
+ // find out the protocol
+ ressh := regexp.MustCompile(`git@`)
+ rehttp := regexp.MustCompile(`http://`)
+ rehttps := regexp.MustCompile(`https://`)
+
+ // separate the protocol and remote link
+ if ressh.MatchString(url) {
+ shorturl = ressh.Split(url, 5)[1]
+ urltype = "ssh"
+ } else if rehttp.MatchString(url) {
+ shorturl = rehttp.Split(url, 5)[1]
+ urltype = "http"
+ } else if rehttps.MatchString(url) {
+ shorturl = rehttps.Split(url, 5)[1]
+ urltype = "https"
+ }
+ return urltype, shorturl
+}
diff --git a/gui/unstagedview.go b/gui/unstagedview.go
new file mode 100644
index 0000000..f5b6e43
--- /dev/null
+++ b/gui/unstagedview.go
@@ -0,0 +1,66 @@
+package gui
+
+import (
+ "fmt"
+
+ "github.com/isacikgoz/gitbatch/core/command"
+ "github.com/jroimartin/gocui"
+)
+
+// not staged view
+func (gui *Gui) openUnStagedView(g *gocui.Gui) error {
+ maxX, maxY := g.Size()
+
+ v, err := g.SetView(unstageViewFeature.Name, maxX/2+1, 5, maxX-6, int(0.75*float32(maxY))-1)
+ if err != nil {
+ if err != gocui.ErrUnknownView {
+ return err
+ }
+ v.Title = unstageViewFeature.Title
+ }
+
+ return refreshUnstagedView(g)
+}
+
+func (gui *Gui) addChanges(g *gocui.Gui, v *gocui.View) error {
+ r := gui.getSelectedRepository()
+
+ _, cy := v.Cursor()
+ _, oy := v.Origin()
+ if len(unstagedFiles) <= 0 || len(unstagedFiles) < cy+oy {
+ return nil
+ }
+ if err := command.Add(r, unstagedFiles[cy+oy], command.AddOptions{}); err != nil {
+ return err
+ }
+
+ return refreshAllStatusView(g, r, true)
+}
+
+func (gui *Gui) addAllChanges(g *gocui.Gui, v *gocui.View) error {
+ r := gui.getSelectedRepository()
+ if err := command.AddAll(r, command.AddOptions{}); err != nil {
+ return err
+ }
+
+ return refreshAllStatusView(g, r, true)
+}
+
+// refresh the main view and re-render the repository representations
+func refreshUnstagedView(g *gocui.Gui) error {
+ stageView, err := g.View(unstageViewFeature.Name)
+ if err != nil {
+ return err
+ }
+ stageView.Clear()
+ _, cy := stageView.Cursor()
+ _, oy := stageView.Origin()
+ for i, file := range unstagedFiles {
+ var prefix string
+ if i == cy+oy {
+ prefix = prefix + selectionIndicator
+ }
+ fmt.Fprintf(stageView, "%s%s%s %s\n", prefix, red.Sprint(string(file.X)), red.Sprint(string(file.Y)), file.Name)
+ }
+ return nil
+}