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 }