A Go password manager for the terminal

Two Widgets

The terminal UI, like in the previous Snapshot columns, uses the termui package from GitHub. Listing 5 calls ui.Init() in line 12 to initialize its functions and calls ui.Close() to acknowledge a user abort in the defer statement in line 15. This neatly folds up the UI to leave a usable terminal for the shell.

Listing 5


01 package main
02 import (
03   "fmt"
04   ui "github.com/gizak/termui/v3"
05   "github.com/gizak/termui/v3/widgets"
06 )
07 func runUI(lines []string) {
08   rows := []string{}
09   for _, line := range lines {
10     rows = append(rows, mask(line))
11   }
12   if err := ui.Init(); err != nil {
13     panic(err)
14   }
15   defer ui.Close()
16   lb := widgets.NewList()
17   lb.Rows = rows
18   lb.SelectedRow = 0
19   lb.SelectedRowStyle = ui.NewStyle(ui.ColorBlack)
20   lb.TextStyle.Fg = ui.ColorGreen
21   lb.Title = fmt.Sprintf("passview 1.0")
22   pa := widgets.NewParagraph()
23   pa.Text = "[Q]uit [Enter]reveal"
24   pa.TextStyle.Fg = ui.ColorBlack
25   w, h := ui.TerminalDimensions()
26   lb.SetRect(0, 0, w, h-3)
27   pa.SetRect(0, h-3, w, h)
28   ui.Render(lb, pa)
29   uiEvents := ui.PollEvents()
30   for {
31     select {
32     case e := <-uiEvents:
33       switch e.ID {
34         case "k":
35           hideCur(lb)
36           lb.ScrollUp()
37           ui.Render(lb)
38         case "j":
39           hideCur(lb)
40           lb.ScrollDown()
41           ui.Render(lb)
42         case "q", "<C-c>":
43           return
44         case "<Enter>":
45           showCur(lb, lines)
46           ui.Render(lb)
47       }
48     }
49   }
50 }
51 func hideCur(lb *widgets.List) {
52   idx := lb.SelectedRow
53   lb.Rows[idx] = mask(lb.Rows[idx])
54 }
55 func showCur(lb *widgets.List, lines []string) {
56   idx := lb.SelectedRow
57   lb.Rows[idx] = lines[idx]
58 }

The terminal UI shown in Figure 1 consists of two stacked widgets: On top, there is a listbox with the password entries, which the user can scroll through. It also supports leafing through multiple pages if the list of entries is longer than the maximum number of lines displayed. Below the listbox, at the bottom of the terminal window, a paragraph widget indicates which keys the user can press next: Enter reveals the selected password, while Q exits the program.

To allow the UI to take advantage of the entire geometry of the terminal window, line 25 queries the window dimensions, using the TerminalsDimensions() helper function from the termui package. From the width and height of the window, lines 26 and 27 then determine the position and dimensions of the two stacked widgets. In this case, the paragraph widget is assigned the bottom three lines, while the listbox on top gets everything else. Horizontally, both widgets extend to the edges of the terminal window.

The listbox entries reside in the Rows attribute of the listbox as an array slice of strings. Line 17 populates this array with the rows array slice. Before this happened, the for loop starting in line 9 stuffed all masked entries into rows but kept the original lines in lines. The two array slices for masked and unmasked entries make it easy to later reveal masked entries: The code only needs to look at the same index number in the original slice to reveal the unmasked content.

After line 28 has drawn the widgets on screen, line 29 fires off the PollEvents() Go routine, which will intercept all of the user's keystrokes concurrently from now on and send them to the uiEvents channel. From there, the program fetches events via the select statement in the infinite for loop starting in line 30 and immediately responds to all incoming keystrokes. If the user presses K to scroll up, line 35 uses hideCur() (starting in line 51) and the mask() function to hide a password that may have been previously revealed in the current listbox entry. Then, ScrollUp() (line 36) tells the listbox to scroll up one item, and the subsequent Render command smoothly displays the change in the UI. The same applies to pressing J, which lets the user scroll down through the list of entries.

Line 44 intercepts presses of the Enter key and calls the showCur() function defined in line 55. The function fetches the original unmasked password entry from the lines list and replaces the currently selected line of the listbox with it. And, hey presto, account and password are displayed on the screen in clear text. hideCur() starting in line 51 does the opposite and hides the current entry using the mask() function when the user moves away.


As always, the binary can be generated from the Go code using the typical three-step process (Listing 6). This process fetches all the dependent libraries from GitHub, compiles them, and binds everything together to create the finished pv binary. You can then copy this to any target computer with a similar architecture. It'll run there without complaints, and it also conveniently even conjures up the UI into the terminal on remote machines. You will want to copy the test.age password file to a file in your home directory for production operation; the password reminder is then ready for use.

Listing 6

Compiling the Program

$ go mod init pv
$ go mod tidy
$ go build pv.go crypto.go util.go ui.go

The Author

Mike Schilli works as a software engineer in the San Francisco Bay Area, California. Each month in his column, which has been running since 1997, he researches practical applications of various programming languages. If you email him at mailto:mschilli@perlmeister.com he will gladly answer any questions.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

comments powered by Disqus
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.

Learn More