Skip to content
Merged

Misc #278

Show file tree
Hide file tree
Changes from all commits
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
73 changes: 73 additions & 0 deletions mdl/repl/color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: Apache-2.0

package repl

import (
"os"

"golang.org/x/term"
)

// ANSI escape codes.
const (
ansiReset = "\033[0m"
ansiBold = "\033[1m"
ansiBoldCyan = "\033[1;36m"
ansiRed = "\033[31m"
ansiGray = "\033[90m"
)

// rlIgnoreStart / rlIgnoreEnd bracket invisible characters in readline prompts
// so readline counts the display width correctly.
const (
rlIgnoreStart = "\001"
rlIgnoreEnd = "\002"
)

// colorPalette applies ANSI colors when stdout is a real TTY and NO_COLOR is unset.
type colorPalette struct {
enabled bool
}

// newColorPalette returns a palette active only when stdout is a terminal and
// the NO_COLOR environment variable is absent.
func newColorPalette() colorPalette {
if os.Getenv("NO_COLOR") != "" {
return colorPalette{}
}
return colorPalette{enabled: term.IsTerminal(int(os.Stdout.Fd()))}
}

func (c colorPalette) wrap(code, s string) string {
if !c.enabled {
return s
}
return code + s + ansiReset
}

// Bold returns s in bold.
func (c colorPalette) Bold(s string) string { return c.wrap(ansiBold, s) }

// Red returns s in red (used for errors).
func (c colorPalette) Red(s string) string { return c.wrap(ansiRed, s) }

// Gray returns s in dark gray (used for secondary text).
func (c colorPalette) Gray(s string) string { return c.wrap(ansiGray, s) }

// PromptPrimary returns a readline-safe bold-cyan prompt string.
// The \001…\002 markers tell readline not to count the escape bytes in the
// visible line width, preventing cursor positioning bugs.
func (c colorPalette) PromptPrimary(s string) string {
if !c.enabled {
return s
}
return rlIgnoreStart + ansiBoldCyan + rlIgnoreEnd + s + rlIgnoreStart + ansiReset + rlIgnoreEnd
}

// PromptContinue returns a readline-safe gray continuation prompt string.
func (c colorPalette) PromptContinue(s string) string {
if !c.enabled {
return s
}
return rlIgnoreStart + ansiGray + rlIgnoreEnd + s + rlIgnoreStart + ansiReset + rlIgnoreEnd
}
21 changes: 12 additions & 9 deletions mdl/repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type REPL struct {
prompt string
rl *readline.Instance
logger *diaglog.Logger
c colorPalette
}

// SetLogger sets the diagnostics logger for the REPL and its executor.
Expand All @@ -41,11 +42,13 @@ func (r *REPL) SetLogger(l *diaglog.Logger) {
func New(input io.Reader, output io.Writer) *REPL {
exec := executor.New(output)
exec.SetBackendFactory(func() backend.FullBackend { return mprbackend.New() })
c := newColorPalette()
return &REPL{
executor: exec,
input: input,
output: output,
prompt: "mdl> ",
c: c,
}
}

Expand Down Expand Up @@ -80,7 +83,7 @@ func (r *REPL) Run() error {
if errors.Is(err, executor.ErrExit) {
return nil
}
fmt.Fprintf(r.output, "Error: %v\n", err)
fmt.Fprintf(r.output, "%s\n", r.c.Red("Error: "+err.Error()))
}
buffer.Reset()
}
Expand All @@ -92,7 +95,7 @@ func (r *REPL) Run() error {
if strings.TrimSpace(input) != "" {
err := r.execute(input)
if err != nil && !errors.Is(err, executor.ErrExit) {
fmt.Fprintf(r.output, "Error: %v\n", err)
fmt.Fprintf(r.output, "%s\n", r.c.Red("Error: "+err.Error()))
}
}
}
Expand Down Expand Up @@ -130,17 +133,17 @@ func (r *REPL) RunWithReadline() error {

var buffer strings.Builder

fmt.Fprintln(r.output, "MDL REPL - Mendix Definition Language")
fmt.Fprintln(r.output, "Type 'help' or '?' for commands, 'exit' or 'quit' to quit")
fmt.Fprintln(r.output, "Tab: autocomplete, ↑↓: history, Ctrl+R: search history")
fmt.Fprintln(r.output, r.c.Bold("MDL REPL")+" — Mendix Definition Language")
fmt.Fprintln(r.output, r.c.Gray("Type 'help' or '?' for commands, 'exit' or 'quit' to quit"))
fmt.Fprintln(r.output, r.c.Gray("Tab: autocomplete, ↑↓: history, Ctrl+R: search history"))
fmt.Fprintln(r.output)

for {
// Set prompt based on whether we're continuing a multi-line statement
if buffer.Len() == 0 {
rl.SetPrompt(r.prompt)
rl.SetPrompt(r.c.PromptPrimary(r.prompt))
} else {
rl.SetPrompt("...> ")
rl.SetPrompt(r.c.PromptContinue("...> "))
}

// Read line with readline (supports history, arrow keys, etc.)
Expand Down Expand Up @@ -189,7 +192,7 @@ func (r *REPL) RunWithReadline() error {
fmt.Fprintln(r.output, "Goodbye!")
return nil
}
fmt.Fprintf(r.output, "Error: %v\n", err)
fmt.Fprintf(r.output, "%s\n", r.c.Red("Error: "+err.Error()))
}
buffer.Reset()
}
Expand Down Expand Up @@ -218,7 +221,7 @@ func (r *REPL) execute(input string) error {
prog, errs := visitor.Build(input)
if len(errs) > 0 {
for _, err := range errs {
fmt.Fprintf(r.output, "Parse error: %v\n", err)
fmt.Fprintf(r.output, "%s\n", r.c.Red("Parse error: "+err.Error()))
}
r.logger.ParseError(input, errs)
return nil // Don't return error, just print and continue
Expand Down
Loading