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
-
Systemd Fixes Bug While Facing New Challenger in GNU Shepherd
The systemd developers have fixed a really nasty bug amid the release of the new GNU Shepherd init system.
-
AlmaLinux 10.0 Beta Released
The AlmaLinux OS Foundation has announced the availability of AlmaLinux 10.0 Beta ("Purple Lion") for all supported devices with significant changes.
-
Gnome 47.2 Now Available
Gnome 47.2 is now available for general use but don't expect much in the way of newness, as this is all about improvements and bug fixes.
-
Latest Cinnamon Desktop Releases with a Bold New Look
Just in time for the holidays, the developer of the Cinnamon desktop has shipped a new release to help spice up your eggnog with new features and a new look.
-
Armbian 24.11 Released with Expanded Hardware Support
If you've been waiting for Armbian to support OrangePi 5 Max and Radxa ROCK 5B+, the wait is over.
-
SUSE Renames Several Products for Better Name Recognition
SUSE has been a very powerful player in the European market, but it knows it must branch out to gain serious traction. Will a name change do the trick?
-
ESET Discovers New Linux Malware
WolfsBane is an all-in-one malware that has hit the Linux operating system and includes a dropper, a launcher, and a backdoor.
-
New Linux Kernel Patch Allows Forcing a CPU Mitigation
Even when CPU mitigations can consume precious CPU cycles, it might not be a bad idea to allow users to enable them, even if your machine isn't vulnerable.
-
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.