Create a bootable USB stick with terminal UI display
Drive Discovery
Meanwhile, the Go routine, which remains active in the background, runs in an infinite loop. At the beginning the init
variable from line 17 has a value of true
. As soon as the function has checked out all the existing devices after the first pass of the for
loop, line 33 changes the init
variable to false
.
Now things start happening thick and fast. The for
loop repeatedly fires up after the Sleep
statement in line 34 and reads the current device entries again and again. If a new device is found that is not yet in the seen
map, line 29 copies the path for the entry to the drivech
Go channel. The main program snaps it up from there, after having eagerly waited in line 56 in a blocking state (but asynchronously in a Go routine) for the results in Listing 3.
Listing 3
isoflash.go
001 package main 002 003 import ( 004 "flag" 005 "fmt" 006 ui "github.com/gizak/termui/v3" 007 "github.com/gizak/termui/v3/widgets" 008 "os" 009 "path" 010 ) 011 012 func main() { 013 flag.Parse() 014 if flag.NArg() != 1 { 015 usage("Argument missing") 016 } 017 isofile := flag.Arg(0) 018 _, err := os.Stat(isofile) 019 if err != nil { 020 usage(fmt.Sprintf("%v\n", err)) 021 } 022 023 if err = ui.Init(); err != nil { 024 panic(err) 025 } 026 var globalError error 027 defer func() { 028 if globalError != nil { 029 fmt.Printf("Error: %v\n", globalError) 030 } 031 }() 032 defer ui.Close() 033 034 p := widgets.NewParagraph() 035 p.SetRect(0, 0, 55, 3) 036 p.Text = "Insert USB Stick" 037 p.TextStyle.Fg = ui.ColorBlack 038 ui.Render(p) 039 040 pb := widgets.NewGauge() 041 pb.Percent = 100 042 pb.SetRect(0, 2, 55, 5) 043 pb.Label = " " 044 pb.BarColor = ui.ColorBlack 045 046 done := make(chan error) 047 update := make(chan int) 048 confirm := make(chan bool) 049 050 uiEvents := ui.PollEvents() 051 drivech := driveWatch(done) 052 053 var usbPath string 054 055 go func() { 056 usbPath = <-drivech 057 058 size, err := driveSize(usbPath) 059 if err != nil { 060 done <- err 061 return 062 } 063 064 p.Text = fmt.Sprintf("Write to %s " + 065 "(%s)? Hit 'y' to continue.\n", 066 usbPath, size) 067 ui.Render(p) 068 }() 069 070 go func() { 071 for { 072 pb.Percent = <-update 073 ui.Render(pb) 074 } 075 }() 076 077 go func() { 078 <-confirm 079 p.Text = fmt.Sprintf("Copying to %s ...\n", usbPath) 080 ui.Render(p) 081 update <- 0 082 err := cpChunks(isofile, usbPath, update) 083 if err != nil { 084 done <- err 085 } 086 p.Text = fmt.Sprintf("Done.\n") 087 update <- 0 088 ui.Render(p, pb) 089 }() 090 091 for { 092 select { 093 case err := <-done: 094 if err != nil { 095 globalError = err 096 return 097 } 098 case e := <-uiEvents: 099 switch e.ID { 100 case "q", "<C-c>": 101 return 102 case "y": 103 confirm <- true 104 } 105 } 106 } 107 } 108 109 func usage(msg string) { 110 fmt.Printf("%s\n", msg) 111 fmt.Printf("usage: %s iso-file\n", 112 path.Base(os.Args[0])) 113 os.Exit(1) 114 }
To discover the USB stick's storage capacity, Listing 2 runs the sfdisk -s /dev/sdd
command in line 43. The standard output of the shell command, triggered in Go via the os.Exec
package, contains a single integer value that indicates the capacity of the stick in kilobytes. Line 52 truncates the line break from the resulting string. Line 53 uses Atoi()
from the strconv package to convert the string into an integer. Line 58 divides the result by 1MB, so that the capacity in gigabytes is finally output in floating-point format.
The function returns the value, nicely formatted as a string, so that the user can verify in the UI that it is really a USB stick and not a (far larger) hard disk.
Better with a UI
A tool with a user interface, even if it is only a terminal application, is far easier to use than one that only uses the standard output. This is especially true where the user is required to make selections or confirm entries.
The main program in Listing 3 uses the termui terminal UI, which we looked at in a previous issue [1]. The user interface shown in the illustrations at the end of this article consists of two widgets located one above the other in the main window of the terminal UI.
The upper widget is a text widget for the p
variable, which provides status messages to the user and displays new instructions. The lower widget, referenced by the variable pb
, is a progress bar of the Gauge
type. It receives updates via a Go channel and moves the bar from left to right to reflect the incoming percentage values.
But before this can happen, line 14 in Listing 3 first checks whether the main program was actually called as required with an ISO file as a parameter. If not, the code branches to the help page (usage()
) starting in line 109. For the internal communication between the different parts of the program, the code uses no less than five different channels, although Go programmers should only make sparing use of these according to the official guidelines.
The drivech
channel discussed earlier reports freshly plugged in USB sticks to the blocking Go routine in line 56. The update
channel supports communication between the data copier, cpChunks()
from Listing 1, and the main program. As soon as the copier reports a new percentage value, line 72 unblocks and stores the percentage value of the progress bar in the pb
variable. The following call to the function Render()
refreshes the UI and makes sure that the bar also visibly moves. When all the data have been received on the USB stick, line 87 resets the progress bar to zero percent.
Keyboard input such as Ctrl+C or Q is also intercepted by the event loop triggered in line 50 using PollEvents()
on the uiEvents
channel. Line 98 analyzes the pressed key and triggers the end of the program for the two abort sequences. If the stick has already been detected, the Go routine puts the brakes on in line 77 to wait for data from the confirm
channel in line 78. If the user presses Y, line 103 feeds the event into the confirm
channel. Line 78 picks it up and opens the flood gates for the copy action.
Deferred or Canceled?
The done
channel in turn is used by the main program to control when the UI should be packed away and the program terminated. The problem arises here that a terminal UI cannot simply write to Stderr
or abort the program with panic()
if a serious error occurs: Stderr
is blocked in graphics mode, and an abruptly aborted program would leave an unusable terminal that users could only fix by closing the terminal window and opening a new one.
The code from Listing 1 helps to feed potentially fatal errors into the done
channel, where line 93 from Listing 3 fields them and stores them in the globalError
variable declared in line 26. The clever sequence of defer
statements in lines 27 and 32 ensures that the UI is always closed first and that only then is the error leading to program termination in globalError
output to stdout
.
Successive defer
statements are executed in reverse order: Go builds a defer
stack by executing the first entries last. Since the defer
in line 27 outputs the global error and the defer
in line 32 breaks down the UI, the main program always breaks down the UI first and then outputs the error. Doing this the opposite way would mean losing the error.
« Previous 1 2 3 Next »
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
-
First Release Candidate for Linux Kernel 6.14 Now Available
Linus Torvalds has officially released the first release candidate for kernel 6.14 and it includes over 500,000 lines of modified code, making for a small release.
-
System76 Refreshes Meerkat Mini PC
If you're looking for a small form factor PC powered by Linux, System76 has exactly what you need in the Meerkat mini PC.
-
Gnome 48 Alpha Ready for Testing
The latest Gnome desktop alpha is now available with plenty of new features and improvements.
-
Wine 10 Includes Plenty to Excite Users
With its latest release, Wine has the usual crop of bug fixes and improvements, along with some exciting new features.
-
Linux Kernel 6.13 Offers Improvements for AMD/Apple Users
The latest Linux kernel is now available, and it includes plenty of improvements, especially for those who use AMD or Apple-based systems.
-
Gnome 48 Debuts New Audio Player
To date, the audio player found within the Gnome desktop has been meh at best, but with the upcoming release that all changes.
-
Plasma 6.3 Ready for Public Beta Testing
Plasma 6.3 will ship with KDE Gear 24.12.1 and KDE Frameworks 6.10, along with some new and exciting features.
-
Budgie 10.10 Scheduled for Q1 2025 with a Surprising Desktop Update
If Budgie is your desktop environment of choice, 2025 is going to be a great year for you.
-
Firefox 134 Offers Improvements for Linux Version
Fans of Linux and Firefox rejoice, as there's a new version available that includes some handy updates.
-
Serpent OS Arrives with a New Alpha Release
After months of silence, Ikey Doherty has released a new alpha for his Serpent OS.