diff --git a/pkg/commands/oscommands/pty.go b/pkg/commands/oscommands/pty.go new file mode 100644 index 000000000..8a803fe60 --- /dev/null +++ b/pkg/commands/oscommands/pty.go @@ -0,0 +1,39 @@ +package oscommands + +import ( + "errors" + "io" + "os" +) + +// Pty is the master side of a pseudo-terminal running a subprocess. The +// concrete implementation is platform-specific: creack/pty on Unix and +// ConPTY on Windows. +type Pty interface { + io.ReadWriteCloser + Resize(cols, rows uint16) error +} + +// StartedPty is the result of StartPty. +type StartedPty struct { + // Pty is the master side of the pseudo-terminal; read from it to get + // the child's combined stdout/stderr and write to it to feed stdin. + Pty Pty + // Process is the spawned child. Useful for signalling; on Windows the + // original *exec.Cmd was not Start()ed (ConPTY spawns via + // CreateProcess, not os/exec) so cmd.Process is nil and this is the + // only handle. + Process *os.Process + // Wait blocks until the child exits and returns a non-nil error on a + // nonzero exit status, matching *exec.Cmd.Wait semantics. + Wait func() error +} + +// ErrPtyUnsupported is returned by StartPty on platforms without a pty +// implementation. Callers may fall back to running the command without a pty. +var ErrPtyUnsupported = errors.New("pty not supported on this platform") + +// StartPty runs cmd in a pseudo-terminal with the given initial dimensions. +// Implemented per-platform in pty_unix.go / pty_windows.go. +// +// func StartPty(cmd *exec.Cmd, cols, rows uint16) (StartedPty, error) diff --git a/pkg/commands/oscommands/pty_unix.go b/pkg/commands/oscommands/pty_unix.go new file mode 100644 index 000000000..6cf63cdff --- /dev/null +++ b/pkg/commands/oscommands/pty_unix.go @@ -0,0 +1,34 @@ +//go:build !windows + +package oscommands + +import ( + "os" + "os/exec" + + creackpty "github.com/creack/pty" +) + +type unixPty struct { + master *os.File +} + +func (u *unixPty) Read(p []byte) (int, error) { return u.master.Read(p) } +func (u *unixPty) Write(p []byte) (int, error) { return u.master.Write(p) } +func (u *unixPty) Close() error { return u.master.Close() } + +func (u *unixPty) Resize(cols, rows uint16) error { + return creackpty.Setsize(u.master, &creackpty.Winsize{Cols: cols, Rows: rows}) +} + +func StartPty(cmd *exec.Cmd, cols, rows uint16) (StartedPty, error) { + f, err := creackpty.StartWithSize(cmd, &creackpty.Winsize{Cols: cols, Rows: rows}) + if err != nil { + return StartedPty{}, err + } + return StartedPty{ + Pty: &unixPty{master: f}, + Process: cmd.Process, + Wait: cmd.Wait, + }, nil +} diff --git a/pkg/commands/oscommands/pty_windows.go b/pkg/commands/oscommands/pty_windows.go new file mode 100644 index 000000000..6d0d7c6ff --- /dev/null +++ b/pkg/commands/oscommands/pty_windows.go @@ -0,0 +1,12 @@ +package oscommands + +import ( + "os/exec" +) + +// StartPty is a stub on Windows for now; callers fall back to the non-pty +// path when ErrPtyUnsupported is returned. A real ConPTY implementation +// replaces this in a follow-up commit. +func StartPty(cmd *exec.Cmd, cols, rows uint16) (StartedPty, error) { + return StartedPty{}, ErrPtyUnsupported +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 08d560ce5..e23afd124 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -85,7 +85,7 @@ type Gui struct { // holds a mapping of view names to ptmx's. This is for rendering command outputs // from within a pty. The point of keeping track of them is so that if we re-size // the window, we can tell the pty it needs to resize accordingly. - viewPtmxMap map[string]*os.File + viewPtmxMap map[string]oscommands.Pty stopChan chan struct{} // when lazygit is opened outside a git directory we want to open to the most @@ -761,7 +761,7 @@ func NewGui( Updater: updater, statusManager: status.NewStatusManager(), viewBufferManagerMap: map[string]*tasks.ViewBufferManager{}, - viewPtmxMap: map[string]*os.File{}, + viewPtmxMap: map[string]oscommands.Pty{}, showRecentRepos: showRecentRepos, RepoPathStack: &utils.StringStack{}, RepoStateMap: map[Repo]*GuiRepoState{}, diff --git a/pkg/gui/pty.go b/pkg/gui/pty.go index 0455c0abf..e5ef2a770 100644 --- a/pkg/gui/pty.go +++ b/pkg/gui/pty.go @@ -1,37 +1,36 @@ -//go:build !windows - package gui import ( + "errors" "fmt" "io" "os" "os/exec" "strings" - "github.com/creack/pty" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/tasks" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) -func (gui *Gui) desiredPtySize(view *gocui.View) *pty.Winsize { +func (gui *Gui) desiredPtySize(view *gocui.View) (cols, rows uint16) { width, height := view.InnerSize() - - return &pty.Winsize{Cols: uint16(width), Rows: uint16(height)} + return uint16(width), uint16(height) } func (gui *Gui) onResize() error { gui.Mutexes.PtyMutex.Lock() defer gui.Mutexes.PtyMutex.Unlock() - for viewName, ptmx := range gui.viewPtmxMap { + for viewName, p := range gui.viewPtmxMap { // TODO: handle resizing properly: we need to actually clear the main view // and re-read the output from our pty. Or we could just re-run the original // command from scratch view, _ := gui.g.View(viewName) - if err := pty.Setsize(ptmx, gui.desiredPtySize(view)); err != nil { + cols, rows := gui.desiredPtySize(view) + if err := p.Resize(cols, rows); err != nil { return utils.WrapError(err) } } @@ -39,6 +38,19 @@ func (gui *Gui) onResize() error { return nil } +// ptyCmd adapts an oscommands.StartedPty result into the tasks.Cmd shape. +// On Windows the original *exec.Cmd was never Start()ed, so we go through +// the explicit Process handle rather than cmd.Process. +type ptyCmd struct { + cmd *exec.Cmd + process *os.Process + wait func() error +} + +func (p ptyCmd) Wait() error { return p.wait() } +func (p ptyCmd) String() string { return p.cmd.String() } +func (p ptyCmd) GetProcess() *os.Process { return p.process } + // Some commands need to output for a terminal to active certain behaviour. // For example, git won't invoke the GIT_PAGER env var unless it thinks it's // talking to a terminal. We typically write cmd outputs straight to a view, @@ -82,24 +94,30 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error manager := gui.getManager(view) - var ptmx *os.File + var p oscommands.Pty start := func() (tasks.Cmd, io.Reader) { - var err error - ptmx, err = pty.StartWithSize(cmd, gui.desiredPtySize(view)) + cols, rows := gui.desiredPtySize(view) + sp, err := oscommands.StartPty(cmd, cols, rows) if err != nil { - gui.c.Log.Error(err) + if !errors.Is(err, oscommands.ErrPtyUnsupported) { + gui.c.Log.Error(err) + } + return tasks.ExecCmd{Cmd: cmd}, nil } + p = sp.Pty gui.Mutexes.PtyMutex.Lock() - gui.viewPtmxMap[view.Name()] = ptmx + gui.viewPtmxMap[view.Name()] = p gui.Mutexes.PtyMutex.Unlock() - return tasks.ExecCmd{Cmd: cmd}, ptmx + return ptyCmd{cmd: cmd, process: sp.Process, wait: sp.Wait}, p } onClose := func() { gui.Mutexes.PtyMutex.Lock() - ptmx.Close() + if p != nil { + p.Close() + } delete(gui.viewPtmxMap, view.Name()) gui.Mutexes.PtyMutex.Unlock() } diff --git a/pkg/gui/pty_windows.go b/pkg/gui/pty_windows.go deleted file mode 100644 index 31d763870..000000000 --- a/pkg/gui/pty_windows.go +++ /dev/null @@ -1,17 +0,0 @@ -package gui - -import ( - "fmt" - "os/exec" - - "github.com/jesseduffield/lazygit/pkg/gocui" -) - -func (gui *Gui) onResize() error { - return nil -} - -func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error { - cmd.Env = append(cmd.Env, fmt.Sprintf("LAZYGIT_COLUMNS=%d", view.InnerWidth())) - return gui.newCmdTask(view, cmd, prefix) -}