Skip to content

Create UI object for interacting with terminal #13

@Polpetta

Description

@Polpetta

We cannot directly interact with the TTY using fmt.Println because this makes the program difficult to test and difficult to change the output render if we ever need it. We have to create an abstraction layer (like UI object) where we can change stuff in the future if we need it.

I attached a draft of module we could use to perform such output to the user.

package main

import (
	"github.com/charmbracelet/glamour"
	"io"
	"os"
)

// StdOutput is a shorthand for io.Writer
type StdOutput io.Writer

// StdError is a shorthand for io.Writer
type StdError io.Writer

// LogLevel indicates the verbosity of what's going to be printed on screen
type LogLevel uint8

// UIOptions is a typedef for setting additional options to the UI
type UIOptions func(*UI, string) string

const (
	// DEBUG will print debug information on screen
	DEBUG LogLevel = iota
	// VERBOSE will print almost everything
	VERBOSE
	// NORMAL is for output that is printed without any particular flag
	NORMAL
	// QUIET only prints warnings (error will always be printed on StdError)
	QUIET
)

// UI is the final endpoint for writing output to the final user. It represents a TTY, and it has functionalities for rendering the output even in a markdown flavoured fashion
type UI struct {
	output StdOutput
	error  StdError
	term   *glamour.TermRenderer
}

// WithMarkdownSyntax is an UIOptions to print the content in a markdown rendered output
func WithMarkdownSyntax(ui *UI, content string) string {
	render, err := ui.term.Render(content)
	if err != nil {
		panic(err)
	}
	return render
}

func (U *UI) out(level LogLevel, s string, opts ...UIOptions) (n int, err error) {
	// FIXME loglevel
	out := s
	for _, opt := range opts {
		out = opt(U, s)
	}
	return U.output.Write([]byte(out))
}

func (U *UI) markdownOut(s string) (n int, err error) {
	render, err := U.term.Render(s)
	if err != nil {
		return 0, err
	}
	return U.output.Write([]byte(render))
}

func (U *UI) err(s string) (n int, err error) {
	return U.error.Write([]byte(s))
}

// NewStdOut returns os.Stdout
func NewStdOut() StdOutput {
	return os.Stdout
}

// NewStdErr returns os.Stderr
func NewStdErr() StdError {
	return os.Stderr
}

// NewUI creates a brand-new UI struct
func NewUI(out StdOutput, errorOut StdError) *UI {
	glamourTermRender, err := glamour.NewTermRenderer(
		glamour.WithAutoStyle(),
		glamour.WithWordWrap(80),
		glamour.WithEmoji(),
	)

	if err != nil {
		panic("Impossible to create terminal. Is this a valid TTY?")
	}

	return &UI{
		output: out,
		error:  errorOut,
		term:   glamourTermRender,
	}
}

This means we have to check out all the fmt.Print{ln}... in the codebase and rewrite it using this object.
Combined with #11 we can easily have this object injected in all the classes we need to output stuff without ever bothering about initializing one "by hand"

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions