github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/statusbar.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "strings" 6 "sync" 7 "time" 8 9 "github.com/xyproto/vt100" 10 ) 11 12 const ( 13 fourSpaces = " " 14 fiveSpaces = " " 15 ) 16 17 var mut *sync.RWMutex 18 19 // StatusBar represents the little status field that can appear at the bottom of the screen 20 type StatusBar struct { 21 editor *Editor // an editor struct (for getting the colors when clearing the status) 22 msg string // status message 23 messageAfterRedraw string // a message to be drawn and cleared AFTER the redraw 24 fg vt100.AttributeColor // draw foreground color 25 bg vt100.AttributeColor // draw background color 26 errfg vt100.AttributeColor // error foreground color 27 errbg vt100.AttributeColor // error background color 28 show time.Duration // show the message for how long before clearing 29 offsetY int // scroll offset 30 isError bool // is this an error message that should be shown after redraw? 31 nanoMode bool // Nano emulation? 32 } 33 34 // Used for keeping track of how many status messages are lined up to be cleared 35 var statusBeingShown int 36 37 // NewStatusBar takes a foreground color, background color, foreground color for clearing, 38 // background color for clearing and a duration for how long to display status messages. 39 func NewStatusBar(fg, bg, errfg, errbg vt100.AttributeColor, editor *Editor, show time.Duration, initialMessageAfterRedraw string, nanoMode bool) *StatusBar { 40 mut = &sync.RWMutex{} 41 return &StatusBar{editor, "", initialMessageAfterRedraw, fg, bg, errfg, errbg, show, 0, false, nanoMode} 42 } 43 44 // Draw will draw the status bar to the canvas 45 func (sb *StatusBar) Draw(c *vt100.Canvas, offsetY int) { 46 w := int(c.W()) 47 48 // Shorten the status message if it's longer than the terminal width 49 if len(sb.msg) >= w && w > 4 { 50 sb.msg = sb.msg[:w-4] + "..." 51 } 52 53 h := c.H() - 1 54 if sb.nanoMode { 55 h -= 2 56 } 57 58 if sb.IsError() { 59 mut.RLock() 60 c.Write(uint((w-len(sb.msg))/2), h, sb.errfg, sb.errbg, sb.msg) 61 mut.RUnlock() 62 } else { 63 mut.RLock() 64 c.Write(uint((w-len(sb.msg))/2), h, sb.fg, sb.bg, sb.msg) 65 mut.RUnlock() 66 } 67 68 if sb.nanoMode { 69 mut.RLock() 70 // x-align 71 x := uint((w - len(nanoHelpString1)) / 2) 72 c.Write(x, h+1, sb.editor.NanoHelpForeground, sb.editor.NanoHelpBackground, nanoHelpString1) 73 c.Write(x, h+2, sb.editor.NanoHelpForeground, sb.editor.NanoHelpBackground, nanoHelpString2) 74 mut.RUnlock() 75 } 76 77 mut.Lock() 78 sb.offsetY = offsetY 79 mut.Unlock() 80 } 81 82 // SetMessage will change the status bar message. 83 // A couple of spaces are added as padding. 84 func (sb *StatusBar) SetMessage(msg string) { 85 mut.Lock() 86 87 if len(msg)%2 == 0 { 88 sb.msg = " " 89 } else { 90 sb.msg = " " 91 } 92 sb.msg += msg + " " 93 94 sb.isError = false 95 mut.Unlock() 96 } 97 98 // Message trims and returns the currently set status bar message 99 func (sb *StatusBar) Message() string { 100 mut.RLock() 101 s := strings.TrimSpace(sb.msg) 102 mut.RUnlock() 103 return s 104 } 105 106 // IsError returns true if the error message to be shown is an error message 107 // (it's being displayed a bit longer) 108 func (sb *StatusBar) IsError() bool { 109 var isError bool 110 111 mut.RLock() 112 isError = sb.isError 113 mut.RUnlock() 114 115 return isError 116 } 117 118 // SetErrorMessage is for setting a message that will be shown after a full editor redraw, 119 // to make the message appear also after jumping around in the text. 120 func (sb *StatusBar) SetErrorMessage(msg string) { 121 mut.Lock() 122 123 if len(msg)%2 == 0 { 124 sb.msg = fiveSpaces 125 } else { 126 sb.msg = fourSpaces 127 } 128 sb.msg += msg + fourSpaces 129 130 sb.isError = true 131 mut.Unlock() 132 } 133 134 // SetError is for setting the error message 135 func (sb *StatusBar) SetError(err error) { 136 sb.SetErrorMessage(err.Error()) 137 } 138 139 // Clear will set the message to nothing and then use the editor contents 140 // to remove the status bar field at the bottom of the editor. 141 func (sb *StatusBar) Clear(c *vt100.Canvas) { 142 mut.Lock() 143 defer mut.Unlock() 144 145 // Clear the message 146 sb.msg = "" 147 // Not an error message 148 sb.isError = false 149 150 if c == nil { 151 return 152 } 153 154 // Then clear/redraw the bottom line 155 h := int(c.H()) 156 if sb.nanoMode { 157 h -= 2 158 } 159 offsetY := sb.editor.pos.OffsetY() 160 sb.editor.WriteLines(c, LineIndex(offsetY), LineIndex(h+offsetY), 0, 0) 161 162 c.Draw() 163 } 164 165 // ClearAll will clear all status messages 166 func (sb *StatusBar) ClearAll(c *vt100.Canvas) { 167 mut.Lock() 168 defer mut.Unlock() 169 170 statusBeingShown = 0 171 172 // Clear the message 173 sb.msg = "" 174 // Not an error message 175 sb.isError = false 176 177 if c == nil { 178 return 179 } 180 181 // Then clear/redraw the bottom line 182 h := int(c.H()) 183 if sb.nanoMode { 184 h -= 2 185 } 186 offsetY := sb.editor.pos.OffsetY() 187 sb.editor.WriteLines(c, LineIndex(offsetY), LineIndex(h+offsetY), 0, 0) 188 189 c.Draw() 190 } 191 192 // Show will draw a status message, then clear it after a certain delay 193 func (sb *StatusBar) Show(c *vt100.Canvas, e *Editor) { 194 mut.Lock() 195 statusBeingShown++ 196 mut.Unlock() 197 198 mut.RLock() 199 if sb.msg == "" && !sb.nanoMode { 200 mut.RUnlock() 201 return 202 } 203 offsetY := e.pos.OffsetY() 204 mut.RUnlock() 205 206 sb.Draw(c, offsetY) 207 208 go func() { 209 mut.RLock() 210 sleepDuration := sb.show 211 mut.RUnlock() 212 213 if sb.IsError() { 214 // Show error messages for 3x as long 215 sleepDuration *= 3 216 } 217 time.Sleep(sleepDuration) 218 219 mut.RLock() 220 // Has everyhing been cleared while sleeping? 221 if statusBeingShown <= 0 { 222 // Yes, so just quit 223 mut.RUnlock() 224 return 225 } 226 mut.RUnlock() 227 228 mut.Lock() 229 statusBeingShown-- 230 mut.Unlock() 231 232 mut.RLock() 233 if statusBeingShown == 0 { 234 mut.RUnlock() 235 mut.Lock() 236 // Clear the message 237 sb.msg = "" 238 // Not an error message 239 sb.isError = false 240 mut.Unlock() 241 } else { 242 mut.RUnlock() 243 } 244 }() 245 c.Draw() 246 } 247 248 // ShowNoTimeout will draw a status message that will not be 249 // cleared after a certain timeout. 250 func (sb *StatusBar) ShowNoTimeout(c *vt100.Canvas, e *Editor) { 251 mut.RLock() 252 if sb.msg == "" && !sb.nanoMode { 253 mut.RUnlock() 254 return 255 } 256 mut.RUnlock() 257 258 mut.RLock() 259 offsetY := e.pos.OffsetY() 260 mut.RUnlock() 261 262 sb.Draw(c, offsetY) 263 264 mut.Lock() 265 statusBeingShown++ 266 mut.Unlock() 267 268 c.Draw() 269 } 270 271 // ShowLineColWordCount shows a status message with the current filename, line, column and word count 272 func (sb *StatusBar) ShowLineColWordCount(c *vt100.Canvas, e *Editor, filename string) { 273 statusString := filename + ": " + e.PositionPercentageAndModeInfo() 274 sb.SetMessage(statusString) 275 sb.ShowNoTimeout(c, e) 276 } 277 278 // NanoInfo shows info about the current position, for the Nano emulation mode 279 func (sb *StatusBar) NanoInfo(c *vt100.Canvas, e *Editor) { 280 l := e.LineNumber() 281 ls := e.LastLineNumber() 282 lp := 0 283 if ls > 0 { 284 lp = int(100.0 * (float64(l) / float64(ls))) 285 } 286 287 // TODO: implement char/byte number, like: [ line 2/2 (100%), col 1/1 (100%), char 8/8 (100%) ] 288 //statusString := fmt.Sprintf("[ line %d/%d (%d%), col 1/1 (100%), char 8/8 (100%) ]", l, ls, int(lp*100.0), e.ColNumber(), 999, ?/?) 289 // also available: e.indentation.Spaces and e.mode 290 291 statusString := fmt.Sprintf("[ line %d/%d (%d%%), col %d, word count %d ]", l, ls, lp, e.ColNumber(), e.WordCount()) 292 293 sb.SetMessage(statusString) 294 sb.ShowNoTimeout(c, e) 295 } 296 297 // HoldMessage can be used to let a status message survive on screen for N seconds, 298 // even if e.redraw has been set. statusMessageAfterRedraw is a pointer to the one-off 299 // variable that will be used in keyloop.go, after redrawing. 300 func (sb *StatusBar) HoldMessage(c *vt100.Canvas, dur time.Duration) { 301 if strings.TrimSpace(sb.msg) != "" { 302 sb.messageAfterRedraw = sb.msg 303 go func() { 304 time.Sleep(dur) 305 sb.ClearAll(c) 306 }() 307 } 308 } 309 310 // SetMessageAfterRedraw prepares a status bar message that will be shown after redraw 311 func (sb *StatusBar) SetMessageAfterRedraw(message string) { 312 sb.messageAfterRedraw = message 313 } 314 315 // SetErrorAfterRedraw prepares a status bar message that will be shown after redraw 316 func (sb *StatusBar) SetErrorAfterRedraw(err error) { 317 sb.messageAfterRedraw = err.Error() 318 } 319 320 // SetErrorMessageAfterRedraw prepares a status bar message that will be shown after redraw 321 func (sb *StatusBar) SetErrorMessageAfterRedraw(errorMessage string) { 322 sb.messageAfterRedraw = errorMessage 323 }