github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/term-pager.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"fmt"
    22  	"math"
    23  	"os"
    24  	"strings"
    25  
    26  	"github.com/charmbracelet/bubbles/viewport"
    27  	tea "github.com/charmbracelet/bubbletea"
    28  	"github.com/charmbracelet/lipgloss"
    29  	"github.com/muesli/reflow/wordwrap"
    30  )
    31  
    32  var percentStyle = lipgloss.NewStyle().Width(4).Align(lipgloss.Left)
    33  
    34  type model struct {
    35  	viewport viewport.Model
    36  	content  string
    37  
    38  	ready        bool
    39  	renderedOnce chan struct{}
    40  }
    41  
    42  func (m model) Init() tea.Cmd {
    43  	return nil
    44  }
    45  
    46  func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    47  	var (
    48  		cmd  tea.Cmd
    49  		cmds []tea.Cmd
    50  	)
    51  
    52  	switch msg := msg.(type) {
    53  	case string:
    54  		m.content += msg
    55  		m.viewport.SetContent(wordwrap.String(m.content, m.viewport.Width-2))
    56  	case tea.KeyMsg:
    57  		switch msg.String() {
    58  		case "ctrl+c", "q", "esc":
    59  			return m, tea.Quit
    60  		}
    61  	case tea.WindowSizeMsg:
    62  		headerHeight := lipgloss.Height(m.headerView())
    63  		footerHeight := lipgloss.Height(m.footerView())
    64  		verticalMarginHeight := headerHeight + footerHeight
    65  
    66  		if !m.ready {
    67  			// Since this program is using the full size of the viewport we
    68  			// need to wait until we've received the window dimensions before
    69  			// we can initialize the viewport. The initial dimensions come in
    70  			// quickly, though asynchronously, which is why we wait for them
    71  			// here.
    72  			m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
    73  			m.viewport.YPosition = headerHeight
    74  			m.viewport.SetContent(m.content)
    75  			m.ready = true
    76  			close(m.renderedOnce)
    77  		} else {
    78  			m.viewport.Width = msg.Width
    79  			m.viewport.Height = msg.Height - verticalMarginHeight
    80  		}
    81  	}
    82  
    83  	// Handle keyboard and mouse events in the viewport
    84  	m.viewport, cmd = m.viewport.Update(msg)
    85  	cmds = append(cmds, cmd)
    86  
    87  	return m, tea.Batch(cmds...)
    88  }
    89  
    90  func (m model) View() string {
    91  	if !m.ready {
    92  		return "\n  Initializing..."
    93  	}
    94  	return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
    95  }
    96  
    97  func (m model) headerView() string {
    98  	info := " (q)uit/esc"
    99  	line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
   100  	return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
   101  }
   102  
   103  func (m model) footerView() string {
   104  	// Disables printing if the viewport is not ready
   105  	if m.viewport.Width == 0 {
   106  		return ""
   107  	}
   108  	if math.IsNaN(m.viewport.ScrollPercent()) {
   109  		return ""
   110  	}
   111  
   112  	viewP := int(m.viewport.ScrollPercent() * 100)
   113  	info := fmt.Sprintf(" %s", percentStyle.Render(fmt.Sprintf("%d%%", viewP)))
   114  	totalLength := m.viewport.Width - lipgloss.Width(info)
   115  	finishedCount := int((float64(totalLength) / 100) * float64(viewP))
   116  
   117  	return lipgloss.JoinHorizontal(
   118  		lipgloss.Center,
   119  		info,
   120  		strings.Repeat("/", finishedCount),
   121  		strings.Repeat("─", max(0, totalLength-finishedCount)),
   122  	)
   123  }
   124  
   125  type termPager struct {
   126  	initialized bool
   127  
   128  	model    *model
   129  	teaPager *tea.Program
   130  
   131  	buf      chan []byte
   132  	statusCh chan error
   133  }
   134  
   135  func (tp *termPager) init() {
   136  	tp.statusCh = make(chan error)
   137  	tp.buf = make(chan []byte)
   138  	tp.model = &model{renderedOnce: make(chan struct{})}
   139  	go func() {
   140  		tp.teaPager = tea.NewProgram(
   141  			tp.model,
   142  		)
   143  
   144  		go func() {
   145  			_, e := tp.teaPager.Run()
   146  			tp.statusCh <- e
   147  			close(tp.statusCh)
   148  		}()
   149  
   150  		fallback := false
   151  		select {
   152  		case <-tp.model.renderedOnce:
   153  		case err := <-tp.statusCh:
   154  			if err != nil {
   155  				fallback = true
   156  			}
   157  		}
   158  		for {
   159  			select {
   160  			case s := <-tp.buf:
   161  				if !fallback {
   162  					tp.teaPager.Send(string(s))
   163  				} else {
   164  					os.Stdout.Write(s)
   165  				}
   166  			case <-tp.statusCh:
   167  				return
   168  			}
   169  		}
   170  	}()
   171  	tp.initialized = true
   172  }
   173  
   174  func (tp *termPager) Write(p []byte) (int, error) {
   175  	if !tp.initialized {
   176  		tp.init()
   177  	}
   178  	tp.buf <- p
   179  	return len(p), nil
   180  }
   181  
   182  func (tp *termPager) WaitForExit() {
   183  	if !tp.initialized {
   184  		return
   185  	}
   186  	// Wait until the term pager this is closed
   187  	// which is trigerred when there is an error
   188  	// or the user quits
   189  	for status := range tp.statusCh {
   190  		_ = status
   191  	}
   192  }
   193  
   194  func newTermPager() *termPager {
   195  	return &termPager{}
   196  }