Skip to content

Commit

Permalink
feat: toggle file picker, scroll textarea
Browse files Browse the repository at this point in the history
  • Loading branch information
folago committed May 15, 2024
1 parent 9d5752f commit 0741cd4
Show file tree
Hide file tree
Showing 7 changed files with 432 additions and 995 deletions.
22 changes: 22 additions & 0 deletions keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

import (
"github.com/charmbracelet/bubbles/key"
)

type keymap = struct {
file, quit key.Binding
}

func newkeymap() keymap {
return keymap{
file: key.NewBinding(
key.WithKeys("f", "F"),
key.WithHelp("f", "toggle file picker"),
),
quit: key.NewBinding(
key.WithKeys("esc", "q"),
key.WithHelp("q", "quit"),
),
}
}
190 changes: 78 additions & 112 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,72 +1,99 @@
package main

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"text/template"
"time"

"github.com/Masterminds/sprig/v3"
"github.com/charmbracelet/bubbles/filepicker"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

const (
helpHeight = 5
)

func newTextarea() textarea.Model {
t := textarea.New()
t.Prompt = ""
t.Placeholder = "~these are not the droids you are looking for~"
t.ShowLineNumbers = true
t.Cursor.Style = cursorStyle
t.FocusedStyle.Placeholder = focusedPlaceholderStyle
t.BlurredStyle.Placeholder = placeholderStyle
t.FocusedStyle.CursorLine = cursorLineStyle
t.FocusedStyle.Base = focusedBorderStyle
t.BlurredStyle.Base = blurredBorderStyle
t.FocusedStyle.EndOfBuffer = endOfBufferStyle
t.BlurredStyle.EndOfBuffer = endOfBufferStyle
t.KeyMap.DeleteWordBackward.SetEnabled(false)
t.KeyMap.LineNext = key.NewBinding(key.WithKeys("down"))
t.KeyMap.LinePrevious = key.NewBinding(key.WithKeys("up"))
t.Blur()
return t
}

type model struct {
textinput textinput.Model
original, rendered textarea.Model
filepicker filepicker.Model
fpvisible bool
selectedFile string
quitting bool
err error
keymap keymap
help help.Model

cwd string
tmpldata map[string]any
}

type clearErrorMsg struct{}

func clearErrorAfter(t time.Duration) tea.Cmd {
return tea.Tick(t, func(_ time.Time) tea.Msg {
return clearErrorMsg{}
})
}

func (m model) Init() tea.Cmd {

return tea.Batch(tea.EnterAltScreen, m.filepicker.Init())
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmdList = []tea.Cmd{}
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
switch {

case key.Matches(msg, m.keymap.quit):
m.quitting = true
return m, tea.Quit
case key.Matches(msg, m.keymap.file):
m.fpvisible = !m.fpvisible
cmdList = append(cmdList, func() tea.Msg {
return focusTexMsg(!m.fpvisible)
})
}
case focusTexMsg:
if msg {
m.original.Focus()
m.rendered.Focus()
} else {
m.original.Blur()
m.rendered.Blur()
}
case tea.WindowSizeMsg:
m.original.SetHeight(msg.Height)
m.original.SetHeight(msg.Height - helpHeight)
m.original.SetWidth(msg.Width / 2)
m.rendered.SetHeight(msg.Height)
m.rendered.SetHeight(msg.Height - helpHeight)
m.rendered.SetWidth(msg.Width / 2)
case clearErrorMsg:
m.err = nil
}

var fcmd, tcmd, icmd tea.Cmd
m.filepicker, fcmd = m.filepicker.Update(msg)
cmdList = append(cmdList, fcmd, tcmd, icmd)
if m.fpvisible || m.quitting {
m.filepicker, fcmd = m.filepicker.Update(msg)
cmdList = append(cmdList, fcmd)
}
m.original, tcmd = m.original.Update(msg)
m.rendered, tcmd = m.rendered.Update(msg)
m.textinput, icmd = m.textinput.Update(msg)

// Did the user select a file?
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
Expand All @@ -82,36 +109,32 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.rendered.SetValue(tmpl)
}

// Did the user select a disabled file?
// This is only necessary to display an error to the user.
if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect {
// Let's clear the selectedFile and display an error.
m.err = errors.New(path + " is not valid.")
m.selectedFile = ""
return m, tea.Batch(fcmd, tcmd, icmd, clearErrorAfter(2*time.Second))
}

return m, fcmd
return m, tea.Batch(cmdList...)
}

type focusTexMsg bool

func (m model) View() string {
if m.quitting {
return ""
}
help := m.help.ShortHelpView([]key.Binding{
m.keymap.file,
m.keymap.quit,
})
var views []string
var s strings.Builder
s.WriteString("\n ")
if m.err != nil {
s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error()))
} else if m.selectedFile == "" {
s.WriteString("Pick a file:")
} else {
s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile))
if m.fpvisible {
s.WriteString("\n ")
if m.selectedFile == "" {
s.WriteString("Pick a file:")
} else {
s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile))
}
s.WriteString("\n\n" + m.filepicker.View() + "\n")
}
s.WriteString("\n\n" + m.filepicker.View() + "\n")
return lipgloss.JoinHorizontal(
lipgloss.Top,
lipgloss.JoinVertical(lipgloss.Left, m.textinput.View(), s.String()),
m.original.View(), m.rendered.View())
views = append(views, s.String(), m.original.View(), m.rendered.View())
return lipgloss.JoinHorizontal(lipgloss.Top, views...) + "\n\n" + help
}

func main() {
Expand All @@ -122,81 +145,24 @@ func main() {
tea.LogToFile("tmpltui.log", "tmpltui")

fp := filepicker.New()
// fp.AllowedTypes = []string{".mod", ".sum", ".go", ".txt", ".md", ".tmpl", ".json", ".yaml"}
fp.AllowedTypes = []string{}
fp.CurrentDirectory, _ = os.Getwd()

m := model{
filepicker: fp,
original: textarea.New(),
rendered: textarea.New(),
original: newTextarea(),
rendered: newTextarea(),
cwd: cwd,
tmpldata: loadTmplData(),
fpvisible: true,
keymap: newkeymap(),
help: help.New(),
}
m.original.CharLimit = 5000
m.rendered.CharLimit = 5000
m.original.CharLimit = 8000
m.rendered.CharLimit = 8000
_, err = tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run()
if err != nil {
fmt.Println(err)
}
fmt.Println("bye")
}

func loadTmplData() map[string]any {
ret := map[string]any{}
data, err := os.ReadFile("data.json")
if err != nil {
log.Fatal(err)
}
err = json.Unmarshal(data, &ret)
if err != nil {
log.Fatal(err)
}

return ret
}

// loadFile reads the file and attempt to render it as a template, returns
// the original file, the rendered. In case of errors the returned value
// will contain the error text.
func loadFile(fpath string, tdata map[string]any) (string, string) {
fdata, err := os.ReadFile(fpath)
if err != nil {
return fmt.Sprintf("error reading file %s: %+v\n", fpath, err), ""
}
t := template.New(fpath).Option("missingkey=zero").Funcs(sprig.FuncMap())

setDelimiters(t, tdata)

fstring := string(fdata)
t, err = t.Parse(fstring)
if err != nil {
return fstring, fmt.Sprintf("error parsing template %s: %+v\n", fpath, err)
}

var buff bytes.Buffer
err = t.Execute(&buff, tdata)
if err != nil {
return fstring, fmt.Sprintf("error rendering template %s: %+v\n", fpath, err)
}

return fstring, buff.String()
}

const (
leftDelim = "left_delimiter"
rightDelim = "right_delimiter"
)

// finds and sets the delimiters
func setDelimiters(t *template.Template, tdata map[string]any) {
// default delimiters, left and right
var ld, rd = "{{", "}}"
if left, ok := tdata[leftDelim]; ok {
ld = left.(string)
}
if right, ok := tdata[rightDelim]; ok {
rd = right.(string)
}
t.Delims(ld, rd)
}
71 changes: 71 additions & 0 deletions render.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
"text/template"

"github.com/Masterminds/sprig/v3"
)

func loadTmplData() map[string]any {
ret := map[string]any{}
data, err := os.ReadFile("data.json")
if err != nil {
log.Fatal(err)
}
err = json.Unmarshal(data, &ret)
if err != nil {
log.Fatal(err)
}

return ret
}

// loadFile reads the file and attempt to render it as a template, returns
// the original file, the rendered. In case of errors the returned value
// will contain the error text.
func loadFile(fpath string, tdata map[string]any) (string, string) {
fdata, err := os.ReadFile(fpath)
if err != nil {
return fmt.Sprintf("error reading file %s: %+v\n", fpath, err), ""
}
t := template.New(fpath).Option("missingkey=zero").Funcs(sprig.FuncMap())

setDelimiters(t, tdata)

fstring := string(fdata)
t, err = t.Parse(fstring)
if err != nil {
return fstring, fmt.Sprintf("error parsing template %s: %+v\n", fpath, err)
}

var buff bytes.Buffer
err = t.Execute(&buff, tdata)
if err != nil {
return fstring, fmt.Sprintf("error rendering template %s: %+v\n", fpath, err)
}

return fstring, buff.String()
}

const (
leftDelim = "left_delimiter"
rightDelim = "right_delimiter"
)

// finds and sets the delimiters
func setDelimiters(t *template.Template, tdata map[string]any) {
// default delimiters, left and right
var ld, rd = "{{", "}}"
if left, ok := tdata[leftDelim]; ok {
ld = left.(string)
}
if right, ok := tdata[rightDelim]; ok {
rd = right.(string)
}
t.Delims(ld, rd)
}
27 changes: 27 additions & 0 deletions style.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import "github.com/charmbracelet/lipgloss"

var (
cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212"))

cursorLineStyle = lipgloss.NewStyle().
Background(lipgloss.Color("57")).
Foreground(lipgloss.Color("230"))

placeholderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("238"))

endOfBufferStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("235"))

focusedPlaceholderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("99"))

focusedBorderStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("238"))

blurredBorderStyle = lipgloss.NewStyle().
Border(lipgloss.HiddenBorder())
)
Loading

0 comments on commit 0741cd4

Please sign in to comment.