A Go terminal UI for displaying network adapters in real time

Programming Snapshot – termui

Article from Issue 218/2019
Author(s):

Even command-line lovers appreciate a classic terminal UI. Mike Schilli shows how to whip up a Go program that dynamically displays network interfaces and IPs.

Every time I connect my laptop to a router for diagnostic purposes, the question arises: On which dynamically assigned IP address will the router see the laptop? After all, you need to enter a router address on the same subnet to display the router's admin page (Figure 1).

Figure 1: This machine uses an IP address on subnet 192.168.147.1/24 to connect to the router.

To do this, I used to type ifconfig several times in a terminal window and extracted the desired address from the mess of data printed next to:

inet 192.168.1.1 netmask 0xfffffff0

I thought there must be an easier way. How about a program that figures out all available network interfaces every couple of seconds, sorts them into a list, and dynamically displays their IP addresses? In a graphical user interface (GUI) that popped up, the relieved user could then watch a plugged in USB adapter appear as a new network interface and see the IP assigned to it by DHCP as well.

But, it doesn't have to be a genuine graphical application, as I recently presented in this column, elegantly programmed with GitHub's Electron framework [1]. Command-line friends prefer terminal UIs à la top, instead; they can be started, read, and closed quickly, without keyboard addicts having to leave the terminal window at all or reach for the unloved mouse.

Ready To Go

What the ifconfig command-line tool prints is something that the net package in Go already has up its sleeve, and it provides network adapter configuration in the form of a data structure. Listing 1 [2] shows the implementation of an ifconfig helper package that exports an AsStrings() function, which returns a formatted list of all network interfaces detected on the current machine.

Listing 1

ifconfig.go

01 package ifconfig
02
03 import (
04   "fmt"
05   "net"
06   "sort"
07   "strings"
08 )
09
10 func AsStrings() []string {
11   var list []string
12
13   ifaces, _ := net.Interfaces()
14   for _, iface := range ifaces {
15     network := fmt.Sprintf("%10s",
16       iface.Name)
17     addrs, _ := iface.Addrs()
18     if len(addrs) == 0 {
19       continue
20     }
21     split := strings.Split(
22       addrs[0].String(), "/")
23     addr := split[0]
24     if net.ParseIP(addr).To4() != nil {
25       network += " " + addr
26       list = append(list, network)
27     }
28   }
29   sort.Strings(list)
30   return list
31 }

In line 13 of Listing 1, the Interfaces() method from the Go net package returns a series of network interface structures, through which the for loop iterates with the help of range as of line 14. The range() function for the delivered slice (a dynamic window on a static array in Go) not only returns the current element for each loop iteration, but also its index into the slice, which is not needed here and is therefore assigned to the _ pseudo-variable and thrown away.

The string formatter in line 15 sets the network variable to the name of the interface (e.g., eth0 for the first Ethernet adapter found), right-justified with a maximum length of 10 characters. The IP addresses that the interface listens on are retrieved by the Addrs() function.

Common in Go, the function returns two parameters, first a slice with all discovered IPs and then an error variable, hopefully set to nil, indicating that everything went fine. To save space in this article, that second error variable is set to _ in line 17 of Listing 1, thus discarding errors – something you should not do on a production system.

If the device does not have an IP assigned to it, the discovered network interface is not relevant and line 19 uses continue to jump to the next one. Of potentially multiple IPs per interface, only the first one is of interest on my simply structured laptop. Since the network there may be in CIDR format instead of an IP (e.g., 192.168.1.1/24), the Split() function from the strings package splits off the netmask in line 21 so that the addr variable contains only the actual IP as a string.

Because I still work with good old IPv4 at home, line 24 blocks IPv6 addresses. The call to

net.ParseIP(addr).To4()

tries to convert any addresses discovered to IPv4 format, which only works for IPv4 addresses and returns an error value other than nil for IPv6 addresses. If your home setup is up-to-date and uses IPv6, this filter condition needs to go, of course, and you'll see IPv6 addresses in the display as well.

Line 29 sorts the formatted list alphabetically before the return statement in the following line returns it to the caller.

Compiler Playing Dumb

When you are picking names for new functions in Go, remember that in a package like ifconfig, functions starting with a lowercase letter are not exported. If the importing main program called an as_strings() function implemented in the package, the Go compiler would refuse to comply and simply claim that such a function does not exist. Instead, the function in ifconfig must begin with an uppercase letter: The capitalized AsStrings() will later also be found by the main program importing the package.

Go compiles everything that belongs to a program into a static binary. For the compiler to find the imported package in Listing 1 when the main program is put together, it must find the static *.a library generated for it in the Go path ($GOPATH), which is typically found below ~/go in your home directory. If the library goes by the name of ifconfig, its source code must be stored in a newly created directory named ifconfig below src and be installed from there with go install:

dir=~/go/src/ifconfig
mkdir -p $dir
cp ifconfig.go $dir
cd $dir
go install

This command sequence creates the static library ifconfig.a below pkg/linux_amd64 in the Go path; later, when building the main program, the Go compiler links the library statically with it.

The termui project on GitHub [3] is used as the terminal GUI for the utility. The beauty of Go is that its code can be installed directly from the web using the go get command-line tool:

go get -u github.com/gizak/termui

The get command fetches it from GitHub, compiles it, and installs the libraries created by this step in the Go path, where the compiler will find them later, if a Go program demands they should be linked with it. The -u flag tells go get not only to install the required package, but also to update any dependent packages.

Exciting Events

Like most GUIs, termui is event-based. The user initially defines some widgets, such as list or text boxes, arranges them with a layout tool in 2D space, starts the loop, and then intercepts events such as Terminal window size reduced or Key combination Ctrl+C pressed or The timer that starts every second has just elapsed. For today's network tool, Listing 2 defines two different widgets, as shown in the screenshot in Figure 2: a list box at the top, which lists the available network interfaces with their IPs as entries, and a text box at the bottom, which only reminds the user to press the q key to exit the program.

Listing 2

iftop.go

01 package main
02
03 import (
04   t "github.com/gizak/termui"
05   "ifconfig"
06   "log"
07 )
08
09 var listItems = []string{}
10
11 func main() {
12   err := t.Init()
13   if err != nil {
14     log.Fatalln("Termui init failed")
15   }
16
17   // Cleanup UI on exit
18   defer t.Close()
19
20   // Listbox displaying interfaces
21   lb := t.NewList()
22   lb.Height = 10
23   lb.BorderLabel = "Networks"
24   lb.BorderFg = t.ColorGreen
25   lb.ItemFgColor = t.ColorBlack
26
27   // Textbox
28   txt := t.NewPar("Type 'q' to quit.")
29   txt.Height = 3
30   txt.BorderFg = t.ColorGreen
31   txt.TextFgColor = t.ColorBlack
32
33   t.Body.AddRows(
34     t.NewRow(
35       t.NewCol(12, 0, lb)),
36     t.NewRow(
37       t.NewCol(12, 0, txt)))
38
39   // Initial rendering
40   t.Body.Align()
41   t.Render(t.Body)
42
43   // Resize widgets when term window
44   // gets resized
45   t.Handle("/sys/wnd/resize",
46     func(t.Event) {
47       t.Body.Width = t.TermWidth()
48       t.Body.Align()
49       t.Render(t.Body)
50     })
51
52   // Refresh every second
53   t.Handle("/timer/1s", func(t.Event) {
54     lb.Items = ifconfig.AsStrings()
55     t.Render(t.Body)
56   })
57
58   // Keyboard input
59   t.Handle("/sys/kbd/C-c", func(t.Event) {
60     t.StopLoop()
61   })
62   t.Handle("/sys/kbd/q", func(t.Event) {
63     t.StopLoop()
64   })
65
66   t.Loop()
67 }
Figure 2: The Go program displays a dynamic list of network ports.

After line 4 has imported the termui package, assigning it the t abbreviation, the main program calls Init() to initialize the GUI for the termui package, wiping the terminal window clean and setting it to graphics mode. At the end of the main program, the Close() call reverts these actions, and a normal text terminal is restored. Thanks to the defer keyword, which comes as part of the Go standard feature set, the cleanup is planned in line 18, but Go delays action until leaving the main function.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Programming Snapshot – Go Network Diagnostics

    Why is the WiFi not working? Instead of always typing the same steps to diagnose the problem, Mike Schilli writes a tool in Go that puts the wireless network through its paces and helps isolate the cause.

  • Sweet Dreams

    Bathtub singer Mike Schilli builds a Go tool that manages song lyrics from YAML files and helps him learn them by heart, line by line.

  • Hide and Seek

    A Go application for the terminal helps Mike Schilli remember his passwords.

  • Hard Disk Dashboard

    To keep an eye on the remaining disk space during storage-intensive operations, you can check out this speedometer/odometer written in Go.

  • Monitoring Station

    With a monitoring system implemented in Go, Mike Schilli displays the Docker containers that have been launched and closed on his system.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News