Skip to content

Instantly share code, notes, and snippets.

@aciidgh
Created July 15, 2016 17:37
Show Gist options
  • Save aciidgh/75de71db0f2580a08b4f40ee82a67589 to your computer and use it in GitHub Desktop.
Save aciidgh/75de71db0f2580a08b4f40ee82a67589 to your computer and use it in GitHub Desktop.
protocol DumbTerminalProtocol {
func write(_ string: String)
func endLine()
func flush()
}
protocol RichTerminalProtocol: DumbTerminalProtocol {
var width: Int { get }
func clearLine()
func moveCursor(y: Int)
func write(_ string: String, inColor color: TermColor)
func wrap(_ string: String, inColor color: TermColor) -> String
}
final class DumbTerminal<Target: OutputStream>: DumbTerminalProtocol {
private var stream: UnsafeMutablePointer<Target>
public init(stream: UnsafeMutablePointer<Target>) {
self.stream = stream
}
func write(_ string: String) {
writeToStream(string)
flush()
}
func endLine() {
writeToStream("\n")
flush()
}
func flush() {
if case let fileStream as OutputFileStreamProtocol = stream.pointee {
fflush(fileStream.file)
}
}
private func writeToStream(_ string: String) {
print(string, terminator: "", to: &stream.pointee)
}
}
final class RichTerminal<Target: OutputStream>: RichTerminalProtocol {
private var stream: UnsafeMutablePointer<Target>
public let width: Int
public init?(stream: UnsafeMutablePointer<Target>) {
// Make sure this is a file stream and tty.
guard case let stdStream as OutputFileStreamProtocol = stream.pointee,
(isatty(stdStream.fd) == 0) else {
return nil
}
width = terminalWidth() ?? 80 // Assume default if we are not able to determine.
self.stream = stream
}
func write(_ string: String) {
writeToStream(string)
flush()
}
/// Flushes the stream if writing to a filestream.
func flush() {
if case let fileStream as OutputFileStreamProtocol = stream.pointee {
fflush(fileStream.file)
}
}
private func writeToStream(_ string: String) {
print(string, terminator: "", to: &stream.pointee)
}
/// Code to clear the line on a tty.
private let clearLineString = "\u{001B}[2K"
/// Code to end any currently active wrapping.
private let resetString = "\u{001B}[0m"
/// Clears the current line and moves the cursor to beginning of the line..
func clearLine() {
writeToStream(clearLineString + "\r")
}
/// Moves the cursor y columns up.
func moveCursor(y: Int) {
writeToStream("\u{001B}[\(y)A")
}
/// Writes a string to the stream.
func write(_ string: String, inColor color: TermColor = .noColor) {
writeToStream(wrap(string, inColor: color))
flush()
}
/// Inserts a new line character into the stream.
func endLine() {
write("\n")
flush()
}
/// Wraps the string into the color mentioned. If terminal is dumb no color code will be inserted.
func wrap(_ string: String, inColor color: TermColor) -> String {
guard !string.isEmpty || color == .noColor else {
return string
}
return color.string + string + resetString
}
}
/// A protocol to operate on terminal based progress bars.
public protocol ProgressBarProtocol {
func update(percent: Int, text: String)
func complete()
}
// Simple ProgressBar which shows the update text in new lines.
public final class SimpleProgressBar: ProgressBarProtocol {
private let term: DumbTerminalProtocol
private let header: String
private var isClear: Bool
init(term: DumbTerminalProtocol, header: String) {
self.term = term
self.header = header
self.isClear = true
}
public func update(percent: Int, text: String) {
if isClear {
term.write(header)
term.endLine()
isClear = false
}
term.write("\(percent)%: " + text)
term.endLine()
}
public func complete() {
}
}
/////// Clients ///////////
/// Three line progress bar with header, redraws on each update.
final class ProgressBar: ProgressBarProtocol {
private let term: RichTerminalProtocol
private let header: String
private var isClear: Bool // true if haven't drawn anything yet.
init(term: RichTerminalProtocol, header: String) {
self.term = term
self.header = header
self.isClear = true
}
func update(percent: Int, text: String) {
if isClear {
term.write(header, inColor: .red)
term.endLine()
isClear = false
}
term.clearLine()
let percentString = percent < 10 ? "0\(percent)" : "\(percent)"
let prefix = "\(percentString)% ["
term.write(prefix, inColor: .green)
// FIXME: Fix the calculation here.
let barWidth = term.width - prefix.utf8.count - 20
let n = Int(Double(barWidth) * Double(percent)/100.0)
term.write("=".repeating(n: n) + "-".repeating(n: barWidth - n))
term.write("]", inColor: .green)
term.endLine()
term.clearLine()
term.write(text)
term.moveCursor(y: 1)
}
func complete() {
term.endLine()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment