Skip to content

Commit 5097e46

Browse files
authored
Merge pull request #278 from mendixlabs/misc
Misc
2 parents a2bc5b3 + 7142b1c commit 5097e46

2 files changed

Lines changed: 85 additions & 9 deletions

File tree

mdl/repl/color.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package repl
4+
5+
import (
6+
"os"
7+
8+
"golang.org/x/term"
9+
)
10+
11+
// ANSI escape codes.
12+
const (
13+
ansiReset = "\033[0m"
14+
ansiBold = "\033[1m"
15+
ansiBoldCyan = "\033[1;36m"
16+
ansiRed = "\033[31m"
17+
ansiGray = "\033[90m"
18+
)
19+
20+
// rlIgnoreStart / rlIgnoreEnd bracket invisible characters in readline prompts
21+
// so readline counts the display width correctly.
22+
const (
23+
rlIgnoreStart = "\001"
24+
rlIgnoreEnd = "\002"
25+
)
26+
27+
// colorPalette applies ANSI colors when stdout is a real TTY and NO_COLOR is unset.
28+
type colorPalette struct {
29+
enabled bool
30+
}
31+
32+
// newColorPalette returns a palette active only when stdout is a terminal and
33+
// the NO_COLOR environment variable is absent.
34+
func newColorPalette() colorPalette {
35+
if os.Getenv("NO_COLOR") != "" {
36+
return colorPalette{}
37+
}
38+
return colorPalette{enabled: term.IsTerminal(int(os.Stdout.Fd()))}
39+
}
40+
41+
func (c colorPalette) wrap(code, s string) string {
42+
if !c.enabled {
43+
return s
44+
}
45+
return code + s + ansiReset
46+
}
47+
48+
// Bold returns s in bold.
49+
func (c colorPalette) Bold(s string) string { return c.wrap(ansiBold, s) }
50+
51+
// Red returns s in red (used for errors).
52+
func (c colorPalette) Red(s string) string { return c.wrap(ansiRed, s) }
53+
54+
// Gray returns s in dark gray (used for secondary text).
55+
func (c colorPalette) Gray(s string) string { return c.wrap(ansiGray, s) }
56+
57+
// PromptPrimary returns a readline-safe bold-cyan prompt string.
58+
// The \001…\002 markers tell readline not to count the escape bytes in the
59+
// visible line width, preventing cursor positioning bugs.
60+
func (c colorPalette) PromptPrimary(s string) string {
61+
if !c.enabled {
62+
return s
63+
}
64+
return rlIgnoreStart + ansiBoldCyan + rlIgnoreEnd + s + rlIgnoreStart + ansiReset + rlIgnoreEnd
65+
}
66+
67+
// PromptContinue returns a readline-safe gray continuation prompt string.
68+
func (c colorPalette) PromptContinue(s string) string {
69+
if !c.enabled {
70+
return s
71+
}
72+
return rlIgnoreStart + ansiGray + rlIgnoreEnd + s + rlIgnoreStart + ansiReset + rlIgnoreEnd
73+
}

mdl/repl/repl.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type REPL struct {
2929
prompt string
3030
rl *readline.Instance
3131
logger *diaglog.Logger
32+
c colorPalette
3233
}
3334

3435
// SetLogger sets the diagnostics logger for the REPL and its executor.
@@ -41,11 +42,13 @@ func (r *REPL) SetLogger(l *diaglog.Logger) {
4142
func New(input io.Reader, output io.Writer) *REPL {
4243
exec := executor.New(output)
4344
exec.SetBackendFactory(func() backend.FullBackend { return mprbackend.New() })
45+
c := newColorPalette()
4446
return &REPL{
4547
executor: exec,
4648
input: input,
4749
output: output,
4850
prompt: "mdl> ",
51+
c: c,
4952
}
5053
}
5154

@@ -80,7 +83,7 @@ func (r *REPL) Run() error {
8083
if errors.Is(err, executor.ErrExit) {
8184
return nil
8285
}
83-
fmt.Fprintf(r.output, "Error: %v\n", err)
86+
fmt.Fprintf(r.output, "%s\n", r.c.Red("Error: "+err.Error()))
8487
}
8588
buffer.Reset()
8689
}
@@ -92,7 +95,7 @@ func (r *REPL) Run() error {
9295
if strings.TrimSpace(input) != "" {
9396
err := r.execute(input)
9497
if err != nil && !errors.Is(err, executor.ErrExit) {
95-
fmt.Fprintf(r.output, "Error: %v\n", err)
98+
fmt.Fprintf(r.output, "%s\n", r.c.Red("Error: "+err.Error()))
9699
}
97100
}
98101
}
@@ -130,17 +133,17 @@ func (r *REPL) RunWithReadline() error {
130133

131134
var buffer strings.Builder
132135

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

138141
for {
139142
// Set prompt based on whether we're continuing a multi-line statement
140143
if buffer.Len() == 0 {
141-
rl.SetPrompt(r.prompt)
144+
rl.SetPrompt(r.c.PromptPrimary(r.prompt))
142145
} else {
143-
rl.SetPrompt("...> ")
146+
rl.SetPrompt(r.c.PromptContinue("...> "))
144147
}
145148

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

0 commit comments

Comments
 (0)