package gust import ( "fmt" "strconv" "strings" "time" "unicode/utf8" "github.com/charmbracelet/lipgloss" "github.com/muesli/reflow/wordwrap" ) func (m Model) View() string { var b strings.Builder b.WriteString(m.viewStatus()) m.viewport.SetContent(m.viewBody()) b.WriteString(m.viewport.View()) b.WriteString("\n") b.WriteString(m.viewHelp()) return b.String() } func (m Model) viewBody() string { style := m.config.Styles sign := m.config.Signs var b strings.Builder if m.config.Mode == "build" || m.end != nil && m.process.cmd.ProcessState.ExitCode() != 0 { b.WriteString(m.viewBuild()) } else { b.WriteString(m.viewRun()) } for _, o := range m.extra { b.WriteString(style.Error.Render(sign.System)) b.WriteString(" ") b.WriteString(o) b.WriteString("\n") } for _, e := range m.systemErrors { b.WriteString(style.Error.Render(sign.System)) b.WriteString(" ") b.WriteString(e.Error()) b.WriteString("\n") } b.WriteString("\n") return b.String() } func (m Model) viewBuild() string { var b strings.Builder maxLineLen := 1 maxColLen := 1 style := m.config.Styles sign := m.config.Signs for _, msg := range m.messages { lineLen := len(msg.Location.Line) colLen := len(msg.Location.Column) if lineLen > maxLineLen { maxLineLen = lineLen } if colLen > maxColLen { maxColLen = colLen } } for _, msg := range m.messages { var mb strings.Builder if msg.Type == "error" { mb.WriteString(style.Error.Render(sign.Error)) mb.WriteString(" ") } else { mb.WriteString(style.Warning.Render(sign.Warning)) mb.WriteString(" ") } if msg.Location.Line != "" { mb.WriteString(style.Line.Align(lipgloss.Right).Width(maxLineLen).Render(msg.Location.Line)) if msg.Location.Column != "" { mb.WriteString(":") mb.WriteString(style.Line.Align(lipgloss.Left).Width(maxColLen).Render(msg.Location.Column)) } } if msg.Location.File != "" { mb.WriteString(" ") mb.WriteString(style.File.Render(msg.Location.File)) mb.WriteString(" ") } mb.WriteString(msg.Message) b.WriteString(wordwrap.String(mb.String(), m.width)) // display context if !m.config.Summarized { b.WriteString("\n") start := msg.Context.Start() style := style.Context for offset, line := range msg.Context.Ordered() { nr := fmt.Sprintf("%d", start+offset) nrStyle := style.PassiveLineNr lineStyle := style.PassiveLine indent := sign.PassiveIndent active := false if msg.Location.Line == nr { active = true nrStyle = style.ActiveLineNr lineStyle = style.ActiveLine indent = sign.ActiveIndent } b.WriteString(nrStyle.Render(indent)) b.WriteString(nrStyle.Align(lipgloss.Right).Render(nr)) b.WriteString(" ") simplifiedLine := strings.ReplaceAll(line, "\t", " ") b.WriteString(lineStyle.Render(simplifiedLine)) // makes this easier to reason about if active && msg.Location.Column != "" { col, _ := strconv.Atoi(msg.Location.Column) // special logic to handle tab characters in the target line tabCount := strings.Count(line[:col-1], "\t") b.WriteString("\n") b.WriteString(style.PassiveLineNr.Render(sign.PassiveIndent)) leftPadding := col + // current column to place the pointer at 3*tabCount + // if the line has tabs, we convert to spaces and offset len(msg.Location.Line) + // we always print line-numbers, so offset by that too 1 // there is a single space between line-number and line content b.WriteString(style.ActiveLineNr.Width(leftPadding).Align(lipgloss.Right).Render("^")) } b.WriteString("\n") } } b.WriteString("\n") } return b.String() } func (m Model) viewRun() string { var b strings.Builder if m.config.Stream == "stderr" { b.WriteString(m.stderr()) } else { b.WriteString(m.stdout()) } return b.String() } func (m Model) viewStatus() string { var b strings.Builder errorCount := 0 warningCount := 0 for _, msg := range m.messages { if msg.Type == "error" { errorCount++ } else if msg.Type == "warning" { warningCount++ } } style := m.config.Styles sign := m.config.Signs // write mode b.WriteString(style.Mode.Render(m.config.Mode)) // write status statusStyle := style.Success switch m.status.Kind { case Running, Initializing: statusStyle = style.Running case Error: statusStyle = style.Error } if len(m.messages) == 0 { b.WriteString(style.Separator.Render(sign.Separator)) b.WriteString(statusStyle.Render(m.status.View())) } plural := func(s string, n int) string { if n == 1 { return s } else { return fmt.Sprintf("%ss", s) } } // write message summary if errorCount != 0 { b.WriteString(style.Separator.Render(sign.Separator)) b.WriteString(style.Error.Render(fmt.Sprintf("%d %s", errorCount, plural("error", errorCount)))) } if warningCount != 0 { b.WriteString(style.Separator.Render(sign.Separator)) b.WriteString(style.Warning.Render(fmt.Sprintf("%d %s", warningCount, plural("warning", warningCount)))) } b.WriteString(style.Separator.Render(sign.Separator)) if m.start != nil && m.end != nil { b.WriteString(style.Duration.Render(m.end.Sub(*m.start).String())) } else if m.start != nil && m.end == nil { b.WriteString(style.Duration.Render(time.Now().Sub(*m.start).String())) } b.WriteString(" ") line := strings.Repeat(sign.HorizontalBar, max(0, (m.viewport.Width-lipgloss.Width(b.String()))/utf8.RuneCountInString(sign.HorizontalBar))) b.WriteString(style.Separator.Render(line)) b.WriteString("\n") return b.String() } func (m Model) viewHelp() string { if !m.config.Help { return "" } var b strings.Builder style := m.config.Styles sign := m.config.Signs b.WriteString("s ") if m.config.Summarized { b.WriteString(style.Separator.Render("show context")) } else { b.WriteString(style.Separator.Render("hide context")) } b.WriteString(style.Separator.Render(sign.Separator)) if m.config.Mode != "build" { b.WriteString("b ") b.WriteString(style.Separator.Render("build")) b.WriteString(style.Separator.Render(sign.Separator)) b.WriteString("o ") if m.config.Stream != "stdout" { b.WriteString(style.Separator.Render("stdout")) b.WriteString(style.Separator.Render(sign.Separator)) } if m.config.Stream != "stderr" { b.WriteString(style.Separator.Render("stderr")) b.WriteString(style.Separator.Render(sign.Separator)) } } if m.config.Mode != "run" { b.WriteString("r ") b.WriteString(style.Separator.Render("run")) b.WriteString(style.Separator.Render(sign.Separator)) } b.WriteString("q ") b.WriteString(style.Separator.Render("quit")) b.WriteString(style.Separator.Render(sign.Separator)) b.WriteString("h/? ") b.WriteString(style.Separator.Render("hide help")) b.WriteString(" ") var rightStatus strings.Builder rightStatus.WriteString(" ") if m.config.Mode == "run" { if m.config.Stream == "stderr" { rightStatus.WriteString(style.Stderr.Render("stderr")) } else { rightStatus.WriteString(style.Stdout.Render("stdout")) } rightStatus.WriteString(style.Separator.Render(sign.Separator)) } rightStatus.WriteString(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) right := rightStatus.String() line := strings.Repeat(sign.HorizontalBar, max(0, (m.viewport.Width-lipgloss.Width(b.String())-lipgloss.Width(right))/utf8.RuneCountInString(sign.HorizontalBar))) b.WriteString(style.Separator.Render(line)) b.WriteString(style.Duration.Render(right)) return lipgloss.NewStyle().Render(b.String()) }