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.

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

  • Progress by Installments

    Desktop applications, websites, and even command-line tools routinely display progress bars to keep impatient users patient during time-consuming actions. Mike Schilli shows several programming approaches for handwritten tools.

  • Programming Snapshot – Bulk Renaming

    Renaming multiple files following a pattern often requires small shell scripts. Mike Schilli looks to simplify this task with a Go program.

  • Book Collector

    Mike Schilli does not put books on the shelf; instead, he scans them and saves the PDFs in Google Drive. A command-line Go program then rummages through the digitized books and downloads them as required.

  • Programming Snapshot – Go

    To find files quickly in the deeply nested subdirectories of his home directory, Mike whips up a Go program to index file metadata in an SQLite database.

  • Motion Sensor

    Inotify lets applications subscribe to change notifications in the filesystem. Mike Schilli uses the cross-platform fsnotify library to instruct a Go program to detect what's happening.

comments powered by Disqus