13 Commits

Author SHA1 Message Date
spencer af1860c970 fix: Removed irrelevent flags from nushell-completion command help text. 2026-06-29 18:17:56 -04:00
spencer 5282a4414b perf!: Removed support for JSON formatted remotes.
BREAKING CHANGE: Databases with a JSON formatted remotes column will no
longer parse correctly. If you have an old database, make sure you
upgrade to envr 0.4.0 and run `envr list` before upgrading to this
version.
2026-06-29 18:12:11 -04:00
spencer ea1627ec2e build: nix version is now pinned to version.txt. 2026-06-29 18:12:10 -04:00
Spencer Brower d978cb6869 chore(main): release 0.4.0 2026-06-29 17:38:20 -04:00
spencer 70b5c0e184 ci: Updated release-please. 2026-06-29 17:32:55 -04:00
spencer fb90342126 fix: Flags in help text are now customized per command. 2026-06-29 17:32:55 -04:00
spencer 17d7fb5eb6 refactor: Removed hardcoded colors. 2026-06-29 17:32:55 -04:00
spencer c3e667e7bc feat: Added --color flag. 2026-06-29 17:32:55 -04:00
spencer 16819f65cb refactor: Used RTTI for more sophisticated flag parsing. 2026-06-29 17:32:55 -04:00
spencer 4d3da01f65 nix: Fixed the nix build. 2026-06-29 17:32:54 -04:00
spencer 2b68617b5d fix: -h short flag now works on subcommands. 2026-06-29 17:32:54 -04:00
spencer 36f124a4fd chore: Re-numbered todos. 2026-06-29 17:32:54 -04:00
spencer bb84c56c98 feat: Added --format, -f flag.
Allows printing data in tabular or json format.
2026-06-29 17:32:54 -04:00
17 changed files with 297 additions and 271 deletions
+1
View File
@@ -2,6 +2,7 @@ on:
push:
branches:
- main
- prerelease
permissions:
contents: write
+31
View File
@@ -1,5 +1,36 @@
# Changelog
## [0.4.0](https://github.com/sbrow/envr/compare/v0.3.0...v0.4.0) (2026-06-29)
### Features
* Added `--color` flag. ([c3e667e](https://github.com/sbrow/envr/commit/c3e667e7bca660b21b5851460691ec8bd5026f80))
* Added `--format`, `-f` flag. ([bb84c56](https://github.com/sbrow/envr/commit/bb84c56c98bead9ce9bad3f89fb3b61b53d05a10))
* Colorized console output. ([33cd7c4](https://github.com/sbrow/envr/commit/33cd7c4eda43287fe3dc2d2289a4e1531b524ef6))
* Removed runtime git dependency. ([12574e1](https://github.com/sbrow/envr/commit/12574e123bdedba3aca813143e906ec5e0b95719))
### Bug Fixes
* -h short flag now works on subcommands. ([2b68617](https://github.com/sbrow/envr/commit/2b68617b5d6c17444b04307d7b4b7f4a3aefb978))
* Databases errors are less likely to go unnoticed. ([f825bc2](https://github.com/sbrow/envr/commit/f825bc2b096632bb258d5681b1072941e9d4233d))
* Fixed leaks. ([c7c254f](https://github.com/sbrow/envr/commit/c7c254f6f2ac871182f3fcc233766884753e2049))
* Fixed memory leaks in the db. ([5059572](https://github.com/sbrow/envr/commit/5059572951b3ec20b3d2027032a9c3be5cb14dba))
* Fixed some leaks in `backup` and `scan`. ([dc72ff5](https://github.com/sbrow/envr/commit/dc72ff56fd0e165930771682b2fd266eef3e7e16))
* Fixed vet errors. ([1562fb3](https://github.com/sbrow/envr/commit/1562fb3665b8704dcffd4944047f7984b308e52e))
* Flags in help text are now customized per command. ([fb90342](https://github.com/sbrow/envr/commit/fb903421265baa65adbbe092f6bf797639f1fb1f))
* Handled mk_dir error. ([de1594d](https://github.com/sbrow/envr/commit/de1594d9d1fe46dd9ebc3be01fc5a5ebaf4064e5))
* **scan:** Fixed a bug preventing TUI from working. ([0083e4e](https://github.com/sbrow/envr/commit/0083e4e0dbc5b949a3cf4cd8b64af4f698ed9d33))
* Used os path separator rather than '/' where appropriate. ([5cc7973](https://github.com/sbrow/envr/commit/5cc79737753f0f19db2bc55304ea5f70237cbffd))
### Performance Improvements
* Improved the performance of table rendering. ([0b5bf4d](https://github.com/sbrow/envr/commit/0b5bf4db73113bdd43d8a225ec0ae53fedd25918))
* remotes are now stored as a newline delimited list. ([96b3d63](https://github.com/sbrow/envr/commit/96b3d6340a95b67c884ba940b46461e51989fcbb))
* Replaced `fd` with custom internals. ([2ef733f](https://github.com/sbrow/envr/commit/2ef733fe58594b0a0b6e3ef85142b74af445ccb8))
## [0.3.0](https://github.com/sbrow/envr/compare/v0.2.1...v0.3.0) (2026-06-16)
Version 0.3.0 represents a significant departure (and improvement) for envr.
+1 -1
View File
@@ -2,7 +2,7 @@
# Builds release artifacts for GitHub releases
APP_NAME := envr
VERSION := $(shell grep 'version = ' flake.nix | head -1 | sed 's/.*version = "\(.*\)";/\1/')
VERSION := $(file < version.txt)
BUILD_DIR := builds
# Binary names
+3 -7
View File
@@ -20,15 +20,11 @@
10. Pass allocator to findr?
11. Smarter flag parsing?
11. Consider getting rid of color global.
12. Rewrite `write_command_help` to use text/tables
12. `write_flags_table` should never return false.
13. Instead of using a writer to strip colors, just don't print the colors.
14. Add a text filter to the multi_select.
15. init -h doesn't show --force flag. Separate into multiple structs: Global_FLags, and Init_Flags?
13. Add a text filter to the multi_select.
## Double-check AI output
+153 -84
View File
@@ -1,9 +1,11 @@
package main
import "base:runtime"
import "core:bufio"
import "core:fmt"
import "core:io"
import "core:os"
import "core:reflect"
import "core:strings"
import "core:terminal"
import "core:text/table"
@@ -17,13 +19,20 @@ Command :: struct {
err: io.Writer,
}
// TODO: Put help test in usage:"whatever" tag.
Flags :: struct {
help: bool `args:"short=h"`,
config_file: string `args:"name=config-file,short=c"`,
output: Output_Format `args:"short=o"`,
color: Color_Mode,
force: bool `args:"short=f"`,
help: bool `args:"short=h" usage:"show this documentation"`,
config_file: string `args:"name=config-file,short=c" usage:"config file" default:"~/.envr/config.json"`,
output: Output_Format `args:"short=o" usage:"the format of output data" default:"table"`,
color: Color_Mode `usage:"Whether or not to colorize output" default:"auto"`,
force: bool `args:"short=f" usage:"Overwrite existing config"`,
}
Flag_Type :: enum {
Help,
Config_File,
Output,
Color,
Force,
}
Output_Format :: enum {
@@ -44,8 +53,12 @@ CommandInfo :: struct {
short: string,
long: string,
aliases: []string,
// Flags supported by the command
flags: bit_set[Flag_Type],
}
GLOBAL_FLAGS: bit_set[Flag_Type] = {.Help, .Config_File, .Color}
COMMANDS := []CommandInfo {
{
"init",
@@ -56,22 +69,45 @@ COMMANDS := []CommandInfo {
encrypt your databse. **Make 100% sure** that you have **a remote copy** of this
key somewhere, otherwise your data could be lost forever.`,
{},
GLOBAL_FLAGS + {.Force},
},
{"scan", "envr scan", "Find and select .env files for backup", "", {}, GLOBAL_FLAGS},
{"sync", "envr sync", "Update or restore your env backups", "", {}, GLOBAL_FLAGS + {.Output}},
{"backup", "envr backup <path>", "Import a .env file into envr", "", {"add"}, GLOBAL_FLAGS},
{
"restore",
"envr restore <path>",
"Restore a .env file from the database",
"",
{},
GLOBAL_FLAGS,
},
{"list", "envr list", "View your tracked files", "", {}, GLOBAL_FLAGS + {.Output}},
{
"remove",
"envr remove <path>",
"Remove a .env file from your database",
"",
{},
GLOBAL_FLAGS,
},
{"check", "envr check [path]", "Check if files are backed up", "", {}, GLOBAL_FLAGS},
{"version", "envr version", "Show envr's version", "", {}, {.Help}},
{
"edit-config",
"envr edit-config",
"Edit your config with your default editor",
"",
{},
GLOBAL_FLAGS,
},
{"scan", "envr scan", "Find and select .env files for backup", "", {}},
{"sync", "envr sync", "Update or restore your env backups", "", {}},
{"backup", "envr backup <path>", "Import a .env file into envr", "", {"add"}},
{"restore", "envr restore <path>", "Restore a .env file from the database", "", {}},
{"list", "envr list", "View your tracked files", "", {}},
{"remove", "envr remove <path>", "Remove a .env file from your database", "", {}},
{"check", "envr check [path]", "Check if files are backed up", "", {}},
{"version", "envr version", "Show envr's version", "", {}},
{"edit-config", "envr edit-config", "Edit your config with your default editor", "", {}},
{
"nushell-completion",
"envr nushell-completion",
"Generate custom completions for nushell",
"",
{},
{.Help},
},
}
@@ -106,7 +142,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 == "" {
@@ -134,6 +170,7 @@ print_command_help :: proc(cmd: ^Command) {
}
write_command_help :: proc(name: string, w: io.Writer) -> bool {
// TODO: rename info to cmd?
info, found := find_command(name)
if !found {
return false
@@ -141,36 +178,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)
}
@@ -179,25 +203,88 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool {
fmt.wprintf(w, "\n%s\n", info.long, flush = false)
}
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 +
` <path> config file (default "~/.envr/config.json")
`,
info.name,
flush = false,
)
tbl: table.Table
table.init(&tbl, context.temp_allocator, context.temp_allocator)
table.padding(&tbl, 2, 0)
if write_flags_table(&tbl, info.flags) {
fmt.wprintf(w, "\n", flush = false)
write_borderless_table(w, &tbl)
}
table_reset(&tbl)
return true
}
flag_field_info :: proc(
ft: Flag_Type,
) -> (
names: string,
value_hint: string,
description: string,
) {
field := reflect.struct_field_at(Flags, int(ft))
args_tag := reflect.struct_tag_get(field.tag, "args")
long_name, _ := strings.replace(field.name, "_", "-", -1, context.temp_allocator)
if n, ok := get_subtag(args_tag, "name"); ok {
long_name = n
}
short, has_short := get_subtag(args_tag, "short")
if has_short {
names = fmt.tprintf("-%s, --%s", short, long_name)
} else {
names = fmt.tprintf("--%s", long_name)
}
base_ti := runtime.type_info_base(field.type)
if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool {
value_hint = ""
} else if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
value_hint = " <value>"
} else if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
parts := make([dynamic]string, 0, len(enum_ti.names), context.temp_allocator)
for name in enum_ti.names {
lower := strings.to_lower(name, context.temp_allocator)
append(&parts, fmt.tprintf("'%s'", lower))
}
value_hint = fmt.tprintf(" %s", strings.join(parts[:], "|", context.temp_allocator))
delete(parts)
}
usage := reflect.struct_tag_get(field.tag, "usage")
default_val := reflect.struct_tag_get(field.tag, "default")
description = usage
if len(default_val) > 0 {
if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
description = fmt.tprintf(`%s (default "%s")`, usage, default_val)
} else if _, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
description = fmt.tprintf("%s (default '%s')", usage, default_val)
}
}
return
}
write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) -> (has_rows: bool) {
if flags == {} do return false
table.caption(tbl, "Flags:")
for ft in Flag_Type {
if ft not_in flags do continue
names, hint, desc := flag_field_info(ft)
if len(hint) > 0 {
display := table.format(
tbl,
"%s%s",
colorize(.Flag, names, tbl.format_allocator),
hint,
)
table.row(tbl, display, desc)
} else {
table.row(tbl, colorize(.Flag, names, tbl.format_allocator), desc)
}
}
return true
}
@@ -250,15 +337,13 @@ at before, restore your backup with:
> envr restore ~/<path to repository>/.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,37 +359,21 @@ 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)
table_reset(&tbl)
table.caption(&tbl, "Flags:")
table.row(&tbl, COLOR_FLAGS + "-h, --help" + ANSI_RESET, `show this documentation`)
table.row(
&tbl,
COLOR_FLAGS + "-c, --config-file" + ANSI_RESET + " <path>",
`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.row(
&tbl,
COLOR_FLAGS + "--color" + ANSI_RESET + " 'auto'|'always'|'never'",
`Whether or not to colorize output. (default 'auto')`,
)
write_borderless_table(w, &tbl)
if write_flags_table(&tbl, GLOBAL_FLAGS) {
write_borderless_table(w, &tbl)
}
table_reset(&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,
)
}
+2 -1
View File
@@ -110,7 +110,8 @@ test_command_help_init_no_aliases :: proc(t: ^testing.T) {
testing.expect(t, !strings.contains(text, "Aliases:"), "init should not have Aliases section")
testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section")
testing.expect(t, strings.contains(text, "show this documentation"), "missing help flag description")
}
testing.expect(t, strings.contains(text, "--force"), "missing --force flag")
}
@(test)
test_command_help_unknown :: proc(t: ^testing.T) {
+1 -5
View File
@@ -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
}
+2 -2
View File
@@ -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 {
+7 -21
View File
@@ -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)
}
}
+2 -2
View File
@@ -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 {
+53 -73
View File
@@ -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..<len(p) {
b := p[i]
@(private = "file")
ANSI_RESET :: ansi.CSI + ansi.RESET + ansi.SGR
switch data.state {
case .Normal:
if b == 0x1b {
if i > 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")
}
}
+9 -65
View File
@@ -4,7 +4,6 @@ import "base:runtime"
import "core:crypto/hash"
import "core:encoding/hex"
import "core:encoding/ini"
import "core:encoding/json"
import "core:fmt"
import "core:mem"
import "core:os"
@@ -222,7 +221,6 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
allocator := db_allocator(db)
results := make([dynamic]EnvFile, 0, 10, allocator)
migrate := false
for {
rc = sqlite.step(stmt)
if rc == sqlite.DONE {
@@ -233,24 +231,13 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
#no_bounds_check return results[:], false
}
// TODO: Remove json support after next major release
remotes: [dynamic]string = ---
remotes_raw := string(sqlite.column_text(stmt, 1))
if len(remotes_raw) > 0 {
if remotes_raw[0] == '[' {
err := json.unmarshal_string(remotes_raw, &remotes, allocator = allocator)
if err != nil {
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
}
migrate = true
} else {
split := strings.split_lines(remotes_raw, context.temp_allocator)
remotes = make([dynamic]string, 0, len(split), allocator = allocator)
for s in split {
append(&remotes, strings.clone(s, allocator))
}
}
split := strings.split_lines(remotes_raw, context.temp_allocator)
remotes := make([dynamic]string, 0, len(split), allocator = allocator)
for s in split {
append(&remotes, strings.clone(s, allocator))
}
path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
append(
@@ -265,10 +252,6 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
)
}
if migrate {
migrate_remotes(db)
}
#no_bounds_check return results[:], true
}
@@ -364,33 +347,15 @@ db_fetch :: proc(db: ^Db, path: string) -> (EnvFile, bool) {
return EnvFile{}, false
}
// TODO: Remove json support after next major release
migrate := false
remotes: [dynamic]string = ---
remotes_raw := string(sqlite.column_text(stmt, 1))
if len(remotes_raw) > 0 {
if remotes_raw[0] == '[' {
err := json.unmarshal_string(remotes_raw, &remotes, allocator = allocator)
if err != nil {
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
}
migrate = true
} else {
split := strings.split_lines(remotes_raw, context.temp_allocator)
remotes = make([dynamic]string, 0, len(split), allocator = allocator)
for s in split {
append(&remotes, strings.clone(s, allocator))
}
}
split := strings.split_lines(remotes_raw, context.temp_allocator)
remotes := make([dynamic]string, 0, len(split), allocator = allocator)
for s in split {
append(&remotes, strings.clone(s, allocator))
}
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
if migrate {
migrate_remotes(db)
}
return EnvFile {
path = file_path,
dir = filepath.dir(file_path),
@@ -525,27 +490,6 @@ db_persist :: proc(db: ^Db, f: ^EnvFile, old_path: string) -> bool {
return db_insert(db, f^)
}
// TODO: Remove after the next major release
migrate_remotes :: proc(db: ^Db) {
sql ::
"UPDATE envr_env_files " +
"SET remotes = COALESCE((" +
" SELECT group_concat(atom, char(10)) " +
" FROM json_each(envr_env_files.remotes)" +
"), '') " +
"WHERE remotes LIKE '[%'"
rc := sqlite.exec(db.conn, sql, nil, nil, nil)
if rc != sqlite.OK {
fmt.eprintf("Warning: failed to migrate remotes: %s\n", sqlite.errmsg(db.conn))
return
}
if sqlite.changes(db.conn) > 0 {
db.changed = true
}
}
try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, SyncError) {
roots, ok := find_git_roots(db.cfg)
if !ok {
+1 -1
View File
@@ -66,7 +66,7 @@
packages.default = pkgs.stdenv.mkDerivation rec {
pname = "envr";
version = "0.3.0";
version = nixpkgs.lib.trim (builtins.readFile ./version.txt);
src = ./.;
nativeBuildInputs = [
+19 -5
View File
@@ -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])
}
}
+1 -1
View File
@@ -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,
+10 -2
View File
@@ -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)
}
+1 -1
View File
@@ -1 +1 @@
0.3.0
0.4.0