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
-
Red Hat Enterprise Linux 9.5 Released
Notify your friends, loved ones, and colleagues that the latest version of RHEL is available with plenty of enhancements.
-
Linux Sees Massive Performance Increase from a Single Line of Code
With one line of code, Intel was able to increase the performance of the Linux kernel by 4,000 percent.
-
Fedora KDE Approved as an Official Spin
If you prefer the Plasma desktop environment and the Fedora distribution, you're in luck because there's now an official spin that is listed on the same level as the Fedora Workstation edition.
-
New Steam Client Ups the Ante for Linux
The latest release from Steam has some pretty cool tricks up its sleeve.
-
Gnome OS Transitioning Toward a General-Purpose Distro
If you're looking for the perfectly vanilla take on the Gnome desktop, Gnome OS might be for you.
-
Fedora 41 Released with New Features
If you're a Fedora fan or just looking for a Linux distribution to help you migrate from Windows, Fedora 41 might be just the ticket.
-
AlmaLinux OS Kitten 10 Gives Power Users a Sneak Preview
If you're looking to kick the tires of AlmaLinux's upstream version, the developers have a purrfect solution.
-
Gnome 47.1 Released with a Few Fixes
The latest release of the Gnome desktop is all about fixing a few nagging issues and not about bringing new features into the mix.
-
System76 Unveils an Ampere-Powered Thelio Desktop
If you're looking for a new desktop system for developing autonomous driving and software-defined vehicle solutions. System76 has you covered.
-
VirtualBox 7.1.4 Includes Initial Support for Linux kernel 6.12
The latest version of VirtualBox has arrived and it not only adds initial support for kernel 6.12 but another feature that will make using the virtual machine tool much easier.