Skip to content

Instantly share code, notes, and snippets.

@marklap
Last active April 4, 2018 00:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marklap/7d04ddba0276b8959418df57eb9fdb13 to your computer and use it in GitHub Desktop.
Save marklap/7d04ddba0276b8959418df57eb9fdb13 to your computer and use it in GitHub Desktop.
Toy CLI progress bar (thread safe) - experiment in using bits as placeholders for the indicator so we can do bitwise operations to shift the spinner left and right instead of mucking with string magic.
////////////////////////////////////////////////////////////////////////////////
// The MIT License (MIT)
//
// Copyright (c) 2018 Mark LaPerriere
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
////////////////////////////////////////////////////////////////////////////////
// Package main is a thread-safe toy CLI progress bar experiment in using bits to store the state
// of the spinner so we can do bitwise operations to move it left and right instead of a glut of
// string operations.
// BONUS: it only depends on the `sync` package (other dependencies are for the example only)
package main
import (
"fmt"
"sync"
"time"
)
const (
indicatorLeft = 0x8000 // 1000 0000 0000 0000
indicatorRight = 0x1 // 0000 0000 0000 0001
directionRight = 0
directionLeft = 1
defaultWidth = 72
indicatorRune = '*'
spaceRune = ' '
)
// progressBar is a simple progress bar that uses bits and bitwise operators to track and move
// an indicator back and forth across a "progress bar".
type progressBar struct {
sync.RWMutex
barState uint16
bar []byte
op int
stat string
width int
drawBuf []byte
}
// NewProgressBar creates a new progress bar
func NewProgressBar() *progressBar {
p := &progressBar{
barState: indicatorLeft,
width: defaultWidth,
bar: make([]byte, 16),
drawBuf: make([]byte, defaultWidth),
}
p.drawBuf[0] = byte('[')
p.drawBuf[18] = byte(']')
p.drawBuf[19] = byte(' ')
return p
}
// Update updates the progress bar representation
func (p *progressBar) Update(stat string) {
// write lock
p.Lock()
defer p.Unlock()
// move the indicator in the appropriate direction
switch p.op {
case directionRight:
p.barState >>= 1
case directionLeft:
p.barState <<= 1
}
switch p.barState {
case indicatorLeft:
// indicator at far left, change direction to right
p.op = directionRight
case indicatorRight:
// indicator at far right, change direction to left
p.op = directionLeft
}
// convert bits to runes
c := uint16(p.barState)
for i := 15; i >= 0; i-- {
if c&1 == 1 {
p.bar[i] = indicatorRune
} else {
p.bar[i] = spaceRune
}
c >>= 1
}
// update the stat message
p.stat = stat
}
// String returns a string representation of the progress bar
func (p *progressBar) String() string {
// read lock
p.RLock()
defer p.RUnlock()
// copy the progress bar into the buffer
copy(p.drawBuf[1:17], p.bar)
// copy the stat message into the buffer
copy(p.drawBuf[20:20+len(p.stat)], []byte(p.stat))
// add the padding from right to left
for i := (20 + len(p.stat)); i < p.width; i++ {
p.drawBuf[i] = byte(' ')
}
// spit out the results
return string(p.drawBuf)
}
// main is just an example use
func main() {
// create a new progress bar
progress := NewProgressBar()
// update a couple times to see it in action
for i := 100; i >= 0; i-- {
var stat string
if i == 0 {
stat = fmt.Sprint("WE'RE DONE!")
} else {
stat = fmt.Sprintf("%d more to go", i)
}
progress.Update(stat)
// make sure to reset the line first with \r
fmt.Printf("\r%s", progress.String())
// artificial pause so we can actually see it
time.Sleep(time.Millisecond * 50)
}
fmt.Println("\ndone")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment