wrds-download

TUI/CLI tool for browsing and downloading WRDS data
Log | Files | Refs | README

dlform.go (4431B)


      1 package tui
      2 
      3 import (
      4 	"fmt"
      5 	"strconv"
      6 	"strings"
      7 
      8 	"github.com/charmbracelet/bubbles/textinput"
      9 	tea "github.com/charmbracelet/bubbletea"
     10 	"github.com/charmbracelet/lipgloss"
     11 )
     12 
     13 type dlFormField int
     14 
     15 const (
     16 	fieldSelect dlFormField = iota
     17 	fieldWhere
     18 	fieldLimit
     19 	fieldOut
     20 	fieldFormat
     21 	fieldCount
     22 )
     23 
     24 // DlForm is the download dialog overlay.
     25 type DlForm struct {
     26 	schema  string
     27 	table   string
     28 	inputs  [fieldCount]textinput.Model
     29 	focused dlFormField
     30 	err     string
     31 }
     32 
     33 // DlSubmitMsg is sent when the user confirms the download form.
     34 type DlSubmitMsg struct {
     35 	Schema  string
     36 	Table   string
     37 	Columns string
     38 	Where   string
     39 	Limit   int
     40 	Out     string
     41 	Format  string
     42 }
     43 
     44 // DlCancelMsg is sent when the user cancels.
     45 type DlCancelMsg struct{}
     46 
     47 func newDlForm(schema, table string, colNames []string) DlForm {
     48 	f := DlForm{schema: schema, table: table}
     49 
     50 	f.inputs[fieldSelect] = textinput.New()
     51 	placeholder := "e.g. gvkey, datadate, sale"
     52 	if len(colNames) > 0 {
     53 		hint := strings.Join(colNames, ", ")
     54 		if len(hint) > 60 {
     55 			hint = hint[:57] + "..."
     56 		}
     57 		placeholder = "e.g. " + hint
     58 	}
     59 	f.inputs[fieldSelect].Placeholder = placeholder
     60 	f.inputs[fieldSelect].CharLimit = 1024
     61 	f.inputs[fieldSelect].SetValue("*")
     62 
     63 	f.inputs[fieldWhere] = textinput.New()
     64 	f.inputs[fieldWhere].Placeholder = "e.g. date >= '2020-01-01'"
     65 	f.inputs[fieldWhere].CharLimit = 512
     66 
     67 	f.inputs[fieldLimit] = textinput.New()
     68 	f.inputs[fieldLimit].Placeholder = "no limit"
     69 	f.inputs[fieldLimit].CharLimit = 12
     70 
     71 	f.inputs[fieldOut] = textinput.New()
     72 	f.inputs[fieldOut].Placeholder = fmt.Sprintf("./%s_%s.parquet", schema, table)
     73 	f.inputs[fieldOut].CharLimit = 256
     74 	f.inputs[fieldOut].SetValue(fmt.Sprintf("./%s_%s.parquet", schema, table))
     75 
     76 	f.inputs[fieldFormat] = textinput.New()
     77 	f.inputs[fieldFormat].Placeholder = "parquet"
     78 	f.inputs[fieldFormat].CharLimit = 10
     79 	f.inputs[fieldFormat].SetValue("parquet")
     80 
     81 	f.inputs[fieldSelect].Focus()
     82 	return f
     83 }
     84 
     85 func (f DlForm) Update(msg tea.Msg) (DlForm, tea.Cmd) {
     86 	switch msg := msg.(type) {
     87 	case tea.KeyMsg:
     88 		switch msg.String() {
     89 		case "esc":
     90 			return f, func() tea.Msg { return DlCancelMsg{} }
     91 		case "enter":
     92 			if f.focused < fieldCount-1 {
     93 				f.inputs[f.focused].Blur()
     94 				f.focused++
     95 				f.inputs[f.focused].Focus()
     96 				return f, textinput.Blink
     97 			}
     98 			// Submit
     99 			out := f.inputs[fieldOut].Value()
    100 			if out == "" {
    101 				out = fmt.Sprintf("./%s_%s.parquet", f.schema, f.table)
    102 			}
    103 			format := strings.ToLower(f.inputs[fieldFormat].Value())
    104 			if format == "" {
    105 				format = "parquet"
    106 			}
    107 			columns := strings.TrimSpace(f.inputs[fieldSelect].Value())
    108 			if columns == "" {
    109 				columns = "*"
    110 			}
    111 			var limit int
    112 			if v := strings.TrimSpace(f.inputs[fieldLimit].Value()); v != "" {
    113 				limit, _ = strconv.Atoi(v)
    114 			}
    115 			return f, func() tea.Msg {
    116 				return DlSubmitMsg{
    117 					Schema:  f.schema,
    118 					Table:   f.table,
    119 					Columns: columns,
    120 					Where:   f.inputs[fieldWhere].Value(),
    121 					Limit:   limit,
    122 					Out:     out,
    123 					Format:  format,
    124 				}
    125 			}
    126 		case "tab", "down":
    127 			f.inputs[f.focused].Blur()
    128 			f.focused = (f.focused + 1) % fieldCount
    129 			f.inputs[f.focused].Focus()
    130 			return f, textinput.Blink
    131 		case "shift+tab", "up":
    132 			f.inputs[f.focused].Blur()
    133 			f.focused = (f.focused + fieldCount - 1) % fieldCount
    134 			f.inputs[f.focused].Focus()
    135 			return f, textinput.Blink
    136 		}
    137 	}
    138 
    139 	var cmd tea.Cmd
    140 	f.inputs[f.focused], cmd = f.inputs[f.focused].Update(msg)
    141 	return f, cmd
    142 }
    143 
    144 func (f DlForm) View(width int) string {
    145 	var sb strings.Builder
    146 
    147 	title := stylePanelHeader.Render(fmt.Sprintf("Download  %s.%s", f.schema, f.table))
    148 	sb.WriteString(title + "\n\n")
    149 
    150 	labels := []string{"SELECT columns", "WHERE clause", "LIMIT rows", "Output path", "Format (parquet/csv)"}
    151 	for i, label := range labels {
    152 		style := lipgloss.NewStyle().Foreground(colorMuted)
    153 		if dlFormField(i) == f.focused {
    154 			style = lipgloss.NewStyle().Foreground(colorFocus)
    155 		}
    156 		sb.WriteString(style.Render(label+"  ") + "\n")
    157 		sb.WriteString(f.inputs[i].View() + "\n\n")
    158 	}
    159 
    160 	hint := styleStatusBar.Render("[tab] next field   [enter] confirm   [esc] cancel")
    161 	sb.WriteString(hint)
    162 
    163 	content := sb.String()
    164 	boxWidth := width - 8
    165 	if boxWidth < 40 {
    166 		boxWidth = 40
    167 	}
    168 
    169 	box := lipgloss.NewStyle().
    170 		Border(lipgloss.RoundedBorder()).
    171 		BorderForeground(colorFocus).
    172 		Padding(1, 2).
    173 		Width(boxWidth).
    174 		Render(content)
    175 
    176 	return lipgloss.Place(width, 20, lipgloss.Center, lipgloss.Center, box)
    177 }