From cbf2c76a29975d77677ff162c0f7db27e280f4da Mon Sep 17 00:00:00 2001 From: Sameer Rahmani Date: Wed, 23 Nov 2022 21:09:40 +0000 Subject: [PATCH] Make a simple draft of filter It reads the input and render it via /dev/tty and let the user select the lines that are important and finaly write them back to stdout. --- filter.go | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ utils.go | 53 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 filter.go create mode 100644 utils.go diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..f017576 --- /dev/null +++ b/filter.go @@ -0,0 +1,162 @@ +package main + +import ( + "fmt" + "log" + + "github.com/gdamore/tcell/v2" +) + +var xmax int +var ymax int + +var highlight tcell.Style + +func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) { + row := y1 + col := x1 + for _, r := range []rune(text) { + s.SetContent(col, row, r, nil, style) + col++ + if col >= x2 { + row++ + col = x1 + } + if row > y2 { + break + } + } +} + +func Render(s tcell.Screen, inputs Items, lowerBound int, upperBound int, current int) { + y := 1 + status := fmt.Sprintf("Current: %d | lower: %d | upper: %d | max: %d", current, lowerBound, upperBound, ymax) + drawText(s, 0, 0, xmax-1, 1, tcell.StyleDefault, status) + for i, item := range (*inputs)[lowerBound:upperBound] { + style := tcell.StyleDefault + var txt string + + if current == lowerBound+i { + style = highlight + } + + if item.Selected { + txt = fmt.Sprintf("XXXX | %s", item.Text) + } else { + txt = fmt.Sprintf("%04d | %s", lowerBound+i, item.Text) + } + + drawText(s, 0, y, xmax-1, y+1, style, txt) + y += 1 + } +} + +func main() { + input, err := ReadFromStdin() + if err != nil { + log.Fatalf("%+v", err) + } + + defStyle := tcell.StyleDefault.Background(tcell.ColorDefault).Foreground(tcell.ColorDefault) + highlight = tcell.StyleDefault.Background(tcell.ColorDarkGray).Foreground(tcell.ColorWhite) + currentLine := 0 + + // Initialize screen + tty, err := tcell.NewDevTty() + if err != nil { + log.Fatalf("%+v", err) + } + + s, err := tcell.NewTerminfoScreenFromTty(tty) + if err != nil { + log.Fatalf("%+v", err) + } + if err := s.Init(); err != nil { + log.Fatalf("%+v", err) + } + s.SetStyle(defStyle) + s.Clear() + xmax, ymax = s.Size() + + quit := func() { + // You have to catch panics in a defer, clean up, and + // re-raise them - otherwise your application can + // die without leaving any diagnostic trace. + maybePanic := recover() + s.Fini() + if maybePanic != nil { + panic(maybePanic) + } + } + defer quit() + + // Here's an example of how to inject a keystroke where it will + // be picked up by the next PollEvent call. Note that the + // queue is LIFO, it has a limited length, and PostEvent() can + // return an error. + // s.PostEvent(tcell.NewEventKey(tcell.KeyRune, rune('a'), 0)) + + done := false + lowerBound := 0 + upperBound := min(ymax-3, len(*input)) + // Event loop + for { + if done { + break + } + s.Clear() + //window := (*input)[lowerBound:upperBound] + Render(s, input, lowerBound, upperBound, currentLine) + + // Update screen + s.Show() + + // Poll event + ev := s.PollEvent() + + // Process event + switch ev := ev.(type) { + case *tcell.EventResize: + s.Sync() + case *tcell.EventKey: + if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC || ev.Rune() == 'q' { + return + } else if ev.Key() == tcell.KeyEnter { + done = true + break + } else if ev.Key() == tcell.KeyCtrlL { + s.Sync() + } else if ev.Rune() == 'C' || ev.Rune() == 'c' { + s.Clear() + } else if ev.Rune() == ' ' { + item := (*input)[currentLine] + item.Selected = !item.Selected + } else if ev.Key() == tcell.KeyUp || ev.Rune() == 'r' { + if currentLine > 0 { + currentLine -= 1 + } + if currentLine <= lowerBound && currentLine != 0 { + lowerBound -= 1 + upperBound -= 1 + } + + } else if ev.Key() == tcell.KeyDown || ev.Rune() == 'n' { + if currentLine < len(*input)-1 { + currentLine += 1 + } + + if currentLine >= upperBound && currentLine != len(*input)-1 { + lowerBound += 1 + upperBound += 1 + } + } + } + } + s.Fini() + for _, item := range *input { + if item.Selected { + fmt.Println(item.Text) + } + } + +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..5c6418f --- /dev/null +++ b/utils.go @@ -0,0 +1,53 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os" +) + +type Item struct { + Text string + Selected bool +} + +type Items = *[]*Item + +func NewItem(text string) *Item { + return &Item{ + Text: text, + Selected: false, + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func ReadFromStdin() (Items, error) { + info, err := os.Stdin.Stat() + if err != nil { + panic(err) + } + + if info.Mode()&os.ModeCharDevice != 0 { + return nil, fmt.Errorf("The command is intended to work with pipes. For example `git log | filter | xarg | ...`") + } + + reader := bufio.NewReader(os.Stdin) + var output []*Item + + for { + input, _, err := reader.ReadLine() + if err != nil && err == io.EOF { + break + } + item := NewItem(string(input)) + output = append(output, item) + } + return &output, nil +}