go

Center text in a terminal with Go
Hi! ๐Ÿ‘‹

Reading time of 921 words
5 minutes
Reading time of 921 words ~ 5 minutes


Did you find this article helpful?
Please consider tipping me a coffee as a thank you.
Ko-fi Buy Me a Coffee
Did you find this article helpful? Please consider tipping me a coffee or three as a thank you.
Tip using Ko-fi or Buy Me a Coffee

This guide will show how simple it is to center text in a terminal or a text file using the Go standard and external libraries. We will create two functions, NCenter, which provides for a width parameter, and a Center func that will determine the center placement based on the width of the terminal screen.

You can find the final code on GitHub or play with NCenter() in the Go Playground.

NCenter()

The NCenter func takes a width and a string parameter and returns bytes.Buffer. This flexible Buffer value enables its usage with io.writers or String and Bytes methods.

func NCenter(width int, s string) *bytes.Buffer {}

Create two constants for clarity. The half value of 2 and a Unicode codepoint for a space character.

func NCenter(width int, s string) *bytes.Buffer {
    const half, space = 2, "\u0020"
}

Create an empty buffer of bytes that will hold our data.

func NCenter(width int, s string) *bytes.Buffer {
    const half, space = 2, "\u0020"
    var b bytes.Buffer
}

Determine the n integer value that is the number of space characters needed to center the s string.

func NCenter(width int, s string) *bytes.Buffer {
    const half, space = 2, "\u0020"
    var b bytes.Buffer
    n := (width - utf8.RuneCountInString(s)) / half
}

The width value is the container width for the text, often known as the column value. We use RuneCount to determine the number of characters in the string. Remember len(s) returns the number of bytes, which is not helpful for this situation. For example, if we have an 80 width value and want to center Hello world! ๐Ÿ‘‹ containing 14 characters.

n = (80 - 14) / 2

n is equal to 33, meaning we need to prefix 33 spaces to align โ€œHello world! ๐Ÿ‘‹โ€ to an 80 column text area.

fmt.Fprintf(&b, "%s%s", strings.Repeat(space, n), s)

Formats and saves the s string to the b buffer. Note that we are also prefixing the s string with n number of repeated space characters.

func NCenter(width int, s string) *bytes.Buffer {
    const half, space = 2, "\u0020"
    var b bytes.Buffer
    n := (width - utf8.RuneCountInString(s)) / half
    fmt.Fprintf(&b, "%s%s", strings.Repeat(space, n), s)
    return &b
}

Finally, we can use our NCenter function.

package main

import (
	"bytes"
	"fmt"
	"strings"
	"unicode/utf8"
)

func main() {
	const s = "Hello world! ๐Ÿ‘‹"
	fmt.Println(NCenter(80, s))
}

func NCenter(width int, s string) *bytes.Buffer {
	const half, space = 2, "\u0020"
	var b bytes.Buffer
	n := (width - utf8.RuneCountInString(s)) / half
	fmt.Fprintf(&b, "%s%s", strings.Repeat(space, n), s)
	return &b
}
$ go run .
                                 Hello world! ๐Ÿ‘‹

The NCenter is now working but has a significant flaw. Go panics if the supplied width parameter matches zero or is less than the number of characters in the string. The s string contains 14 characters, but if we attempt to center it to a text width of 10, it results in an invalid negative n value.

func main() {
    const s = "Hello world! ๐Ÿ‘‹"
    fmt.Println(NCenter(10, s))
}
$ go run .
panic: strings: negative Repeat count

To avoid this flaw, we must only use strings.Repeat if the n value is greater than zero. Otherwise, we return the string with as-is.

func NCenter(width int, s string) *bytes.Buffer {
    const half, space = 2, "\u0020"
    var b bytes.Buffer
    n := (width - utf8.RuneCountInString(s)) / half
    if n < 1 {
        fmt.Fprint(&b, s)
        return &b
    }
    fmt.Fprintf(&b, "%s%s", strings.Repeat(space, n), s)
    return &b
}
$ go run .
Hello world! ๐Ÿ‘‹

Center()

The Center func will determine a width int value for NCenter and return the result. The width value will either be the number of columns of the active terminal screen or zero. As of writing, Go v1.17 cannot determine the column count of PowerShell or Windows CMD shells.

func Center(s string) *bytes.Buffer {}

We now need the file descriptor integer value for the operating systemโ€™s standard input.

func Center(s string) *bytes.Buffer {
    fd := int(os.Stdin.Fd())
}

Finally, we can use the extended term libraryโ€™s GetSize function to obtain the w width of our active terminal window. If an error occurs getting this, then pass on a zero-width value.

func Center(s string) *bytes.Buffer {
    fd := int(os.Stdin.Fd())
    w, _, err := term.GetSize(fd)
    if err != nil {
        return NCenter(0, s)
    }
    return NCenter(w, s)
}

Letโ€™s test it out.

func main() {
	const s = "Hello world! ๐Ÿ‘‹"
	fmt.Println(Center(s))
}
$ go run .
macOS iTerm2 terminal
macOS iTerm2 terminal

Final code

package main

import (
    "bytes"
    "fmt"
    "os"
    "strings"
    "unicode/utf8"

    "golang.org/x/term"
)

func main() {}

// NCenter centers the string to the column width.
func NCenter(width int, s string) *bytes.Buffer {
    const half, space = 2, "\u0020"
    var b bytes.Buffer
    n := (width - utf8.RuneCountInString(s)) / half
    if n < 1 {
        fmt.Fprintf(&b, s)
        return &b
    }
    fmt.Fprintf(&b, "%s%s", strings.Repeat(space, int(n)), s)
    return &b
}

// Center the string to the width of the terminal.
// When the width is unknown, the string is left-aligned.
func Center(s string) *bytes.Buffer {
    fd := int(os.Stdin.Fd())
    w, _, err := term.GetSize(fd)
    if err != nil {
        return NCenter(0, s)
    }
    return NCenter(w, s)
}

Various uses

func main() {
	const s = "Hello world! ๐Ÿ‘‹"

	// print to the terminal
	buf := Center(s)
	fmt.Println(buf)
	fmt.Fprintln(os.Stdout, buf)
	io.Copy(os.Stdout, buf)

	// save to a text file
	buf = NCenter(80, s)
	if err := os.WriteFile("hi.txt", buf.Bytes(), 0666); err != nil {
		log.Fatal(err)
	}
}
macOS iTerm2 terminal
macOS iTerm2 terminal
macOS iTerm2 terminal
macOS TextEdit displaying the hi.txt file
Ubuntu bash tab
Windows Terminal running a PowerShell tab in the top pane and a Ubuntu bash tab in the bottom pane.

Written by Ben Garrett

Did you find this article helpful?
Please consider tipping me a coffee as a thank you.
Ko-fi Buy Me a Coffee
Did you find this article helpful? Please consider tipping me a coffee or three as a thank you.
Tip using Ko-fi or Buy Me a Coffee