Skip to content

Commit

Permalink
Add fzf support (--fzf).
Browse files Browse the repository at this point in the history
  • Loading branch information
marcopaganini committed Feb 17, 2024
1 parent 05bc615 commit 4eb742c
Showing 1 changed file with 169 additions and 55 deletions.
224 changes: 169 additions & 55 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
// This file is part of termOTP, a TOTP program for your terminal.
// https://github.com/marcopaganini/termotp.
// (C) 2024 by Marco Paganini
package main

import (
"errors"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"time"

"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/romana/rlog"
)

Expand All @@ -26,24 +31,23 @@ type otpEntry struct {
token string
}

// die logs a message with rlog.Critical and exists with a return code.
// cmdLineFlags contains the command-line flags.
type cmdLineFlags struct {
input string
fuzzy bool
fzf bool
plain bool
version bool
}

// die logs a message with rlog.Critical and exits with a return code.
func die(v ...any) {
if v != nil {
rlog.Critical(v...)
}
os.Exit(1)
}

// usage prints a usage message, defaults for flags, and exits with error.
func usage(s string) {
if s != "" {
fmt.Fprintf(os.Stderr, "Please specify input file with --input\n\n")
}
flag.Usage()
flag.PrintDefaults()
os.Exit(1)
}

// inputFile expands the glob passed as an argument and returns the file with
// the most recent modification time in the list.
func inputFile(fileglob string) (string, error) {
Expand Down Expand Up @@ -72,38 +76,159 @@ func inputFile(fileglob string) (string, error) {
return nfile, nil
}

func main() {
// Usage prints the default usage for this program.
flag.Usage = func() {
_, program := filepath.Split(os.Args[0])
fmt.Fprintf(os.Stderr, "Usage:\n %s [options] [matching_regexp]\n\n", program)
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
// outputTable returns a tabular representation of the vault.
func outputTable(vault []otpEntry, flags cmdLineFlags) string {
// Don't print anything (even a header) if vault is empty.
if len(vault) == 0 {
return ""
}

var (
flagInput = flag.String("input", "", "Input (encrypted) JSON file glob.")
flagFuzzy = flag.Bool("fuzzy", false, "Use interactive fuzzy finder.")
flagVersion = flag.Bool("version", false, "Show program version and exit.")
)
// If no interactive mode requested, print a table by default.
tbl := table.NewWriter()

// "Plain" style and box used for fzf compatible output.
styleBoxPlain := table.BoxStyle{
BottomLeft: "",
BottomRight: "",
BottomSeparator: "",
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("+")),
Left: "",
LeftSeparator: "",
MiddleHorizontal: "",
MiddleSeparator: "",
MiddleVertical: "",
PaddingLeft: " ",
PaddingRight: " ",
PageSeparator: "",
Right: "",
RightSeparator: "",
TopLeft: "",
TopRight: "",
TopSeparator: "",
UnfinishedRow: "",
}
stylePlain := table.StyleDefault
stylePlain.Box = styleBoxPlain

tbl.SetStyle(table.StyleLight)
if flags.plain {
tbl.SetStyle(stylePlain)
}

// Don't use headers (or automerge) in the output for FZF.
if !flags.fzf {
tbl.AppendHeader(table.Row{"Issuer", "Name", "OTP"})
}

for _, v := range vault {
tbl.AppendRow(table.Row{v.issuer, v.account, v.token})
}

tbl.SortBy([]table.SortBy{
{Name: "Issuer", Mode: table.Asc},
{Name: "Name", Mode: table.Asc},
})
tbl.SetColumnConfigs([]table.ColumnConfig{{Number: 1, AutoMerge: !flags.fzf}})
tbl.Style().Options.SeparateRows = false
return tbl.Render()
}

// parseFlags parses the command line flags and returns a cmdLineFlag struct.
func parseFlags() (cmdLineFlags, error) {
flags := cmdLineFlags{}

flag.StringVar(&flags.input, "input", "", "Input (encrypted) JSON file glob.")
flag.BoolVar(&flags.fuzzy, "fuzzy", false, "Use interactive fuzzy finder.")
flag.BoolVar(&flags.fzf, "fzf", false, "Use fzf (needs external binary in path).")
flag.BoolVar(&flags.plain, "plain", false, "Use plain output (disables fuzzy finder and tabular output.)")
flag.BoolVar(&flags.version, "version", false, "Show program version and exit.")

flag.Parse()

if *flagVersion {
if flags.version {
fmt.Printf("Build Version: %s\n", BuildVersion)
return
os.Exit(0)
}

// Flag sanity checking.
if flags.input == "" {
return cmdLineFlags{}, errors.New("Please specify input file with --input")
}

if *flagInput == "" {
usage("Please specify input file with --input")
// Only one output format allowed.
n := 0
for _, v := range []bool{flags.plain, flags.fuzzy, flags.fzf} {
if v {
n++
}
}
if n > 1 {
return cmdLineFlags{}, errors.New("Please only specify ONE output format.")
}

if len(flag.Args()) > 1 {
usage("Specify one or zero regular expressions to match.")
return cmdLineFlags{}, errors.New("Specify one or zero regular expressions to match.")
}

// FZF uses plain output, with modifications (no headers, no automerge)
if flags.fzf {
flags.plain = true
}

return flags, nil
}

// fzf runs fzf on the output and return the chosen token.
func fzf(table string) (string, error) {
cmd := exec.Command("fzf", "--sync")
cmd.Stderr = os.Stderr

stdin, err := cmd.StdinPipe()
if err != nil {
return "", err
}
// Generate output for fzf's stdin.
for _, line := range strings.Split(table, "\n") {
// Remove lines containing only spaces added
// by table. TODO: Find a better fix for this.
if strings.TrimSpace(line) == "" {
continue
}
fmt.Fprintln(stdin, line)
}
stdin.Close()

output, err := cmd.Output()
if err != nil {
return "", err
}

c := strings.TrimSpace(string(output))
f := strings.Fields(c)
// This should not happen (empty line)
if len(f) < 1 {
return "", nil
}
// FZF returns the entire line. The last element contains the token.
return f[len(f)-1], nil
}

func main() {
// Usage prints the default usage for this program.
flag.Usage = func() {
_, program := filepath.Split(os.Args[0])
fmt.Fprintf(os.Stderr, "Usage:\n %s [options] [matching_regexp]\n\n", program)
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
}

flags, err := parseFlags()
if err != nil {
die(err)
}

// Get input file from the input files glob.
input, err := inputFile(*flagInput)
input, err := inputFile(flags.input)
if err != nil {
die(err)
}
Expand Down Expand Up @@ -140,34 +265,23 @@ func main() {
return key1 > key2
})

// Interactive fuzzy finder.
if *flagFuzzy {
token, err := fuzzyFind(vault)
switch {
case flags.fuzzy:
// Interactive fuzzy finder.
if flags.fuzzy {
token, err := fuzzyFind(vault)
if err != nil {
die(err)
}
fmt.Println(token)
}
case flags.fzf:
t, err := fzf(outputTable(vault, flags))
if err != nil {
die(err)
}
fmt.Println(token)
return
fmt.Println(t)
default:
fmt.Println(outputTable(vault, flags))
}

// If no interactive mode requested, print a table by default.
tbl := table.NewWriter()
automerge := table.RowConfig{AutoMerge: true}

tbl.AppendHeader(table.Row{"Issuer", "Name", "OTP"}, automerge)
for _, v := range vault {
tbl.AppendRow(table.Row{v.issuer, v.account, v.token}, automerge)
}

tbl.SortBy([]table.SortBy{
{Name: "Issuer", Mode: table.Asc},
{Name: "Name", Mode: table.Asc},
})
tbl.SetOutputMirror(os.Stdout)
tbl.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true},
})
tbl.SetStyle(table.StyleLight)
tbl.Style().Options.SeparateRows = true
tbl.Render()
}

0 comments on commit 4eb742c

Please sign in to comment.