summaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2019-03-12 21:43:56 +1100
committerJesse Duffield <jessedduffield@gmail.com>2019-04-07 17:15:01 +1000
commit55538a36959739e07c4f1146571e27093f2b45a0 (patch)
tree811cafcbd534657fea823d16a15152abe410cb0b /pkg
parent878a15aff40639eb69cca19876da74a1ae8bbcc4 (diff)
support custom commands
Diffstat (limited to 'pkg')
-rw-r--r--pkg/commands/os.go5
-rw-r--r--pkg/gui/files_panel.go12
-rw-r--r--pkg/gui/gui.go146
-rw-r--r--pkg/gui/keybindings.go6
-rw-r--r--pkg/i18n/dutch.go6
-rw-r--r--pkg/i18n/english.go6
-rw-r--r--pkg/i18n/polish.go6
7 files changed, 156 insertions, 31 deletions
diff --git a/pkg/commands/os.go b/pkg/commands/os.go
index 1fb8e8da6..02d79a339 100644
--- a/pkg/commands/os.go
+++ b/pkg/commands/os.go
@@ -296,3 +296,8 @@ func (c *OSCommand) GetLazygitPath() string {
}
return filepath.ToSlash(ex)
}
+
+// RunCustomCommand returns the pointer to a custom command
+func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
+ return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command)
+}
diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go
index eb41ab033..5e89632eb 100644
--- a/pkg/gui/files_panel.go
+++ b/pkg/gui/files_panel.go
@@ -599,3 +599,15 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createMenu("", options, len(options), handleMenuPress)
}
+
+func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
+ // gui.subProcessChan <- gui.OSCommand.RunCustomCommand(`read -p "Name: " name; echo $name; read -p "Okay: " okay; echo $okay`)
+
+ // return nil
+
+ return gui.createPromptPanel(g, v, gui.Tr.SLocalize("CustomCommand"), func(g *gocui.Gui, v *gocui.View) error {
+ command := gui.trimmedContent(v)
+ gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
+ return gui.Errors.ErrSubProcess
+ })
+}
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 99250c9ac..14e748857 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -1,6 +1,8 @@
package gui
import (
+ "bytes"
+ "io"
"math"
"sync"
@@ -62,19 +64,20 @@ type Teml i18n.Teml
// Gui wraps the gocui Gui object which handles rendering and events
type Gui struct {
- g *gocui.Gui
- Log *logrus.Entry
- GitCommand *commands.GitCommand
- OSCommand *commands.OSCommand
- SubProcess *exec.Cmd
- State guiState
- Config config.AppConfigurer
- Tr *i18n.Localizer
- Errors SentinelErrors
- Updater *updates.Updater
- statusManager *statusManager
- credentials credentials
- waitForIntro sync.WaitGroup
+ g *gocui.Gui
+ Log *logrus.Entry
+ GitCommand *commands.GitCommand
+ OSCommand *commands.OSCommand
+ SubProcess *exec.Cmd
+ subProcessChan chan (*exec.Cmd)
+ State guiState
+ Config config.AppConfigurer
+ Tr *i18n.Localizer
+ Errors SentinelErrors
+ Updater *updates.Updater
+ statusManager *statusManager
+ credentials credentials
+ waitForIntro sync.WaitGroup
}
// for now the staging panel state, unlike the other panel states, is going to be
@@ -145,6 +148,7 @@ type guiState struct {
WorkingTreeState string // one of "merging", "rebasing", "normal"
Contexts map[string]string
CherryPickedCommits []*commands.Commit
+ SubProcessOutput string
}
// NewGui builds a new gui handler
@@ -457,30 +461,29 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return err
}
- // these are only called once (it's a place to put all the things you want
- // to happen on startup after the screen is first rendered)
- gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
- if err := gui.updateRecentRepoList(); err != nil {
+ // doing this here because it'll only happen once
+ if err := gui.loadNewRepo(); err != nil {
return err
}
- gui.waitForIntro.Done()
+ }
- if _, err := gui.g.SetCurrentView(filesView.Name()); err != nil {
+ if gui.g.CurrentView() == nil {
+ if _, err := gui.g.SetCurrentView(gui.getFilesView().Name()); err != nil {
return err
}
- if err := gui.refreshSidePanels(gui.g); err != nil {
+ if err := gui.switchFocus(gui.g, nil, gui.getFilesView()); err != nil {
return err
}
+ }
- if err := gui.switchFocus(g, nil, filesView); err != nil {
- return err
- }
-
- if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
- if err := gui.promptAnonymousReporting(); err != nil {
- return err
- }
+ if gui.State.SubProcessOutput != "" {
+ output := gui.State.SubProcessOutput
+ gui.State.SubProcessOutput = ""
+ x, y := gui.g.Size()
+ // if we just came back from vim, we don't want vim's output to show up in our popup
+ if float64(len(output))*1.5 < float64(x*y) {
+ return gui.createMessagePanel(gui.g, nil, "Output", output)
}
}
@@ -514,6 +517,27 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return gui.resizeCurrentPopupPanel(g)
}
+func (gui *Gui) loadNewRepo() error {
+ gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
+ if err := gui.updateRecentRepoList(); err != nil {
+ return err
+ }
+ gui.waitForIntro.Done()
+
+ if err := gui.refreshSidePanels(gui.g); err != nil {
+ return err
+ }
+
+ if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
+ if err := gui.promptAnonymousReporting(); err != nil {
+ return err
+ }
+ }
+
+ go gui.listenForSubprocesses()
+ return nil
+}
+
func (gui *Gui) promptAnonymousReporting() error {
return gui.createConfirmationPanel(gui.g, nil, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error {
gui.waitForIntro.Done()
@@ -622,6 +646,24 @@ func (gui *Gui) Run() error {
return err
}
+func (gui *Gui) listenForSubprocesses() {
+ // every time there is a subprocess, we're going to halt the execution of the UI via an update block, and wait for the command to finish
+ gui.subProcessChan = make(chan *exec.Cmd, 0)
+ for {
+ subProcess := <-gui.subProcessChan
+ gui.g.Update(func(*gocui.Gui) error {
+ subProcess.Stdin = os.Stdin
+ output, _ := runCommand(subProcess)
+ gui.State.SubProcessOutput = output
+
+ subProcess.Stdout = ioutil.Discard
+ subProcess.Stderr = ioutil.Discard
+ subProcess.Stdin = nil
+ return nil
+ })
+ }
+}
+
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
// otherwise it handles the error, possibly by quitting the application
@@ -634,9 +676,11 @@ func (gui *Gui) RunWithSubprocesses() error {
continue
} else if err == gui.Errors.ErrSubProcess {
gui.SubProcess.Stdin = os.Stdin
- gui.SubProcess.Stdout = os.Stdout
- gui.SubProcess.Stderr = os.Stderr
- gui.SubProcess.Run()
+ output, err := gui.runCommand(gui.SubProcess)
+ if err != nil {
+ return err
+ }
+ gui.State.SubProcessOutput = output
gui.SubProcess.Stdout = ioutil.Discard
gui.SubProcess.Stderr = ioutil.Discard
gui.SubProcess.Stdin = nil
@@ -649,6 +693,46 @@ func (gui *Gui) RunWithSubprocesses() error {
return nil
}
+// adapted from https://blog.kowalczyk.info/article/wOYk/advanced-command-execution-in-go-with-osexec.html
+func (gui *Gui) runCommand(cmd *exec.Cmd) (string, error) {
+ var stdoutBuf bytes.Buffer
+ stdoutIn, _ := cmd.StdoutPipe()
+ stderrIn, _ := cmd.StderrPipe()
+
+ stdout := io.MultiWriter(os.Stdout, &stdoutBuf)
+ stderr := io.MultiWriter(os.Stderr, &stdoutBuf)
+ err := cmd.Start()
+ if err != nil {
+ return "", err
+ }
+
+ var wg sync.WaitGroup
+ wg.Add(1)
+
+ go func() {
+ if _, err := io.Copy(stdout, stdoutIn); err != nil {
+ gui.Log.Error(err)
+ }
+
+ wg.Done()
+ }()
+
+ if _, err := io.Copy(stderr, stderrIn); err != nil {
+ return "", err
+ }
+
+ wg.Wait()
+
+ if err := cmd.Wait(); err != nil {
+ // not handling the error explicitly because usually we're going to see it
+ // in the output anyway
+ gui.Log.Error(err)
+ }
+
+ outStr := stdoutBuf.String()
+ return outStr, nil
+}
+
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
if gui.State.Updating {
return gui.createUpdateQuitConfirmation(g, v)
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 7397aa8d2..0387b6028 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -239,6 +239,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleGitFetch,
Description: gui.Tr.SLocalize("fetch"),
}, {
+ ViewName: "files",
+ Key: 'X',
+ Modifier: gocui.ModNone,
+ Handler: gui.handleCustomCommand,
+ Description: gui.Tr.SLocalize("executeCustomCommand"),
+ }, {
ViewName: "branches",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go
index 2a5cd5551..a7c6efd23 100644
--- a/pkg/i18n/dutch.go
+++ b/pkg/i18n/dutch.go
@@ -718,6 +718,12 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "SureCreateFixupCommit",
Other: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
+ }, &i18n.Message{
+ ID: "executeCustomCommand",
+ Other: "execute custom command",
+ }, &i18n.Message{
+ ID: "CustomCommand",
+ Other: "Custom Command:",
},
)
}
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 7cebb04d0..b64cc582f 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -741,6 +741,12 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "SureCreateFixupCommit",
Other: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
+ }, &i18n.Message{
+ ID: "executeCustomCommand",
+ Other: "execute custom command",
+ }, &i18n.Message{
+ ID: "CustomCommand",
+ Other: "Custom Command:",
},
)
}
diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go
index 1eb2486dc..fc17323b9 100644
--- a/pkg/i18n/polish.go
+++ b/pkg/i18n/polish.go
@@ -701,6 +701,12 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "SureCreateFixupCommit",
Other: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
+ }, &i18n.Message{
+ ID: "executeCustomCommand",
+ Other: "execute custom command",
+ }, &i18n.Message{
+ ID: "CustomCommand",
+ Other: "Custom Command:",
},
)
}