package tui import ( "os" "os/exec" "strings" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/muesli/reflow/ansi" ) type msgTempfileEditorClosed string // openTempfileEditor opens an $EDITOR on a new temporary file with the given // content. Upon closing, the contents of the file are read back returned // wrapped in a msgTempfileEditorClosed returned by the tea.Cmd func openTempfileEditor(pattern string, content string, placeholder string) tea.Cmd { msgFile, _ := os.CreateTemp("/tmp", pattern) err := os.WriteFile(msgFile.Name(), []byte(placeholder+content), os.ModeAppend) if err != nil { return wrapError(err) } editor := os.Getenv("EDITOR") if editor == "" { editor = "vim" } c := exec.Command(editor, msgFile.Name()) return tea.ExecProcess(c, func(err error) tea.Msg { bytes, err := os.ReadFile(msgFile.Name()) if err != nil { return msgError(err) } os.Remove(msgFile.Name()) fileContents := string(bytes) if strings.HasPrefix(fileContents, placeholder) { fileContents = fileContents[len(placeholder):] } stripped := strings.Trim(fileContents, "\n \t") return msgTempfileEditorClosed(stripped) }) } // similar to lipgloss.Height, except returns 0 on empty strings func height(str string) int { if str == "" { return 0 } return strings.Count(str, "\n") + 1 } // truncate a string until its rendered cell width + the provided tail fits // within the given width func truncateToCellWidth(str string, width int, tail string) string { cellWidth := ansi.PrintableRuneWidth(str) if cellWidth <= width { return str } tailWidth := ansi.PrintableRuneWidth(tail) for { str = str[:len(str)-((cellWidth+tailWidth)-width)] cellWidth = ansi.PrintableRuneWidth(str) if cellWidth+tailWidth <= max(width, 0) { break } } return str + tail } // fraction is the fraction of the total screen height into view the offset // should be scrolled into view. 0.5 = items will be snapped to middle of // view func scrollIntoView(vp *viewport.Model, offset int, fraction float32) { currentOffset := vp.YOffset if offset >= currentOffset && offset < currentOffset+vp.Height { return } distance := currentOffset - offset if distance < 0 { // we should scroll down until it just comes into view vp.SetYOffset(currentOffset - (distance + (vp.Height - int(float32(vp.Height)*fraction))) + 1) } else { // we should scroll up vp.SetYOffset(currentOffset - distance - int(float32(vp.Height)*fraction)) } }