feat: Colorized console output.

This commit is contained in:
2026-06-19 08:02:53 -04:00
parent a03d388a0c
commit 33cd7c4eda
9 changed files with 194 additions and 52 deletions

View File

@@ -1,11 +1,13 @@
# TODOs # TODOs
1. Consider giving db its own allocator 1. envr scan crashes when there are zero results.
27. Commands are still leaking. 27. Commands are still leaking.
28. **db.odin** — Inconsistencies in how struct vs sqlite are named. 28. **db.odin** — Inconsistencies in how struct vs sqlite are named.
29. Add color flag and support non colored output.
2. Generate md and man pages again. 2. Generate md and man pages again.
3. **db.odin:324-327** — Map iteration (`remote_set`) is non-deterministic. Same file can produce different JSON on each backup, causing spurious DB diffs. Sort remotes before storing. 3. **db.odin:324-327** — Map iteration (`remote_set`) is non-deterministic. Same file can produce different JSON on each backup, causing spurious DB diffs. Sort remotes before storing.

View File

@@ -137,13 +137,38 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool {
return false return false
} }
fmt.wprintf(w, "Usage: %s [flags]\n\n", info.usage, flush = false) fmt.wprintf(
fmt.wprintf(w, "%s\n", info.short, flush = false) w,
"%s\n\n\n" +
COLOR_HEADINGS +
"Usage:" +
ANSI_RESET +
"\n\n " +
COLOR_FLAGS +
"%s" +
ANSI_RESET +
" [flags]\n\n",
info.short,
info.usage,
flush = false,
)
if len(info.aliases) > 0 { if len(info.aliases) > 0 {
fmt.wprintf(w, "\nAliases:\n %s", info.name, flush = false) fmt.wprintf(
w,
"\n" +
COLOR_HEADINGS +
"Aliases:" +
ANSI_RESET +
"\n\n " +
COLOR_COMMANDS +
"%s" +
ANSI_RESET,
info.name,
flush = false,
)
for a in info.aliases { for a in info.aliases {
fmt.wprintf(w, ", %s", a, flush = false) fmt.wprintf(w, ", " + COLOR_COMMANDS + "%s" + ANSI_RESET, a, flush = false)
} }
fmt.wprintf(w, "\n", flush = false) fmt.wprintf(w, "\n", flush = false)
} }
@@ -154,7 +179,20 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool {
fmt.wprintf( fmt.wprintf(
w, w,
"\nFlags:\n -h, --help help for %s\n -c, --config-file <path> config file (default \"~/.envr/config.json\")\n", "\n" +
COLOR_HEADINGS +
"Flags:" +
ANSI_RESET +
"\n\n " +
COLOR_FLAGS +
"-h, --help" +
ANSI_RESET +
" help for %s\n " +
COLOR_FLAGS +
"-c, --config-file" +
ANSI_RESET +
` <path> config file (default "~/.envr/config.json")
`,
info.name, info.name,
flush = false, flush = false,
) )
@@ -210,21 +248,29 @@ at before, restore your backup with:
> envr restore ~/<path to repository>/.env > envr restore ~/<path to repository>/.env
Usage: %sUsage:%s
envr [command]
Available Commands: %senvr%s [command]
%sAvailable Commands:%s
`, `,
COLOR_HEADINGS,
ANSI_RESET,
COLOR_FLAGS,
ANSI_RESET,
COLOR_HEADINGS,
ANSI_RESET,
flush = false, flush = false,
) )
for c in COMMANDS { for c in COMMANDS {
name_start := len(c.name) name_start := len(c.name)
fmt.wprintf(w, "%s", c.name, flush = false) fmt.wprintf(w, " %s%s", COLOR_COMMANDS, c.name, flush = false)
for a in c.aliases { for a in c.aliases {
fmt.wprintf(w, ", %s", a, flush = false) fmt.wprintf(w, ", %s", a, flush = false)
name_start += len(a) + 2 name_start += len(a) + 2
} }
fmt.wprint(w, ANSI_RESET)
padding := 20 - name_start padding := 20 - name_start
if padding > 0 { if padding > 0 {
for _ in 0 ..< padding { for _ in 0 ..< padding {
@@ -236,24 +282,32 @@ Available Commands:
fmt.wprintf( fmt.wprintf(
w, w,
` "\n" +
Flags: COLOR_HEADINGS +
-h, --help help for envr "Flags:" +
-c, --config-file <path> config file (default "~/.envr/config.json") ANSI_RESET +
"\n\n " +
COLOR_FLAGS +
"-h, --help" +
ANSI_RESET +
" help for envr\n" +
COLOR_FLAGS +
` -c, --config-file` +
ANSI_RESET +
` <path> config file (default "~/.envr/config.json")
Use "envr [command] --help" for more information about a command. Use "` +
COLOR_FLAGS +
"envr" +
ANSI_RESET +
` [command] --help" for more information about a command.
`, `,
flush = false, flush = false,
) )
} }
has_flag :: proc(cmd: ^Command, name: string) -> bool { has_flag :: proc(cmd: ^Command, name: string) -> bool {
_, ok := cmd.flags[name] return name in cmd.flags || name in cmd.bool_set
if ok {
return true
}
_, ok2 := cmd.bool_set[name]
return ok2
} }
delete_command :: proc(cmd: ^Command) { delete_command :: proc(cmd: ^Command) {

View File

@@ -57,7 +57,7 @@ test_usage_text_contains_flags_and_help_hint :: proc(t: ^testing.T) {
testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section") testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section")
testing.expect(t, strings.contains(text, "--help"), "missing --help flag") testing.expect(t, strings.contains(text, "--help"), "missing --help flag")
testing.expect(t, strings.contains(text, "[command] --help"), "missing help hint") testing.expect(t, strings.contains(text, "[command] --help"), "missing help hint")
} }
@(test) @(test)
test_command_help_backup :: proc(t: ^testing.T) { test_command_help_backup :: proc(t: ^testing.T) {

15
colors.odin Normal file
View File

@@ -0,0 +1,15 @@
package main
import "core:terminal/ansi"
COLOR_HEADINGS ::
ansi.CSI + ansi.FG_BRIGHT_GREEN + ";" + ansi.BOLD + ";" + ansi.UNDERLINE + ansi.SGR
COLOR_COMMANDS :: ansi.CSI + ansi.FG_BRIGHT_CYAN + ";" + ansi.BOLD + ansi.SGR
COLOR_EXAMPLE :: ansi.CSI + ansi.ITALIC + ansi.SGR
COLOR_FLAGS :: ansi.CSI + ansi.BOLD + ";" + ansi.FG_BRIGHT_WHITE + ansi.SGR
ANSI_RESET :: ansi.CSI + ansi.RESET + ansi.SGR

View File

@@ -1,6 +1,5 @@
package main package main
import "core:crypto/_fiat/field_p384r1"
import "core:fmt" import "core:fmt"
import "core:mem" import "core:mem"
import "core:os" import "core:os"

View File

@@ -309,7 +309,6 @@ test_ssh_key_parse_from_fixtures :: proc(t: ^testing.T) {
if !x_ok { if !x_ok {
return return
} }
defer delete(x25519_pairs)
testing.expect(t, len(x25519_pairs) == 1, "should have 1 x25519 keypair") testing.expect(t, len(x25519_pairs) == 1, "should have 1 x25519 keypair")
} }

View File

@@ -79,6 +79,13 @@
mysqlite mysqlite
]; ];
doCheck = true;
checkPhase = ''
runHook preCheck
odin test . -all-packages
runHook postCheck
'';
buildPhase = '' buildPhase = ''
runHook preBuild runHook preBuild
echo '${version}' > version.txt echo '${version}' > version.txt

View File

@@ -4,6 +4,7 @@ import "core:encoding/json"
import "core:fmt" import "core:fmt"
import "core:io" import "core:io"
import "core:strings" import "core:strings"
import "core:terminal/ansi"
render_table :: proc(w: io.Writer, headers: []string, rows: [][]string) { render_table :: proc(w: io.Writer, headers: []string, rows: [][]string) {
col_widths := make([dynamic]int, 0, len(headers), context.temp_allocator) col_widths := make([dynamic]int, 0, len(headers), context.temp_allocator)
@@ -45,14 +46,38 @@ render_table :: proc(w: io.Writer, headers: []string, rows: [][]string) {
hline(w, &b, "\u250c", "\u252c", "\u2510", col_widths) hline(w, &b, "\u250c", "\u252c", "\u2510", col_widths)
cell :: proc(b: ^strings.Builder, s: string, width: int) { cell :: proc(b: ^strings.Builder, s: string, width: int, color: string = "", center := false) {
extra := len(s) - strings.rune_count(s) before: int
fmt.sbprintf(b, " %-*s \u2502", width + extra, s) after: int
total_pad := width - strings.rune_count(s)
if center {
before = total_pad / 2
after = total_pad - before
} else {
before = 0
after = total_pad
}
fmt.sbprintf(
b,
" %s%s%s%*s%s%*s%s \u2502",
ansi.CSI,
color,
ansi.SGR,
before,
"",
s,
after,
"",
ansi.CSI + ansi.RESET + ansi.SGR,
)
} }
strings.write_string(&b, "\u2502") strings.write_string(&b, "\u2502")
for i in 0 ..< len(headers) { for i in 0 ..< len(headers) {
cell(&b, headers[i], col_widths[i]) cell(&b, headers[i], col_widths[i], ansi.FG_BRIGHT_GREEN, true)
} }
fmt.wprintf(w, "%s\n", strings.to_string(b), flush = false) fmt.wprintf(w, "%s\n", strings.to_string(b), flush = false)
strings.builder_reset(&b) strings.builder_reset(&b)

View File

@@ -3,6 +3,7 @@ package main
import "core:encoding/json" import "core:encoding/json"
import "core:fmt" import "core:fmt"
import "core:strings" import "core:strings"
import "core:terminal/ansi"
import "core:testing" import "core:testing"
@(test) @(test)
@@ -116,13 +117,30 @@ test_render_table_normal :: proc(t: ^testing.T) {
output := strings.to_string(b) output := strings.to_string(b)
expected := `┌──────┬─────────────────────────┐ g := ansi.CSI + ansi.FG_BRIGHT_GREEN + ansi.SGR
│ Name │ Path │ r := ANSI_RESET
├──────┼─────────────────────────┤ n := ansi.CSI + ansi.SGR
│ foo │ /home/user/.env │
│ bar │ /home/user/project/.env │ expected := fmt.tprintf(
─────────────────────────────── "┌───────────────────────────────┐\n" +
` "│ %sName%s │ %s Path %s │\n" +
"├──────┼─────────────────────────┤\n" +
"│ %sfoo %s │ %s/home/user/.env %s │\n" +
"│ %sbar %s │ %s/home/user/project/.env%s │\n" +
"└──────┴─────────────────────────┘\n",
g,
r,
g,
r,
n,
r,
n,
r,
n,
r,
n,
r,
)
testing.expect( testing.expect(
t, t,
output == expected, output == expected,
@@ -148,11 +166,17 @@ test_render_table_empty :: proc(t: ^testing.T) {
output := strings.to_string(b) output := strings.to_string(b)
expected := `┌──────┐ g := ansi.CSI + ansi.FG_BRIGHT_GREEN + ansi.SGR
│ Name │ r := ANSI_RESET
├──────┤
└──────┘ expected := fmt.tprintf(
` "┌──────┐\n" +
"│ %sName%s │\n" +
"├──────┤\n" +
"└──────┘\n",
g,
r,
)
testing.expect( testing.expect(
t, t,
output == expected, output == expected,
@@ -178,13 +202,30 @@ test_render_table_unicode :: proc(t: ^testing.T) {
output := strings.to_string(b) output := strings.to_string(b)
expected := `┌─────────────┬────────┐ g := ansi.CSI + ansi.FG_BRIGHT_GREEN + ansi.SGR
│ Status │ Detail │ r := ANSI_RESET
├─────────────┼────────┤ n := ansi.CSI + ansi.SGR
│ ✓ Available │ ok │
│ ✗ Missing │ fail │ expected := fmt.tprintf(
───────────────────── "┌─────────────────────┐\n" +
` "│ %s Status %s │ %sDetail%s │\n" +
"├─────────────┼────────┤\n" +
"│ %s✓ Available%s │ %sok %s │\n" +
"│ %s✗ Missing %s │ %sfail %s │\n" +
"└─────────────┴────────┘\n",
g,
r,
g,
r,
n,
r,
n,
r,
n,
r,
n,
r,
)
testing.expect( testing.expect(
t, t,
output == expected, output == expected,