mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Abstract pty startup behind a platform-specific primitive
Move the pty master behind a small interface (Read/Write/Close/Resize), and push the actual startup into a platform-specific StartPty function in pkg/commands/oscommands. The Unix implementation still uses creack/pty; the Windows implementation is a stub that returns ErrPtyUnsupported, at which point newPtyTask falls back to a plain cmd task — matching the existing Windows behavior. The primitive lives in oscommands rather than pkg/gui because the cmd_obj_runner pty handler (also in oscommands) is going to consume it too, and tasks → oscommands is the existing dependency direction. Same observable behavior on every platform; this just carves out a seam for a real ConPTY implementation on Windows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8ec4283e55
commit
c85f7530bb
39
pkg/commands/oscommands/pty.go
Normal file
39
pkg/commands/oscommands/pty.go
Normal file
|
|
@ -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)
|
||||
34
pkg/commands/oscommands/pty_unix.go
Normal file
34
pkg/commands/oscommands/pty_unix.go
Normal file
|
|
@ -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
|
||||
}
|
||||
12
pkg/commands/oscommands/pty_windows.go
Normal file
12
pkg/commands/oscommands/pty_windows.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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{},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in a new issue