package util import ( "fmt" "os" "os/exec" "strings" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "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 func() tea.Msg { return 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 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, edge int) { 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 - edge)) + 1) } else { // we should scroll up vp.SetYOffset(currentOffset - distance - edge) } } func ErrorBanner(err error, width int) string { if err == nil { return "" } return lipgloss.NewStyle(). Width(width). AlignHorizontal(lipgloss.Center). Bold(true). Foreground(lipgloss.Color("1")). Render(fmt.Sprintf("%s", err)) }