Skip to content

Commit

Permalink
Start pseudo terminal with explicit window size
Browse files Browse the repository at this point in the history
Ref: #254

Make sure to start pseudo terminal with fixed size in case the columns
flag was used.

Refactor `ptexec` package to allow for more flexibility.

Introduce test cases for `ptexec` package.
  • Loading branch information
HeavyWombat committed Jan 2, 2025
1 parent 6637d2b commit 7cc1398
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 15 deletions.
6 changes: 4 additions & 2 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@ window including all terminal colors and text decorations.

var scaffold = img.NewImageCreator()
var buf bytes.Buffer
var pt = ptexec.New()

// Initialise scaffold with a column sizing so that the
// content can be wrapped accordingly
//
if columns, err := cmd.Flags().GetInt("columns"); err == nil && columns > 0 {
scaffold.SetColumns(columns)
pt.Cols(uint16(columns))
}

// Disable window shadow if requested
Expand Down Expand Up @@ -110,7 +112,7 @@ window including all terminal colors and text decorations.
// Run the provided command in a pseudo terminal and capture
// the output to be later rendered into the screenshot
//
bytes, err := ptexec.RunCommandInPseudoTerminal(args[0], args[1:]...)
bytes, err := pt.Command(args[0], args[1:]...).Run()
if err != nil {
return err
}
Expand All @@ -135,7 +137,7 @@ window including all terminal colors and text decorations.
editor = "vi"
}

if _, err := ptexec.RunCommandInPseudoTerminal(editor, tmpFile.Name()); err != nil {
if _, err := ptexec.New().Command(editor, tmpFile.Name()).Run(); err != nil {
return err
}

Expand Down
103 changes: 90 additions & 13 deletions internal/ptexec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,64 @@ import (
"golang.org/x/term"
)

// RunCommandInPseudoTerminal runs the provided program with the given
// arguments in a pseudo terminal (PTY) so that the behavior is the same
// if it would be executed in a terminal
func RunCommandInPseudoTerminal(name string, args ...string) ([]byte, error) {
var errors = []error{}
type PseudoTerminal struct {
name string
args []string

shell string

cols uint16
rows uint16
resize bool

stdout io.Writer
}

func New() *PseudoTerminal {
return &PseudoTerminal{
shell: "/bin/sh",
resize: true,
stdout: os.Stdout,
}
}

func (c *PseudoTerminal) Cols(cols uint16) *PseudoTerminal {
c.cols = cols
return c
}

func (c *PseudoTerminal) Rows(rows uint16) *PseudoTerminal {
c.rows = rows
return c
}

func (c *PseudoTerminal) Stdout(stdout io.Writer) *PseudoTerminal {
c.stdout = stdout
return c
}

func (c *PseudoTerminal) Command(name string, args ...string) *PseudoTerminal {
c.name = name
c.args = args
return c
}

func (c *PseudoTerminal) Run() ([]byte, error) {
if c.name == "" {
return nil, fmt.Errorf("no command specified")
}

// Convenience hack in case command contains a space, for example in case
// typical construct like "foo | grep" are used.
if strings.Contains(name, " ") {
args = []string{
if strings.Contains(c.name, " ") {
c.args = []string{
"-c",
strings.Join(append(
[]string{name},
args...,
[]string{c.name},
c.args...,
), " "),
}
name = "/bin/sh"
c.name = c.shell
}

// Set RAW mode for Stdin
Expand All @@ -65,13 +106,17 @@ func RunCommandInPseudoTerminal(name string, args ...string) ([]byte, error) {
defer func() { _ = term.Restore(int(os.Stdin.Fd()), oldState) }()
}

pt, err := pty.Start(exec.Command(name, args...))
// collect all errors along the way
var errors = []error{}

// #nosec G204 -- since this is exactly what we want, arbitrary commands
pt, err := c.pseudoTerminal(exec.Command(c.name, c.args...))
if err != nil {
return nil, err
}

// Support terminal resizing
if isTerminal(os.Stdin) {
if c.resize && isTerminal(os.Stdin) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGWINCH)
go func() {
Expand All @@ -98,7 +143,7 @@ func RunCommandInPseudoTerminal(name string, args ...string) ([]byte, error) {
}()

var buf bytes.Buffer
if err = copy(io.MultiWriter(os.Stdout, &buf), pt); err != nil {
if err = copy(io.MultiWriter(c.stdout, &buf), pt); err != nil {
return nil, err
}

Expand All @@ -112,6 +157,38 @@ func RunCommandInPseudoTerminal(name string, args ...string) ([]byte, error) {
return buf.Bytes(), nil
}

func (c *PseudoTerminal) pseudoTerminal(cmd *exec.Cmd) (*os.File, error) {
if c.cols == 0 && c.rows == 0 {
return pty.Start(cmd)
}

size, err := pty.GetsizeFull(os.Stdout)
if err != nil {
return nil, err
}

if c.rows != 0 {
size.Rows = c.rows
}

if c.cols != 0 {
size.Cols = c.cols
}

c.resize = false

return pty.StartWithSize(cmd, size)
}

// RunCommandInPseudoTerminal runs the provided program with the given
// arguments in a pseudo terminal (PTY) so that the behavior is the same
// if it would be executed in a terminal
//
// Deprecated: Use New().Command().Run() instead.
func RunCommandInPseudoTerminal(name string, args ...string) ([]byte, error) {
return New().Command(name, args...).Run()
}

func copy(dst io.Writer, src io.Reader) error {
_, err := io.Copy(dst, src)
if err != nil {
Expand Down
60 changes: 60 additions & 0 deletions internal/ptexec/exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright © 2024 The Homeport Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package ptexec_test

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

. "github.com/homeport/termshot/internal/ptexec"
)

var _ = Describe("Pseudo Terminal Execute Suite", func() {
Context("running commands in pseudo terminal", func() {
It("should run a command just fine", func() {
out, err := New().Stdout(GinkgoWriter).
Command("echo", "hello").
Run()

Expect(err).ToNot(HaveOccurred())
Expect(string(out)).To(Equal("hello\r\n"))
})

It("should run a script just fine", func() {
out, err := New().Stdout(GinkgoWriter).
Command("echo hello").
Run()

Expect(err).ToNot(HaveOccurred())
Expect(string(out)).To(Equal("hello\r\n"))
})

It("should run with fixed terminal size", func() {
out, err := New().Stdout(GinkgoWriter).Cols(80).Command("tput", "cols").Run()
Expect(err).ToNot(HaveOccurred())
Expect(string(out)).To(Equal("80\r\n"))

out, err = New().Stdout(GinkgoWriter).Rows(25).Command("tput", "lines").Run()
Expect(err).ToNot(HaveOccurred())
Expect(string(out)).To(Equal("25\r\n"))
})
})
})
33 changes: 33 additions & 0 deletions internal/ptexec/ptexec_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright © 2024 The Homeport Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package ptexec_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestPtexec(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Pseudo Terminal Exec Suite")
}

0 comments on commit 7cc1398

Please sign in to comment.