diff --git a/TODOS.md b/TODOS.md index 21bbe96..96b9f43 100644 --- a/TODOS.md +++ b/TODOS.md @@ -24,7 +24,7 @@ 12. Rewrite `write_command_help` to use text/tables -13. Instead of using a writer to strip colors, just don't print the colors. +13. Consider getting rid of color global. 14. Add a text filter to the multi_select. diff --git a/cli.odin b/cli.odin index a201df3..7e490c4 100644 --- a/cli.odin +++ b/cli.odin @@ -106,7 +106,7 @@ parse_args :: proc(args: []string, out: io.Stream, err: io.Stream) -> (cmd: Comm cmd.flags.color = terminal.is_terminal(os.stdout) ? .Always : .Never } if cmd.flags.color == .Never { - cmd.out = make_ansi_strip_writer(cmd.out) + disable_color = true } if cmd.flags.config_file == "" { @@ -141,36 +141,23 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool { fmt.wprintf( w, - "%s\n\n\n" + - COLOR_HEADINGS + - "Usage:" + - ANSI_RESET + - "\n\n " + - COLOR_FLAGS + - "%s" + - ANSI_RESET + - " [flags]\n\n", + "%s\n\n\n%s\n\n %s [flags]\n\n", info.short, - info.usage, + colorize(.Heading, "Usage:"), + colorize(.Flag, info.usage), flush = false, ) if len(info.aliases) > 0 { fmt.wprintf( w, - "\n" + - COLOR_HEADINGS + - "Aliases:" + - ANSI_RESET + - "\n\n " + - COLOR_COMMANDS + - "%s" + - ANSI_RESET, - info.name, + "\n%s\n\n %s", + colorize(.Heading, "Aliases:"), + colorize(.Command, info.name), flush = false, ) for a in info.aliases { - fmt.wprintf(w, ", " + COLOR_COMMANDS + "%s" + ANSI_RESET, a, flush = false) + fmt.wprintf(w, ", %s", colorize(.Command, a), flush = false) } fmt.wprintf(w, "\n", flush = false) } @@ -181,21 +168,14 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool { fmt.wprintf( w, - "\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 + - ` config file (default "~/.envr/config.json") + "\n%s\n\n %s" + + ` help for %s + %s config file (default "~/.envr/config.json") `, + colorize(.Heading, "Flags:"), + colorize(.Flag, "-h, --help"), info.name, + colorize(.Flag, "-c, --config-file"), flush = false, ) return true @@ -250,15 +230,13 @@ at before, restore your backup with: > envr restore ~//.env -%sUsage:%s +%s - %senvr%s [command] + %s [command] `, - COLOR_HEADINGS, - ANSI_RESET, - COLOR_FLAGS, - ANSI_RESET, + colorize(.Heading, "Usage:"), + colorize(.Flag, "envr"), flush = false, ) @@ -274,7 +252,7 @@ at before, restore your backup with: for a in c.aliases { name = strings.join([]string{name, a}, ", ", tbl.format_allocator) } - table.row(&tbl, table.format(&tbl, "%s%s%s", COLOR_COMMANDS, name, ANSI_RESET), c.short) + table.row(&tbl, colorize(.Command, name, tbl.format_allocator), c.short) } write_borderless_table(w, &tbl) @@ -282,29 +260,40 @@ at before, restore your backup with: table.caption(&tbl, "Flags:") - table.row(&tbl, COLOR_FLAGS + "-h, --help" + ANSI_RESET, `show this documentation`) + table.row(&tbl, colorize(.Flag, "-h, --help", tbl.format_allocator), `show this documentation`) table.row( &tbl, - COLOR_FLAGS + "-c, --config-file" + ANSI_RESET + " ", + table.format( + &tbl, + "%s ", + colorize(.Flag, "-c, --config-file", tbl.format_allocator), + ), `config file (default "~/.envr/config.json")`, ) table.row( &tbl, - COLOR_FLAGS + "-o, --output" + ANSI_RESET + " 'table'|'json'", - `the format of output data. (default 'table')`, + table.format( + &tbl, + "%s 'table'|'json'", + colorize(.Flag, "-o, --output", tbl.format_allocator), + ), + `The format of output data. (default 'table')`, ) table.row( &tbl, - COLOR_FLAGS + "--color" + ANSI_RESET + " 'auto'|'always'|'never'", + table.format( + &tbl, + "%s 'auto'|'always'|'never'", + colorize(.Flag, "--color", tbl.format_allocator), + ), `Whether or not to colorize output. (default 'auto')`, ) write_borderless_table(w, &tbl) fmt.wprintf( w, - `Use "%senvr%s [command] --help" for more information about a command.`, - COLOR_FLAGS, - ANSI_RESET, + `Use "%s [command] --help" for more information about a command.`, + colorize(.Flag, "envr", tbl.format_allocator), flush = false, ) } diff --git a/cmd_init.odin b/cmd_init.odin index 9322fdb..9b36ced 100644 --- a/cmd_init.odin +++ b/cmd_init.odin @@ -38,11 +38,7 @@ Generate one with: ssh-keygen -t ed25519`, selected, result := multi_select("Select SSH private keys:", keys[:]) defer delete(selected) if result == .Cancel { - fmt.wprintln( - cmd.out, - ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET, - flush = false, - ) + fmt.wprintln(cmd.out, colorize(.Message, "Cancelled."), flush = false) return } diff --git a/cmd_list.odin b/cmd_list.odin index c3ba9f9..2bb9965 100644 --- a/cmd_list.odin +++ b/cmd_list.odin @@ -32,8 +32,8 @@ cmd_list :: proc(cmd: ^Command) { table.aligned_header_of_values( &t, .Center, - COLOR_TABLE_HEADING + "Directory" + ANSI_RESET, - COLOR_TABLE_HEADING + "Path" + ANSI_RESET, + colorize(.Table_Heading, "Directory"), + colorize(.Table_Heading, "Path"), ) for row in rows { diff --git a/cmd_scan.odin b/cmd_scan.odin index 437c09d..e775f69 100644 --- a/cmd_scan.odin +++ b/cmd_scan.odin @@ -4,7 +4,6 @@ import "core:encoding/json" import "core:fmt" import "core:os" import "core:terminal" -import "core:terminal/ansi" cmd_scan :: proc(cmd: ^Command) { db, db_ok := db_open(cmd.flags.config_file) @@ -72,11 +71,7 @@ cmd_scan :: proc(cmd: ^Command) { selected, result := multi_select("Select .env files to backup:", files[:]) defer delete(selected) if result == .Cancel { - fmt.wprintln( - cmd.out, - ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET, - flush = false, - ) + fmt.wprintln(cmd.out, colorize(.Message, "Cancelled."), flush = false) return } @@ -100,25 +95,16 @@ cmd_scan :: proc(cmd: ^Command) { } if added_count > 0 { - fmt.wprintf( + fmt.wprintln( cmd.out, - ansi.CSI + - ansi.BOLD + - ";" + - ansi.FG_GREEN + - ansi.SGR + - "Successfully added %d file(s) to backup." + - ANSI_RESET + - "\n", - added_count, + colorize( + .Sucess, + fmt.tprintf("Successfully added %d file(s) to backup.", added_count), + ), flush = false, ) } else { - fmt.wprintln( - cmd.out, - ansi.CSI + ansi.FAINT + ansi.SGR + "No files were added." + ANSI_RESET, - flush = false, - ) + fmt.wprintln(cmd.out, colorize(.Message, "No files were added."), flush = false) } } diff --git a/cmd_sync.odin b/cmd_sync.odin index dbd3e51..f3c7e80 100644 --- a/cmd_sync.odin +++ b/cmd_sync.odin @@ -54,8 +54,8 @@ cmd_sync :: proc(cmd: ^Command) { table.aligned_header_of_values( &t, .Center, - COLOR_TABLE_HEADING + "File" + ANSI_RESET, - COLOR_TABLE_HEADING + "Status" + ANSI_RESET, + colorize(.Table_Heading, "File"), + colorize(.Table_Heading, "Status"), ) for res in results { diff --git a/colors.odin b/colors.odin index 214a6bc..24e04ba 100644 --- a/colors.odin +++ b/colors.odin @@ -1,86 +1,66 @@ package main -import "core:io" +import "base:runtime" +import "core:strings" 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 - -COLOR_TABLE_HEADING :: ansi.CSI + ansi.FG_BRIGHT_GREEN + ansi.SGR - -ANSI_RESET :: ansi.CSI + ansi.RESET + ansi.SGR - -ANSI_Strip_State :: enum { Normal, GotESC, InCSI } - -ANSI_Strip_Data :: struct { - inner: io.Writer, - state: ANSI_Strip_State, +Color_Code :: enum { + Heading, + Command, + // Example + Flag, + Table_Heading, + Message, + Sucess, + Caret, + Option_Label, } -ansi_strip_proc :: proc( - stream_data: rawptr, - mode: io.Stream_Mode, - p: []byte, - offset: i64, - whence: io.Seek_From, -) -> (n: i64, err: io.Error) { - data := cast(^ANSI_Strip_Data) stream_data +// COLOR_EXAMPLE :: ansi.CSI + ansi.ITALIC + ansi.SGR - #partial switch mode { - case .Write: - start := 0 - for i in 0.. start { - io.write(data.inner, p[start:i]) - } - data.state = .GotESC - } +disable_color := false - case .GotESC: - if b == '[' { - data.state = .InCSI - } else { - start = i - data.state = .Normal - } - - case .InCSI: - if b >= 0x40 && b <= 0x7E { - start = i + 1 - data.state = .Normal - } - } - } - - if data.state == .Normal && len(p) > start { - io.write(data.inner, p[start:]) - } - - n = i64(len(p)) - return - - case .Flush: - return 0, io.flush(data.inner) - case .Close: - return 0, io.close(data.inner) - case: - return data.inner.procedure(data.inner.data, mode, p, offset, whence) +colorize :: proc( + color: Color_Code, + text: string, + allocator := context.temp_allocator, + disable := disable_color, +) -> ( + string, + runtime.Allocator_Error, +) #optional_allocator_error { + if disable { + return text, nil + } else { + return strings.concatenate( + {ansi.CSI, color_code(color), ansi.SGR, text, ANSI_RESET}, + allocator, + ) } } -make_ansi_strip_writer :: proc(inner: io.Writer) -> io.Writer { - data := new(ANSI_Strip_Data, context.temp_allocator) - data.inner = inner - return io.Writer{procedure = ansi_strip_proc, data = rawptr(data)} +@(private = "file") +color_code :: proc(code: Color_Code) -> string { + switch code { + case .Heading: + return ansi.BOLD + ";" + ansi.UNDERLINE + ";" + ansi.FG_BRIGHT_GREEN + case .Command: + return ansi.BOLD + ";" + ansi.FG_BRIGHT_CYAN + case .Flag: + return ansi.BOLD + ";" + ansi.FG_BRIGHT_WHITE + case .Table_Heading: + return ansi.FG_BRIGHT_GREEN + case .Message: + return ansi.FAINT + case .Sucess, .Caret: + return ansi.BOLD + ";" + ansi.FG_GREEN + case .Option_Label: + return ansi.BOLD + ";" + ansi.FG_CYAN + case: + panic("Unknown case") + } } + diff --git a/prompt.odin b/prompt.odin index e31b2a9..3b0d0b7 100644 --- a/prompt.odin +++ b/prompt.odin @@ -66,11 +66,17 @@ multi_select :: proc( case .Space: selected[cursor] = !selected[cursor] case .Enter: - fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW, visible + 1) + fmt.printf( + ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW, + visible + 1, + ) result = .Confirm return case .Escape: - fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW, visible + 1) + fmt.printf( + ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW, + visible + 1, + ) result = .Cancel return case .Unknown: @@ -89,7 +95,10 @@ render_options :: proc( cursor: int, scroll_offset: int, ) -> int { - fmt.printf(ansi.CSI + ansi.BOLD + ";" + ansi.FG_CYAN + ansi.SGR + "%s" + ANSI_RESET + " (↑/↓ move, space select, enter confirm)\r\n", prompt) + fmt.printf( + "%s (↑/↓ move, space select, enter confirm)\r\n", + colorize(.Option_Label, prompt), + ) end := scroll_offset + MAX_VISIBLE if end > len(options) { @@ -102,9 +111,14 @@ render_options :: proc( checkbox = "x" } if i == cursor { - fmt.printf(ansi.CSI + ansi.BOLD + ";" + ansi.FG_GREEN + ansi.SGR + "> " + ANSI_RESET + "[" + ansi.CSI + ansi.FG_GREEN + ansi.SGR + "%s" + ANSI_RESET + "] %s\r\n", checkbox, options[i]) + fmt.printf( + "%s [%s] %s\r\n", + colorize(.Caret, ">"), + colorize(.Sucess, checkbox), + options[i], + ) } else { - fmt.printf(" [" + ansi.CSI + ansi.FAINT + ansi.SGR + "%s" + ANSI_RESET + "] %s\r\n", checkbox, options[i]) + fmt.printf(" [%s] %s\r\n", colorize(.Sucess, checkbox), options[i]) } } diff --git a/table.odin b/table.odin index 5d3f186..4f3de8d 100644 --- a/table.odin +++ b/table.odin @@ -42,7 +42,7 @@ write_borderless_table :: proc(w: io.Writer, t: ^table.Table) { if t.caption != "" { table.write_text_align( w, - fmt.tprintf("%s%s%s", COLOR_HEADINGS, t.caption, ANSI_RESET), + colorize(.Heading, t.caption), .Left, 0, //t.lpad, 0, //t.rpad, diff --git a/table_test.odin b/table_test.odin index 0344c50..9330669 100644 --- a/table_test.odin +++ b/table_test.odin @@ -2,6 +2,7 @@ package main +import "core:strings" import "core:testing" @(test) @@ -16,7 +17,7 @@ test_ansi_aware_width_empty :: proc(t: ^testing.T) { @(test) test_ansi_aware_width_with_color_codes :: proc(t: ^testing.T) { - colored := COLOR_TABLE_HEADING + "Directory" + ANSI_RESET + colored := colorize(.Heading, "Directory", disable = false) testing.expect_value(t, ansi_aware_width(colored), 9) } @@ -28,6 +29,13 @@ test_ansi_aware_width_multibyte :: proc(t: ^testing.T) { @(test) test_ansi_aware_width_multiple_escape_sequences :: proc(t: ^testing.T) { - colored := COLOR_TABLE_HEADING + "a" + ANSI_RESET + "b" + COLOR_TABLE_HEADING + "c" + ANSI_RESET + colored := strings.concatenate( + { + colorize(.Heading, "a", disable = false), + colorize(.Heading, "b", disable = false), + colorize(.Heading, "c", disable = false), + }, + ) testing.expect_value(t, ansi_aware_width(colored), 3) } +