A Go terminal UI for displaying network adapters in real time
Programming Snapshot – termui
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).
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 }
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
(incl. VAT)
Buy Linux Magazine
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.
News
-
Canonical Bumps LTS Support to 12 years
If you're worried that your Ubuntu LTS release won't be supported long enough to last, Canonical has a surprise for you in the form of 12 years of security coverage.
-
Fedora 40 Beta Released Soon
With the official release of Fedora 40 coming in April, it's almost time to download the beta and see what's new.
-
New Pentesting Distribution to Compete with Kali Linux
SnoopGod is now available for your testing needs
-
Juno Computers Launches Another Linux Laptop
If you're looking for a powerhouse laptop that runs Ubuntu, the Juno Computers Neptune 17 v6 should be on your radar.
-
ZorinOS 17.1 Released, Includes Improved Windows App Support
If you need or desire to run Windows applications on Linux, there's one distribution intent on making that easier for you and its new release further improves that feature.
-
Linux Market Share Surpasses 4% for the First Time
Look out Windows and macOS, Linux is on the rise and has even topped ChromeOS to become the fourth most widely used OS around the globe.
-
KDE’s Plasma 6 Officially Available
KDE’s Plasma 6.0 "Megarelease" has happened, and it's brimming with new features, polish, and performance.
-
Latest Version of Tails Unleashed
Tails 6.0 is based on Debian 12 and includes GNOME 43.
-
KDE Announces New Slimbook V with Plenty of Power and KDE’s Plasma 6
If you're a fan of KDE Plasma, you'll be thrilled to hear they've announced a new Slimbook with an AMD CPU and the latest version of KDE Plasma desktop.
-
Monthly Sponsorship Includes Early Access to elementary OS 8
If you want to get a glimpse of what's in the pipeline for elementary OS 8, just set up a monthly sponsorship to help fund its continued existence.