diff --git a/go.mod b/go.mod index 801681c92..01ac2c369 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/sync v0.16.0 + golang.org/x/sys v0.34.0 gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -77,7 +78,6 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.34.0 // indirect golang.org/x/term v0.33.0 // indirect golang.org/x/text v0.27.0 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index e43ec749c..0fa706bb2 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -390,6 +390,7 @@ type KeybindingConfig struct { type KeybindingUniversalConfig struct { Quit string `yaml:"quit"` QuitAlt1 string `yaml:"quit-alt1"` + SuspendApp string `yaml:"suspendApp"` Return string `yaml:"return"` QuitWithoutChangingDirectory string `yaml:"quitWithoutChangingDirectory"` TogglePanel string `yaml:"togglePanel"` @@ -854,6 +855,7 @@ func GetDefaultConfig() *UserConfig { Universal: KeybindingUniversalConfig{ Quit: "q", QuitAlt1: "", + SuspendApp: "", Return: "", QuitWithoutChangingDirectory: "Q", TogglePanel: "", diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index f83a3f8e9..20b52b2cb 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -109,6 +109,7 @@ func (gui *Gui) resetHelpersAndControllers() { AmendHelper: helpers.NewAmendHelper(helperCommon, gpgHelper), FixupHelper: helpers.NewFixupHelper(helperCommon), Commits: commitsHelper, + SuspendResume: helpers.NewSuspendResumeHelper(helperCommon), Snake: helpers.NewSnakeHelper(helperCommon), Diff: diffHelper, Repos: reposHelper, diff --git a/pkg/gui/controllers/global_controller.go b/pkg/gui/controllers/global_controller.go index fcf281cf4..734e65516 100644 --- a/pkg/gui/controllers/global_controller.go +++ b/pkg/gui/controllers/global_controller.go @@ -123,6 +123,20 @@ func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*type Modifier: gocui.ModNone, Handler: self.quitWithoutChangingDirectory, }, + { + Key: opts.GetKey(opts.Config.Universal.SuspendApp), + Modifier: gocui.ModNone, + Handler: self.c.Helpers().SuspendResume.SuspendApp, + Description: self.c.Tr.SuspendApp, + GetDisabledReason: func() *types.DisabledReason { + if !self.c.Helpers().SuspendResume.CanSuspendApp() { + return &types.DisabledReason{ + Text: self.c.Tr.CannotSuspendApp, + } + } + return nil + }, + }, { Key: opts.GetKey(opts.Config.Universal.ToggleWhitespaceInDiffView), Handler: self.toggleWhitespace, diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index 1f1050dc9..4c9c79f3d 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -35,6 +35,7 @@ type Helpers struct { AmendHelper *AmendHelper FixupHelper *FixupHelper Commits *CommitsHelper + SuspendResume *SuspendResumeHelper Snake *SnakeHelper // lives in context package because our contexts need it to render to main Diff *DiffHelper diff --git a/pkg/gui/controllers/helpers/signal_handling.go b/pkg/gui/controllers/helpers/signal_handling.go new file mode 100644 index 000000000..40d04f689 --- /dev/null +++ b/pkg/gui/controllers/helpers/signal_handling.go @@ -0,0 +1,59 @@ +//go:build !windows + +package helpers + +import ( + "os" + "os/signal" + "syscall" + + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +func canSuspendApp() bool { + return true +} + +func sendStopSignal() error { + return syscall.Kill(0, syscall.SIGSTOP) +} + +// setForegroundPgrp sets the current process group as the foreground process group +// for the terminal, allowing the program to read input after resuming from suspension. +func setForegroundPgrp() error { + fd, err := unix.Open("/dev/tty", unix.O_RDWR, 0) + if err != nil { + return err + } + defer unix.Close(fd) + + pgid := syscall.Getpgrp() + + return unix.IoctlSetPointerInt(fd, unix.TIOCSPGRP, pgid) +} + +func handleResumeSignal(log *logrus.Entry, onResume func() error) { + if err := setForegroundPgrp(); err != nil { + log.Warning(err) + return + } + + if err := onResume(); err != nil { + log.Warning(err) + } +} + +func installResumeSignalHandler(log *logrus.Entry, onResume func() error) { + go func() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGCONT) + + for sig := range sigs { + switch sig { + case syscall.SIGCONT: + handleResumeSignal(log, onResume) + } + } + }() +} diff --git a/pkg/gui/controllers/helpers/signal_handling_windows.go b/pkg/gui/controllers/helpers/signal_handling_windows.go new file mode 100644 index 000000000..8517e775d --- /dev/null +++ b/pkg/gui/controllers/helpers/signal_handling_windows.go @@ -0,0 +1,16 @@ +package helpers + +import ( + "github.com/sirupsen/logrus" +) + +func canSuspendApp() bool { + return false +} + +func sendStopSignal() error { + return nil +} + +func installResumeSignalHandler(log *logrus.Entry, onResume func() error) { +} diff --git a/pkg/gui/controllers/helpers/suspend_resume_helper.go b/pkg/gui/controllers/helpers/suspend_resume_helper.go new file mode 100644 index 000000000..cc63af495 --- /dev/null +++ b/pkg/gui/controllers/helpers/suspend_resume_helper.go @@ -0,0 +1,31 @@ +package helpers + +type SuspendResumeHelper struct { + c *HelperCommon +} + +func NewSuspendResumeHelper(c *HelperCommon) *SuspendResumeHelper { + return &SuspendResumeHelper{ + c: c, + } +} + +func (s *SuspendResumeHelper) CanSuspendApp() bool { + return canSuspendApp() +} + +func (s *SuspendResumeHelper) SuspendApp() error { + if !canSuspendApp() { + return nil + } + + if err := s.c.Suspend(); err != nil { + return err + } + + return sendStopSignal() +} + +func (s *SuspendResumeHelper) InstallResumeSignalHandler() { + installResumeSignalHandler(s.c.Log, s.c.Resume) +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index bfa0a392c..1bd5788f2 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -848,6 +848,8 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error { gui.BackgroundRoutineMgr.startBackgroundRoutines() + gui.Helpers().SuspendResume.InstallResumeSignalHandler() + gui.c.Log.Info("starting main loop") // setting here so we can use it in layout.go diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index c77c81e79..d946659d1 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -42,6 +42,14 @@ func (self *guiCommon) RunSubprocess(cmdObj *oscommands.CmdObj) (bool, error) { return self.gui.runSubprocessWithSuspense(cmdObj) } +func (self *guiCommon) Suspend() error { + return self.gui.suspend() +} + +func (self *guiCommon) Resume() error { + return self.gui.resume() +} + func (self *guiCommon) Context() types.IContextMgr { return self.gui.State.ContextMgr } diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index be8c10a15..bea52fbae 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -56,6 +56,9 @@ type IGuiCommon interface { RunSubprocess(cmdObj *oscommands.CmdObj) (bool, error) RunSubprocessAndRefresh(*oscommands.CmdObj) error + Suspend() error + Resume() error + Context() IContextMgr ContextForKey(key ContextKey) Context diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index faedeb6e5..28d7ed449 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -382,6 +382,8 @@ type TranslationSet struct { ScrollUp string ScrollUpMainWindow string ScrollDownMainWindow string + SuspendApp string + CannotSuspendApp string AmendCommitTitle string AmendCommitPrompt string AmendCommitWithConflictsMenuPrompt string @@ -1456,6 +1458,8 @@ func EnglishTranslationSet() *TranslationSet { ScrollUp: "Scroll up", ScrollUpMainWindow: "Scroll up main window", ScrollDownMainWindow: "Scroll down main window", + SuspendApp: "Suspend the application", + CannotSuspendApp: "Suspending the application is not supported on Windows", AmendCommitTitle: "Amend commit", AmendCommitPrompt: "Are you sure you want to amend this commit with your staged files?", AmendCommitWithConflictsMenuPrompt: "WARNING: you are about to amend the last finished commit with your resolved conflicts. This is very unlikely to be what you want at this point. More likely, you simply want to continue the rebase instead.\n\nDo you still want to amend the previous commit?",