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  }