diff --git a/cmd/oras/internal/display/status/console/console.go b/cmd/oras/internal/display/status/console/console.go index 7c29492b5..29cd27fc7 100644 --- a/cmd/oras/internal/display/status/console/console.go +++ b/cmd/oras/internal/display/status/console/console.go @@ -16,11 +16,9 @@ limitations under the License. package console import ( - "os" - - "github.com/containerd/console" + containerd "github.com/containerd/console" "github.com/morikuni/aec" - "oras.land/oras/internal/testutils" + "os" ) const ( @@ -34,55 +32,70 @@ const ( Restore = "\0338" ) -// Console is a wrapper around containerd's console.Console and ANSI escape -// codes. -type Console struct { - console.Console +// Console is a wrapper around containerd's Console and ANSI escape codes. +type Console interface { + containerd.Console + GetHeightWidth() (height, width int) + Save() + NewRow() + OutputTo(upCnt uint, str string) + Restore() +} + +type console struct { + containerd.Console +} + +// NewConsole generates a console from a file. +func NewConsole(f *os.File) (Console, error) { + if f != nil && f.Name() == os.DevNull { + return NewDiscardConsole(f), nil + } + c, err := containerd.ConsoleFromFile(f) + if err != nil { + return nil, err + } + return &console{c}, nil } // Size returns the width and height of the console. // If the console size cannot be determined, returns a default value of 80x10. -func (c *Console) Size() (width, height int) { - width = MinWidth - height = MinHeight - size, err := c.Console.Size() - if err == nil { - if size.Height > MinHeight { - height = int(size.Height) +func (c *console) Size() (size containerd.WinSize, err error) { + size, err = c.Console.Size() + if err != nil { + size.Height = MinHeight + size.Width = MinWidth + } else { + if size.Height < MinHeight { + size.Height = MinHeight } - if size.Width > MinWidth { - width = int(size.Width) + if size.Width < MinWidth { + size.Width = MinWidth } } return } -// New generates a Console from a file. -func New(f *os.File) (*Console, error) { - if f != nil && f.Name() == os.DevNull { - return &Console{testutils.NewMockConsole(f)}, nil - } - c, err := console.ConsoleFromFile(f) - if err != nil { - return nil, err - } - return &Console{c}, nil +// GetHeightWidth returns the width and height of the console. +func (c *console) GetHeightWidth() (height, width int) { + windowSize, _ := c.Size() + return int(windowSize.Height), int(windowSize.Width) } // Save saves the current cursor position. -func (c *Console) Save() { +func (c *console) Save() { _, _ = c.Write([]byte(aec.Hide.Apply(Save))) } // NewRow allocates a horizontal space to the output area with scroll if needed. -func (c *Console) NewRow() { +func (c *console) NewRow() { _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte("\n")) _, _ = c.Write([]byte(Save)) } // OutputTo outputs a string to a specific line. -func (c *Console) OutputTo(upCnt uint, str string) { +func (c *console) OutputTo(upCnt uint, str string) { _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte(aec.PreviousLine(upCnt).Apply(str))) _, _ = c.Write([]byte("\n")) @@ -90,7 +103,7 @@ func (c *Console) OutputTo(upCnt uint, str string) { } // Restore restores the saved cursor position. -func (c *Console) Restore() { +func (c *console) Restore() { // cannot use aec.Restore since DEC has better compatibility than SCO _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte(aec.Column(0). diff --git a/cmd/oras/internal/display/status/console/console_test.go b/cmd/oras/internal/display/status/console/console_test.go index 00e1f694c..ded00e4fb 100644 --- a/cmd/oras/internal/display/status/console/console_test.go +++ b/cmd/oras/internal/display/status/console/console_test.go @@ -20,7 +20,7 @@ package console import ( "testing" - "github.com/containerd/console" + containerd "github.com/containerd/console" ) func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int) { @@ -34,30 +34,30 @@ func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int) } func TestConsole_Size(t *testing.T) { - pty, _, err := console.NewPty() + pty, _, err := containerd.NewPty() if err != nil { t.Fatal(err) } - c := &Console{ + c := &console{ Console: pty, } // minimal width and height - gotWidth, gotHeight := c.Size() + gotHeight, gotWidth := c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // zero width - _ = pty.Resize(console.WinSize{Width: 0, Height: MinHeight}) - gotWidth, gotHeight = c.Size() + _ = pty.Resize(containerd.WinSize{Width: 0, Height: MinHeight}) + gotHeight, gotWidth = c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // zero height - _ = pty.Resize(console.WinSize{Width: MinWidth, Height: 0}) - gotWidth, gotHeight = c.Size() + _ = pty.Resize(containerd.WinSize{Width: MinWidth, Height: 0}) + gotHeight, gotWidth = c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // valid zero and height - _ = pty.Resize(console.WinSize{Width: 200, Height: 100}) - gotWidth, gotHeight = c.Size() + _ = pty.Resize(containerd.WinSize{Width: 200, Height: 100}) + gotHeight, gotWidth = c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, 200, 100) } diff --git a/cmd/oras/internal/display/status/console/discard.go b/cmd/oras/internal/display/status/console/discard.go new file mode 100644 index 000000000..52b335022 --- /dev/null +++ b/cmd/oras/internal/display/status/console/discard.go @@ -0,0 +1,85 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package console + +import ( + "os" + + containerd "github.com/containerd/console" +) + +type discardConsole struct { + *os.File +} + +func NewDiscardConsole(f *os.File) Console { + dc := discardConsole{ + File: f, + } + return &dc +} + +// Fd returns its file descriptor +func (mc *discardConsole) Fd() uintptr { + return os.Stderr.Fd() +} + +// Name returns its file name +func (mc *discardConsole) Name() string { + return mc.File.Name() +} + +func (mc *discardConsole) Resize(_ containerd.WinSize) error { + return nil +} + +func (mc *discardConsole) ResizeFrom(containerd.Console) error { + return nil +} +func (mc *discardConsole) SetRaw() error { + return nil +} +func (mc *discardConsole) DisableEcho() error { + return nil +} +func (mc *discardConsole) Reset() error { + return nil +} +func (mc *discardConsole) Size() (containerd.WinSize, error) { + ws := containerd.WinSize{ + Width: 80, + Height: 24, + } + return ws, nil +} + +// GetHeightWidth returns the width and height of the console. +func (mc *discardConsole) GetHeightWidth() (height, width int) { + windowSize, _ := mc.Size() + return int(windowSize.Height), int(windowSize.Width) +} + +func (mc *discardConsole) Save() { +} + +func (mc *discardConsole) NewRow() { +} + +func (mc *discardConsole) OutputTo(_ uint, _ string) { +} + +func (mc *discardConsole) Restore() { +} diff --git a/cmd/oras/internal/display/status/console/discard_test.go b/cmd/oras/internal/display/status/console/discard_test.go new file mode 100644 index 000000000..f8420e381 --- /dev/null +++ b/cmd/oras/internal/display/status/console/discard_test.go @@ -0,0 +1,65 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package console + +import ( + "os" + "testing" + + containerd "github.com/containerd/console" +) + +func TestConsole_New(t *testing.T) { + mockFile, err := os.OpenFile(os.DevNull, os.O_RDWR, 0666) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + sut, err := NewConsole(mockFile) + if err != nil { + t.Errorf("Unexpected error %v", err) + } + + if err = sut.Resize(containerd.WinSize{}); err != nil { + t.Errorf("Unexpected erro for Resize: %v", err) + } + if err = sut.ResizeFrom(nil); err != nil { + t.Errorf("Unexpected erro for Resize: %v", err) + } + if err = sut.SetRaw(); err != nil { + t.Errorf("Unexpected erro for Resize: %v", err) + } + if err = sut.DisableEcho(); err != nil { + t.Errorf("Unexpected erro for Resize: %v", err) + } + if err = sut.Reset(); err != nil { + t.Errorf("Unexpected erro for Resize: %v", err) + } + windowSize, _ := sut.Size() + if windowSize.Height != 24 { + t.Errorf("Expected size 24 actual %d", windowSize.Height) + } + if windowSize.Width != 80 { + t.Errorf("Expected size 80 actual %d", windowSize.Width) + } + h, w := sut.GetHeightWidth() + if h != 24 { + t.Errorf("Expected size 24 actual %d", h) + } + if w != 80 { + t.Errorf("Expected size 80 actual %d", w) + } +} diff --git a/cmd/oras/internal/display/status/progress/manager.go b/cmd/oras/internal/display/status/progress/manager.go index d0a3186dd..a3c182650 100644 --- a/cmd/oras/internal/display/status/progress/manager.go +++ b/cmd/oras/internal/display/status/progress/manager.go @@ -44,7 +44,7 @@ type Manager interface { type manager struct { status []*status statusLock sync.RWMutex - console *console.Console + console console.Console actionPrompt string donePrompt string updating sync.WaitGroup @@ -54,7 +54,7 @@ type manager struct { // NewManager initialized a new progress manager. func NewManager(actionPrompt string, donePrompt string, tty *os.File) (Manager, error) { - c, err := console.New(tty) + c, err := console.NewConsole(tty) if err != nil { return nil, err } @@ -92,7 +92,7 @@ func (m *manager) render() { m.statusLock.RLock() defer m.statusLock.RUnlock() // todo: update size in another routine - width, height := m.console.Size() + height, width := m.console.GetHeightWidth() lineCount := len(m.status) * 2 offset := 0 if lineCount > height { diff --git a/cmd/oras/internal/display/status/progress/manager_test.go b/cmd/oras/internal/display/status/progress/manager_test.go index 516e4aaf2..9a76aa2d4 100644 --- a/cmd/oras/internal/display/status/progress/manager_test.go +++ b/cmd/oras/internal/display/status/progress/manager_test.go @@ -32,10 +32,15 @@ func Test_manager_render(t *testing.T) { t.Fatal(err) } defer device.Close() + sole, err := console.NewConsole(device) + if err != nil { + t.Fatal(err) + } + m := &manager{ - console: &console.Console{Console: pty}, + console: sole, } - _, height := m.console.Size() + height, _ := m.console.GetHeightWidth() for i := 0; i < height; i++ { if _, err := m.Add(); err != nil { t.Fatal(err) diff --git a/internal/testutils/mock_console.go b/internal/testutils/mock_console.go deleted file mode 100644 index 3c5e108b4..000000000 --- a/internal/testutils/mock_console.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package testutils - -import ( - "github.com/containerd/console" - "os" -) - -type MockConsole struct { - *os.File -} - -func NewMockConsole(f *os.File) MockConsole { - return MockConsole{ - File: f, - } -} - -// Fd returns its file descriptor -func (mc MockConsole) Fd() uintptr { - return os.Stderr.Fd() -} - -// Name returns its file name -func (mc MockConsole) Name() string { - return mc.File.Name() -} - -func (mc MockConsole) Resize(_ console.WinSize) error { - return nil -} - -func (mc MockConsole) ResizeFrom(console.Console) error { - return nil -} -func (mc MockConsole) SetRaw() error { - return nil -} -func (mc MockConsole) DisableEcho() error { - return nil -} -func (mc MockConsole) Reset() error { - return nil -} -func (mc MockConsole) Size() (console.WinSize, error) { - ws := console.WinSize{ - Width: 80, - Height: 24, - } - return ws, nil -}