lmcli/pkg/cli/util.go
Matt Low 239ded18f3 Add edit command
Various refactoring:
- reduced repetition with conversation message handling
- made some functions internal
2024-01-02 04:31:21 +00:00

174 lines
4.3 KiB
Go

package cli
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"reflect"
"strconv"
"strings"
"time"
)
// InputFromEditor retrieves user input by opening an editor (one specified by
// $EDITOR or 'vim' if $EDITOR is not set) on a temporary file. Once the editor
// closes, the contents of the file are read and the file is deleted. If the
// contents of the file exactly match the value of placeholder (no edits to the
// file were made), then an empty string is returned. Otherwise, the contents
// are returned. Example patten: message.*.md
func InputFromEditor(placeholder string, pattern string, content string) (string, error) {
msgFile, _ := os.CreateTemp("/tmp", pattern)
defer os.Remove(msgFile.Name())
os.WriteFile(msgFile.Name(), []byte(placeholder + content), os.ModeAppend)
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vim" // default to vim if no EDITOR env variable
}
execCmd := exec.Command(editor, msgFile.Name())
execCmd.Stdin = os.Stdin
execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr
if err := execCmd.Run(); err != nil {
return "", err
}
bytes, _ := os.ReadFile(msgFile.Name())
content = string(bytes)
if placeholder != "" {
if content == placeholder {
return "", nil
}
// strip placeholder if content begins with it
if strings.HasPrefix(content, placeholder) {
content = content[len(placeholder):]
}
}
return strings.Trim(content, "\n \t"), nil
}
// humanTimeElapsedSince returns a human-friendly "in the past" representation
// of the given duration.
func humanTimeElapsedSince(d time.Duration) string {
seconds := d.Seconds()
minutes := seconds / 60
hours := minutes / 60
days := hours / 24
weeks := days / 7
months := days / 30
years := days / 365
switch {
case seconds < 60:
return "seconds ago"
case minutes < 2:
return "1 minute ago"
case minutes < 60:
return fmt.Sprintf("%d minutes ago", int64(minutes))
case hours < 2:
return "1 hour ago"
case hours < 24:
return fmt.Sprintf("%d hours ago", int64(hours))
case days < 2:
return "1 day ago"
case days < 7:
return fmt.Sprintf("%d days ago", int64(days))
case weeks < 2:
return "1 week ago"
case weeks <= 4:
return fmt.Sprintf("%d weeks ago", int64(weeks))
case months < 2:
return "1 month ago"
case months < 12:
return fmt.Sprintf("%d months ago", int64(months))
case years < 2:
return "1 year ago"
default:
return fmt.Sprintf("%d years ago", int64(years))
}
}
// SetStructDefaultValues checks for any nil ptr fields within the passed
// struct, and sets the values of those fields to the value that is defined by
// their "default" struct tag. Handles setting string, int, and bool values.
// Returns whether any changes were made to the struct.
func SetStructDefaults(data interface{}) bool {
v := reflect.ValueOf(data).Elem()
changed := false
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
// Check if we can set the field's value
if !field.CanSet() {
continue
}
// We won't bother with non-pointer fields
if field.Kind() != reflect.Ptr {
continue
}
t := field.Type() // type of pointer
e := t.Elem() // type of value of pointer
// Handle nested structs recursively
if e.Kind() == reflect.Struct {
if field.IsNil() {
field.Set(reflect.New(e))
changed = true
}
result := SetStructDefaults(field.Interface())
if result {
changed = true
}
continue
}
if !field.IsNil() {
continue
}
// Get the "default" struct tag
defaultTag := v.Type().Field(i).Tag.Get("default")
if defaultTag == "" {
continue
}
// Set nil pointer fields to their defined defaults
switch e.Kind() {
case reflect.String:
defaultValue := defaultTag
field.Set(reflect.ValueOf(&defaultValue))
case reflect.Int, reflect.Int32, reflect.Int64:
intValue, _ := strconv.ParseInt(defaultTag, 10, 64)
field.Set(reflect.New(e))
field.Elem().SetInt(intValue)
case reflect.Bool:
boolValue := defaultTag == "true"
field.Set(reflect.ValueOf(&boolValue))
}
changed = true
}
return changed
}
// FileContents returns the string contents of the given file.
// TODO: we should support retrieving the content (or an approximation of)
// non-text documents, e.g. PDFs.
func FileContents(file string) (string, error) {
path := filepath.Clean(file)
content, err := os.ReadFile(path)
if err != nil {
return "", err
}
return strings.Trim(string(content), "\n\t "), nil
}