func (handler *VerticalUI) Close() {
	if handler.config.Options.ShowSummaryFooter {
		message := ""
		handler.frame.Footer().Open()
		if len(handler.runtimeData.Failed) > 0 {
			if handler.config.Options.LogPath != "" {
				message = utils.Bold(" See log for details (" + handler.config.Options.LogPath + ")")
			}
			renderedFooter := handler.footer(runtime.StatusError, message)
			handler.frame.Footer().WriteStringAndClose(renderedFooter)
		} else {
			renderedFooter := handler.footer(runtime.StatusSuccess, message)
			handler.frame.Footer().WriteStringAndClose(renderedFooter)
		}
		handler.frame.Footer().Close()
	}
	handler.frame.Close()
}
todo: move footer logic based on jotframe requirements
package handler
import (
	"bytes"
	"fmt"
	"github.com/google/uuid"
	color "github.com/mgutz/ansi"
	"github.com/tj/go-spin"
	"github.com/wagoodman/bashful/pkg/config"
	"github.com/wagoodman/bashful/pkg/runtime"
	"github.com/wagoodman/bashful/utils"
	"github.com/wagoodman/jotframe"
	"github.com/wayneashleyberry/terminal-dimensions"
	"io"
	"strconv"
	"strings"
	"sync"
	"text/template"
	"time"
)
type VerticalUI struct {
	lock        sync.Mutex
	config      *config.Config
	data        map[uuid.UUID]*display
	spinner     *spin.Spinner
	ticker      *time.Ticker
	startTime   time.Time
	runtimeData *runtime.TaskStatistics
	frame       *jotframe.FixedFrame
}
type display struct {
	Task *runtime.Task
	Template *template.Template
	Index int
	Values lineInfo
	line *jotframe.Line
}
type summary struct {
	Status  string
	Percent string
	Msg     string
	Runtime string
	Eta     string
	Split   string
	Steps   string
	Errors  string
}
type lineInfo struct {
	Status string
	Title string
	Msg string
	Prefix string
	Eta string
	Split string
}
var (
	summaryTemplate, _ = template.New("summary line").Parse(` {{.Status}}    ` + color.Reset + ` {{printf "%-16s" .Percent}}` + color.Reset + ` {{.Steps}}{{.Errors}}{{.Msg}}{{.Split}}{{.Runtime}}{{.Eta}}`)
	lineDefaultTemplate, _ = template.New("default line").Parse(` {{.Status}}  ` + color.Reset + ` {{printf "%1s" .Prefix}} {{printf "%-25s" .Title}} {{.Msg}}{{.Split}}{{.Eta}}`)
	lineParallelTemplate, _ = template.New("parallel line").Parse(` {{.Status}}  ` + color.Reset + ` {{printf "%1s" .Prefix}} ├─ {{printf "%-25s" .Title}} {{.Msg}}{{.Split}}{{.Eta}}`)
	lineLastParallelTemplate, _ = template.New("last parallel line").Parse(` {{.Status}}  ` + color.Reset + ` {{printf "%1s" .Prefix}} └─ {{printf "%-25s" .Title}} {{.Msg}}{{.Split}}{{.Eta}}`)
)
func NewVerticalUI(cfg *config.Config) *VerticalUI {
	updateInterval := 150 * time.Millisecond
	if cfg.Options.UpdateInterval > 150 {
		updateInterval = time.Duration(cfg.Options.UpdateInterval) * time.Millisecond
	}
	handler := &VerticalUI{
		data:      make(map[uuid.UUID]*display, 0),
		spinner:   spin.New(),
		ticker:    time.NewTicker(updateInterval),
		startTime: time.Now(),
		config:    cfg,
	}
	go handler.spinnerHandler()
	return handler
}
func (handler *VerticalUI) AddRuntimeData(data *runtime.TaskStatistics) {
	handler.runtimeData = data
}
func (handler *VerticalUI) spinnerHandler() {
	for {
		select {
		case <-handler.ticker.C:
			handler.lock.Lock()
			handler.spinner.Next()
			for _, displayData := range handler.data {
				task := displayData.Task
				if task.Config.CmdString != "" {
					if !task.Completed && task.Started {
						displayData.Values.Prefix = handler.spinner.Current()
						displayData.Values.Eta = handler.CurrentEta(task)
					}
					handler.displayTask(task)
				}
				for _, subTask := range task.Children {
					childDisplayData := handler.data[subTask.Id]
					if !subTask.Completed && subTask.Started {
						childDisplayData.Values.Prefix = handler.spinner.Current()
						childDisplayData.Values.Eta = handler.CurrentEta(subTask)
					}
					handler.displayTask(subTask)
				}
				if handler.config.Options.ShowSummaryFooter {
					renderedFooter := handler.footer(runtime.StatusPending, "")
					io.WriteString(handler.frame.Footer(), renderedFooter)
				}
			}
			handler.lock.Unlock()
		}
	}
}