Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions cmd/terminal-to-html/terminal-to-html.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func webservice(listen string, preview bool, screen *terminal.Screen) {
// > Request.Body.
// However, it lets us provide Content-Length in all cases.
b := bytes.NewBuffer(nil)
if _, _, err := process(b, r.Body, preview, &screen); err != nil {
if _, _, err := process(b, r.Body, preview, "html", false, &screen); err != nil {
log.Printf("error starting preview: %v", err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Error creating preview.")
Expand Down Expand Up @@ -180,9 +180,8 @@ func (wc *writeCounter) Write(b []byte) (int, error) {

func (wc *writeCounter) WriteString(s string) { wc.Write([]byte(s)) }

// process streams the src through a terminal renderer to the dst. If preview is
// true, the preview wrapper is added.
func process(dst io.Writer, src io.Reader, preview bool, screen *terminal.Screen) (in, out int, err error) {
// process streams the src through a terminal renderer to the dst.
func process(dst io.Writer, src io.Reader, preview bool, format string, timestamps bool, screen *terminal.Screen) (in, out int, err error) {
// Wrap dst in writeCounter to count bytes written
wc := &writeCounter{out: dst}

Expand All @@ -193,7 +192,11 @@ func process(dst io.Writer, src io.Reader, preview bool, screen *terminal.Screen
}

// Attach the scrollout callback before streaming input.
screen.ScrollOutFunc = wc.WriteString
// Note: ScrollOutFunc always outputs HTML. For plain text format,
// streaming is not supported - use buffer-max-lines=0 to disable streaming.
if format == "html" {
screen.ScrollOutFunc = wc.WriteString
}

inBytes, err := io.Copy(screen, src)
if err != nil {
Expand All @@ -202,7 +205,11 @@ func process(dst io.Writer, src io.Reader, preview bool, screen *terminal.Screen

// Write what remains in the screen buffer (everything that didn't scroll
// out of the top).
wc.WriteString(screen.AsHTML())
if format == "plain" {
wc.WriteString(screen.AsPlainTextWithTimestamps(timestamps))
} else {
wc.WriteString(screen.AsHTML())
}

if preview {
if err := writePreviewEnd(wc); err != nil {
Expand Down Expand Up @@ -230,6 +237,15 @@ func main() {
Name: "preview",
Usage: "wrap output in HTML & CSS so it can be easily viewed directly in a browser",
},
&cli.StringFlag{
Name: "format",
Value: "html",
Usage: "output format: 'html' (default) or 'plain' for plain text",
},
&cli.BoolFlag{
Name: "timestamps",
Usage: "include UTC timestamps in plain text output (only applies when --format=plain)",
},
&cli.BoolFlag{
Name: "log-stats-to-stderr",
Usage: "Logs a JSON object to stderr containing resource and processing statistics after successfully processing",
Expand All @@ -256,6 +272,12 @@ func main() {
},
}
app.Action = func(c *cli.Context) error {
// Validate format flag
format := c.String("format")
if format != "html" && format != "plain" {
return fmt.Errorf("invalid format %q: must be 'html' or 'plain'", format)
}

screen, err := terminal.NewScreen(
terminal.WithMaxSize(c.Int("window-max-cols"), c.Int("buffer-max-lines")),
terminal.WithSize(c.Int("window-cols"), c.Int("window-lines")),
Expand Down Expand Up @@ -283,7 +305,7 @@ func main() {
input = f
}

in, out, err := process(os.Stdout, input, c.Bool("preview"), screen)
in, out, err := process(os.Stdout, input, c.Bool("preview"), format, c.Bool("timestamps"), screen)
if err != nil {
return err
}
Expand Down
38 changes: 38 additions & 0 deletions output.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,41 @@ func (l *screenLine) asPlain() string {
}
return line
}

// lineToPlain joins parts of a line together and renders them as plain text,
// optionally with a UTC timestamp prefix. The output string will have a
// terminating \n.
func lineToPlain(parts []screenLine, timestamps bool) string {
var buf strings.Builder

if timestamps {
// Combine metadata - last metadata wins.
bkmd := make(map[string]string)
for _, l := range parts {
maps.Copy(bkmd, l.metadata[bkNamespace])
}
if t, ok := bkmd["t"]; ok {
if millis, err := strconv.ParseInt(t, 10, 64); err == nil {
ts := time.Unix(millis/1000, (millis%1000)*1_000_000).UTC()
buf.WriteString(ts.Format("2006-01-02T15:04:05Z"))
buf.WriteString(" ")
}
}
}

// Render the text content.
for _, l := range parts {
for _, node := range l.nodes {
if !node.style.element() {
buf.WriteRune(node.blob)
}
}
}

line := buf.String()
line = strings.TrimRight(line, " \t")
if line == "" && !timestamps {
return "\n"
}
return line + "\n"
}
20 changes: 20 additions & 0 deletions screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,26 @@ func (s *Screen) AsPlainText() string {
return strings.TrimSuffix(sb.String(), "\n")
}

// AsPlainTextWithTimestamps renders the screen as plain text, optionally
// with UTC timestamp prefixes.
func (s *Screen) AsPlainTextWithTimestamps(timestamps bool) string {
if !timestamps {
return s.AsPlainText()
}

var sb strings.Builder
for i := 0; i < len(s.screen); {
// Find the end of this logical line.
lineEnd := i + 1
for lineEnd < len(s.screen) && !s.screen[lineEnd-1].newline {
lineEnd++
}
sb.WriteString(lineToPlain(s.screen[i:lineEnd], true))
i = lineEnd
}
return strings.TrimSuffix(sb.String(), "\n")
}

func (s *Screen) newLine() {
// Do the carriage return first to ensure that currentLineForWriting can't
// give us the next line if the cursor was placed past the end of the line.
Expand Down