mirror of
https://github.com/sbrow/envr.git
synced 2026-08-26 18:03:32 -04:00
Compare commits
14 Commits
v0.4.0
...
997f11b257
| Author | SHA1 | Date | |
|---|---|---|---|
| 997f11b257 | |||
| f5624e91f1 | |||
| 80353f387a | |||
| aae1495d59 | |||
| 209ec89add | |||
| 7ffc38171c | |||
| 8b9a9789ab | |||
| 6244a6c8ce | |||
| 7aaa415777 | |||
| ca992f05f4 | |||
| ae42d53f02 | |||
| af1860c970 | |||
| 5282a4414b | |||
| ea1627ec2e |
@@ -2,7 +2,7 @@
|
|||||||
# Builds release artifacts for GitHub releases
|
# Builds release artifacts for GitHub releases
|
||||||
|
|
||||||
APP_NAME := envr
|
APP_NAME := envr
|
||||||
VERSION := $(shell grep 'version = ' flake.nix | head -1 | sed 's/.*version = "\(.*\)";/\1/')
|
VERSION := $(file < version.txt)
|
||||||
BUILD_DIR := builds
|
BUILD_DIR := builds
|
||||||
|
|
||||||
# Binary names
|
# Binary names
|
||||||
|
|||||||
@@ -6,25 +6,14 @@
|
|||||||
|
|
||||||
3. procedures should be ordered by use, main at the top, then in the order they are called from main.
|
3. procedures should be ordered by use, main at the top, then in the order they are called from main.
|
||||||
|
|
||||||
4. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
|
4. Test all command branches.
|
||||||
|
|
||||||
5. Test all cmds / terminal branches.
|
5. Generate md and man pages again.
|
||||||
|
|
||||||
6. Generate md and man pages again.
|
6. Bash Shell completion
|
||||||
|
|
||||||
7. Shell completion
|
7. Add tests for untested commands.
|
||||||
|
|
||||||
8. Add tests for untested commands.
|
|
||||||
|
|
||||||
9. Update `read_wire_string` to use a slice.
|
|
||||||
|
|
||||||
10. Pass allocator to findr?
|
|
||||||
|
|
||||||
11. Consider getting rid of color global.
|
|
||||||
|
|
||||||
12. `write_flags_table` should never return false.
|
|
||||||
|
|
||||||
13. Add a text filter to the multi_select.
|
|
||||||
|
|
||||||
## Double-check AI output
|
## Double-check AI output
|
||||||
|
|
||||||
@@ -54,7 +43,7 @@
|
|||||||
- [ ] db_test.odin
|
- [ ] db_test.odin
|
||||||
- [ ] flags.odin
|
- [ ] flags.odin
|
||||||
- [x] main.odin
|
- [x] main.odin
|
||||||
- [x] prompt.odin
|
- [ ] prompt.odin
|
||||||
- [x] scan.odin
|
- [x] scan.odin
|
||||||
- [ ] scan_test.odin
|
- [ ] scan_test.odin
|
||||||
- [ ] sodium.odin
|
- [ ] sodium.odin
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ Command :: struct {
|
|||||||
Flags :: struct {
|
Flags :: struct {
|
||||||
help: bool `args:"short=h" usage:"show this documentation"`,
|
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"`,
|
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"`,
|
output: Output_Format `args:"short=o" usage:"the format of output data" default:"table" completion:"output"`,
|
||||||
color: Color_Mode `usage:"Whether or not to colorize output" default:"auto"`,
|
color: Color_Mode `usage:"Whether or not to colorize output" default:"auto" completion:"color"`,
|
||||||
force: bool `args:"short=f" usage:"Overwrite existing config"`,
|
force: bool `args:"short=f" usage:"Overwrite existing config"`,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +47,12 @@ Color_Mode :: enum {
|
|||||||
Never,
|
Never,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Positional_Arg :: struct {
|
||||||
|
name: string,
|
||||||
|
completion: string,
|
||||||
|
optional: bool,
|
||||||
|
}
|
||||||
|
|
||||||
CommandInfo :: struct {
|
CommandInfo :: struct {
|
||||||
name: string,
|
name: string,
|
||||||
usage: string,
|
usage: string,
|
||||||
@@ -55,59 +61,81 @@ CommandInfo :: struct {
|
|||||||
aliases: []string,
|
aliases: []string,
|
||||||
// Flags supported by the command
|
// Flags supported by the command
|
||||||
flags: bit_set[Flag_Type],
|
flags: bit_set[Flag_Type],
|
||||||
|
args: []Positional_Arg,
|
||||||
}
|
}
|
||||||
|
|
||||||
GLOBAL_FLAGS: bit_set[Flag_Type] = {.Help, .Config_File, .Color}
|
GLOBAL_FLAGS: bit_set[Flag_Type] = {.Help, .Config_File, .Color}
|
||||||
|
|
||||||
COMMANDS := []CommandInfo {
|
COMMANDS := []CommandInfo {
|
||||||
{
|
{
|
||||||
"init",
|
name = "init",
|
||||||
"envr init",
|
usage = "envr init",
|
||||||
"Set up envr",
|
short = "Set up envr",
|
||||||
`The init command generates your initial config and saves it to
|
long = `The init command generates your initial config and saves it to
|
||||||
~/.envr/config in JSON format.\n\nDuring setup, you will be prompted to select one or more ssh keys with which to
|
~/.envr/config in JSON format.\n\nDuring setup, you will be prompted to select one or more ssh keys with which to
|
||||||
encrypt your databse. **Make 100% sure** that you have **a remote copy** of this
|
encrypt your databse. **Make 100% sure** that you have **a remote copy** of this
|
||||||
key somewhere, otherwise your data could be lost forever.`,
|
key somewhere, otherwise your data could be lost forever.`,
|
||||||
{},
|
flags = GLOBAL_FLAGS + {.Force},
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nushell-completion",
|
name = "scan",
|
||||||
"envr nushell-completion",
|
usage = "envr scan",
|
||||||
"Generate custom completions for nushell",
|
short = "Find and select .env files for backup",
|
||||||
"",
|
flags = GLOBAL_FLAGS,
|
||||||
{},
|
},
|
||||||
GLOBAL_FLAGS - {.Color},
|
{
|
||||||
|
name = "sync",
|
||||||
|
usage = "envr sync",
|
||||||
|
short = "Update or restore your env backups",
|
||||||
|
flags = GLOBAL_FLAGS + {.Output},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "backup",
|
||||||
|
usage = "envr backup <path>",
|
||||||
|
short = "Import a .env file into envr",
|
||||||
|
aliases = {"add"},
|
||||||
|
flags = GLOBAL_FLAGS,
|
||||||
|
args = {{name = "path", completion = "untracked-paths"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "restore",
|
||||||
|
usage = "envr restore <path>",
|
||||||
|
short = "Restore a .env file from the database",
|
||||||
|
flags = GLOBAL_FLAGS,
|
||||||
|
args = {{name = "path", completion = "tracked-paths"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "list",
|
||||||
|
usage = "envr list",
|
||||||
|
short = "View your tracked files",
|
||||||
|
flags = GLOBAL_FLAGS + {.Output},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "remove",
|
||||||
|
usage = "envr remove <path>",
|
||||||
|
short = "Remove a .env file from your database",
|
||||||
|
flags = GLOBAL_FLAGS,
|
||||||
|
args = {{name = "path", completion = "tracked-paths"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "check",
|
||||||
|
usage = "envr check [path]",
|
||||||
|
short = "Check if files are backed up",
|
||||||
|
flags = GLOBAL_FLAGS,
|
||||||
|
args = {{name = "path", optional = true}},
|
||||||
|
},
|
||||||
|
{name = "version", usage = "envr version", short = "Show envr's version", flags = {.Help}},
|
||||||
|
{
|
||||||
|
name = "edit-config",
|
||||||
|
usage = "envr edit-config",
|
||||||
|
short = "Edit your config with your default editor",
|
||||||
|
flags = GLOBAL_FLAGS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "nushell-completion",
|
||||||
|
usage = "envr nushell-completion",
|
||||||
|
short = "Generate custom completions for nushell",
|
||||||
|
flags = {.Help},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +155,7 @@ parse_args :: proc(args: []string, out: io.Stream, err: io.Stream) -> (cmd: Comm
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.name = args[1]
|
cmd.name = args[1]
|
||||||
cmd.args = make([dynamic]string)
|
cmd.args = make([dynamic]string, 0, len(args[2:]))
|
||||||
|
|
||||||
overflow := parse_flags(&cmd.flags, args[2:])
|
overflow := parse_flags(&cmd.flags, args[2:])
|
||||||
for arg in overflow {
|
for arg in overflow {
|
||||||
@@ -206,21 +234,30 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool {
|
|||||||
tbl: table.Table
|
tbl: table.Table
|
||||||
table.init(&tbl, context.temp_allocator, context.temp_allocator)
|
table.init(&tbl, context.temp_allocator, context.temp_allocator)
|
||||||
table.padding(&tbl, 2, 0)
|
table.padding(&tbl, 2, 0)
|
||||||
if write_flags_table(&tbl, info.flags) {
|
write_flags_table(&tbl, info.flags)
|
||||||
fmt.wprintf(w, "\n", flush = false)
|
fmt.wprintf(w, "\n", flush = false)
|
||||||
write_borderless_table(w, &tbl)
|
write_borderless_table(w, &tbl)
|
||||||
}
|
|
||||||
table_reset(&tbl)
|
table_reset(&tbl)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
flag_field_info :: proc(
|
Flag_Kind :: enum {
|
||||||
ft: Flag_Type,
|
Bool,
|
||||||
) -> (
|
String,
|
||||||
names: string,
|
Enum,
|
||||||
value_hint: string,
|
}
|
||||||
description: string,
|
|
||||||
) {
|
Flag_Field :: struct {
|
||||||
|
long_name: string,
|
||||||
|
short_name: string,
|
||||||
|
kind: Flag_Kind,
|
||||||
|
usage: string,
|
||||||
|
default_val: string,
|
||||||
|
enum_values: string,
|
||||||
|
completion: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
flag_field :: proc(ft: Flag_Type) -> Flag_Field {
|
||||||
field := reflect.struct_field_at(Flags, int(ft))
|
field := reflect.struct_field_at(Flags, int(ft))
|
||||||
|
|
||||||
args_tag := reflect.struct_tag_get(field.tag, "args")
|
args_tag := reflect.struct_tag_get(field.tag, "args")
|
||||||
@@ -230,45 +267,81 @@ flag_field_info :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
short, has_short := get_subtag(args_tag, "short")
|
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)
|
base_ti := runtime.type_info_base(field.type)
|
||||||
|
kind: Flag_Kind
|
||||||
|
enum_values: string
|
||||||
|
|
||||||
if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool {
|
if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool {
|
||||||
value_hint = ""
|
kind = .Bool
|
||||||
} else if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
|
} else if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
|
||||||
value_hint = " <value>"
|
kind = .String
|
||||||
} else if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
|
} else if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
|
||||||
|
kind = .Enum
|
||||||
parts := make([dynamic]string, 0, len(enum_ti.names), context.temp_allocator)
|
parts := make([dynamic]string, 0, len(enum_ti.names), context.temp_allocator)
|
||||||
for name in enum_ti.names {
|
for name in enum_ti.names {
|
||||||
lower := strings.to_lower(name, context.temp_allocator)
|
lower := strings.to_lower(name, context.temp_allocator)
|
||||||
append(&parts, fmt.tprintf("'%s'", lower))
|
append(&parts, fmt.tprintf("'%s'", lower))
|
||||||
}
|
}
|
||||||
value_hint = fmt.tprintf(" %s", strings.join(parts[:], "|", context.temp_allocator))
|
enum_values = strings.join(parts[:], "|", context.temp_allocator)
|
||||||
delete(parts)
|
delete(parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
usage := reflect.struct_tag_get(field.tag, "usage")
|
usage := reflect.struct_tag_get(field.tag, "usage")
|
||||||
default_val := reflect.struct_tag_get(field.tag, "default")
|
default_val := reflect.struct_tag_get(field.tag, "default")
|
||||||
|
completion := reflect.struct_tag_get(field.tag, "completion")
|
||||||
|
|
||||||
description = usage
|
return {
|
||||||
if len(default_val) > 0 {
|
long_name = long_name,
|
||||||
if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
|
short_name = has_short ? short : "",
|
||||||
description = fmt.tprintf(`%s (default "%s")`, usage, default_val)
|
kind = kind,
|
||||||
} else if _, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
|
usage = usage,
|
||||||
description = fmt.tprintf("%s (default '%s')", usage, default_val)
|
default_val = default_val,
|
||||||
|
enum_values = enum_values,
|
||||||
|
completion = completion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flag_field_info :: proc(
|
||||||
|
ft: Flag_Type,
|
||||||
|
) -> (
|
||||||
|
names: string,
|
||||||
|
value_hint: string,
|
||||||
|
description: string,
|
||||||
|
) {
|
||||||
|
f := flag_field(ft)
|
||||||
|
|
||||||
|
if len(f.short_name) > 0 {
|
||||||
|
names = fmt.tprintf("-%s, --%s", f.short_name, f.long_name)
|
||||||
|
} else {
|
||||||
|
names = fmt.tprintf("--%s", f.long_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch f.kind {
|
||||||
|
case .Bool:
|
||||||
|
value_hint = ""
|
||||||
|
case .String:
|
||||||
|
value_hint = " <value>"
|
||||||
|
case .Enum:
|
||||||
|
value_hint = fmt.tprintf(" %s", f.enum_values)
|
||||||
|
}
|
||||||
|
|
||||||
|
description = f.usage
|
||||||
|
if len(f.default_val) > 0 {
|
||||||
|
switch f.kind {
|
||||||
|
case .Bool:
|
||||||
|
// do nothing
|
||||||
|
case .String:
|
||||||
|
description = fmt.tprintf(`%s (default "%s")`, f.usage, f.default_val)
|
||||||
|
case .Enum:
|
||||||
|
description = fmt.tprintf("%s (default '%s')", f.usage, f.default_val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) -> (has_rows: bool) {
|
write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) {
|
||||||
if flags == {} do return false
|
|
||||||
table.caption(tbl, "Flags:")
|
table.caption(tbl, "Flags:")
|
||||||
for ft in Flag_Type {
|
for ft in Flag_Type {
|
||||||
if ft not_in flags do continue
|
if ft not_in flags do continue
|
||||||
@@ -285,7 +358,6 @@ write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) -> (has_
|
|||||||
table.row(tbl, colorize(.Flag, names, tbl.format_allocator), desc)
|
table.row(tbl, colorize(.Flag, names, tbl.format_allocator), desc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
find_command :: proc(name: string) -> (CommandInfo, bool) {
|
find_command :: proc(name: string) -> (CommandInfo, bool) {
|
||||||
@@ -365,9 +437,8 @@ at before, restore your backup with:
|
|||||||
write_borderless_table(w, &tbl)
|
write_borderless_table(w, &tbl)
|
||||||
table_reset(&tbl)
|
table_reset(&tbl)
|
||||||
|
|
||||||
if write_flags_table(&tbl, GLOBAL_FLAGS) {
|
write_flags_table(&tbl, GLOBAL_FLAGS)
|
||||||
write_borderless_table(w, &tbl)
|
write_borderless_table(w, &tbl)
|
||||||
}
|
|
||||||
table_reset(&tbl)
|
table_reset(&tbl)
|
||||||
|
|
||||||
fmt.wprintf(
|
fmt.wprintf(
|
||||||
|
|||||||
@@ -1,10 +1,99 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
COMPLETION_SCRIPT: string : string(#load("mod.nu"))
|
nushell_header :: `def tracked-paths [] {
|
||||||
|
(
|
||||||
|
^envr list
|
||||||
|
| from json
|
||||||
|
| each {
|
||||||
|
[$in.directory $in.path] | path join
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def untracked-paths [] {
|
||||||
|
(
|
||||||
|
^envr scan
|
||||||
|
| from json
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def color [] {
|
||||||
|
['auto' 'always' 'never']
|
||||||
|
}
|
||||||
|
|
||||||
|
def output [] {
|
||||||
|
['auto' 'table' 'json']
|
||||||
|
}
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
cmd_nushell_completion :: proc(cmd: ^Command) {
|
cmd_nushell_completion :: proc(cmd: ^Command) {
|
||||||
fmt.wprint(cmd.out, COMPLETION_SCRIPT, flush = false)
|
fmt.wprint(cmd.out, generate_nushell_completion(), flush = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_nushell_completion :: proc() -> string {
|
||||||
|
sb: strings.Builder
|
||||||
|
strings.builder_init(&sb, context.temp_allocator)
|
||||||
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
|
fmt.sbprint(&sb, nushell_header)
|
||||||
|
|
||||||
|
for c in COMMANDS {
|
||||||
|
fmt.sbprintf(&sb, "# %s\n", c.short)
|
||||||
|
fmt.sbprintf(&sb, "export extern \"envr %s\" [\n", c.name)
|
||||||
|
for ft in Flag_Type {
|
||||||
|
if ft not_in c.flags do continue
|
||||||
|
f := flag_field(ft)
|
||||||
|
fmt.sbprintf(&sb, " %s\n", nushell_flag_line(f))
|
||||||
|
}
|
||||||
|
for arg in c.args {
|
||||||
|
fmt.sbprintf(&sb, " %s\n", nushell_positional_line(arg))
|
||||||
|
}
|
||||||
|
fmt.sbprintf(&sb, "]\n")
|
||||||
|
|
||||||
|
for a in c.aliases {
|
||||||
|
fmt.sbprintf(&sb, "\nexport alias \"envr %s\" = envr %s\n", a, c.name)
|
||||||
|
}
|
||||||
|
fmt.sbprintf(&sb, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.to_string(sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
nushell_flag_line :: proc(f: Flag_Field) -> string {
|
||||||
|
line: string
|
||||||
|
if len(f.short_name) > 0 {
|
||||||
|
line = fmt.tprintf("--%s(-%s)", f.long_name, f.short_name)
|
||||||
|
} else {
|
||||||
|
line = fmt.tprintf("--%s", f.long_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch f.kind {
|
||||||
|
case .Bool:
|
||||||
|
case .String:
|
||||||
|
line = fmt.tprintf("%s: path", line)
|
||||||
|
case .Enum:
|
||||||
|
if len(f.completion) > 0 {
|
||||||
|
line = fmt.tprintf("%s: string@%s", line, f.completion)
|
||||||
|
} else {
|
||||||
|
line = fmt.tprintf("%s: string", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.tprintf("%s # %s", line, f.usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
nushell_positional_line :: proc(arg: Positional_Arg) -> string {
|
||||||
|
name := arg.name
|
||||||
|
if arg.optional {
|
||||||
|
name = fmt.tprintf("%s?", name)
|
||||||
|
}
|
||||||
|
if len(arg.completion) > 0 {
|
||||||
|
return fmt.tprintf("%s: path@%s", name, arg.completion)
|
||||||
|
}
|
||||||
|
return fmt.tprintf("%s: path", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,31 +7,72 @@ import "core:testing"
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_nushell_completion_nonempty :: proc(t: ^testing.T) {
|
test_nushell_completion_nonempty :: proc(t: ^testing.T) {
|
||||||
testing.expect(t, len(COMPLETION_SCRIPT) > 0, "completion script should not be empty")
|
script := generate_nushell_completion()
|
||||||
|
testing.expect(t, len(script) > 0, "completion script should not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_nushell_completion_contains_externs :: proc(t: ^testing.T) {
|
test_nushell_completion_contains_commands :: proc(t: ^testing.T) {
|
||||||
|
script := generate_nushell_completion()
|
||||||
expected := []string{
|
expected := []string{
|
||||||
"tracked-paths",
|
"tracked-paths",
|
||||||
"untracked-paths",
|
"untracked-paths",
|
||||||
"envr backup",
|
"envr backup",
|
||||||
"envr check",
|
"envr check",
|
||||||
"envr edit-config",
|
"envr edit-config",
|
||||||
"envr help",
|
|
||||||
"envr init",
|
"envr init",
|
||||||
"envr list",
|
"envr list",
|
||||||
"envr remove",
|
"envr remove",
|
||||||
"envr restore",
|
"envr restore",
|
||||||
"envr scan",
|
"envr scan",
|
||||||
"envr sync",
|
"envr sync",
|
||||||
|
"envr version",
|
||||||
"envr nushell-completion",
|
"envr nushell-completion",
|
||||||
}
|
}
|
||||||
for ext in expected {
|
for ext in expected {
|
||||||
testing.expect(
|
testing.expect(
|
||||||
t,
|
t,
|
||||||
strings.contains(COMPLETION_SCRIPT, ext),
|
strings.contains(script, ext),
|
||||||
fmt.tprintf("expected script to contain %q", ext),
|
fmt.tprintf("expected script to contain %q", ext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_nushell_completion_contains_flags :: proc(t: ^testing.T) {
|
||||||
|
script := generate_nushell_completion()
|
||||||
|
expected_flags := []string{
|
||||||
|
"--help(-h)",
|
||||||
|
"--config-file(-c)",
|
||||||
|
"--color",
|
||||||
|
"--force(-f)",
|
||||||
|
"--output(-o)",
|
||||||
|
}
|
||||||
|
for flag in expected_flags {
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
strings.contains(script, flag),
|
||||||
|
fmt.tprintf("expected script to contain %q", flag),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_nushell_completion_contains_aliases :: proc(t: ^testing.T) {
|
||||||
|
script := generate_nushell_completion()
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
strings.contains(script, "envr add"),
|
||||||
|
"expected script to contain 'envr add' alias",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_nushell_completion_no_help_command :: proc(t: ^testing.T) {
|
||||||
|
script := generate_nushell_completion()
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
!strings.contains(script, "envr help"),
|
||||||
|
"script should not contain 'envr help' (not a real command)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+4
-3
@@ -201,8 +201,7 @@ find_git_roots :: proc(
|
|||||||
ok: bool,
|
ok: bool,
|
||||||
) {
|
) {
|
||||||
paths := search_paths(cfg, allocator)
|
paths := search_paths(cfg, allocator)
|
||||||
// TODO: Pass allocator to findr
|
findr.find_repos(paths[:], &roots, os.get_processor_core_count(), allocator)
|
||||||
findr.find_repos(paths[:], &roots, os.get_processor_core_count())
|
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -214,6 +213,7 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st
|
|||||||
}
|
}
|
||||||
|
|
||||||
paths := new_clone(cfg.scan_config.include, allocator)
|
paths := new_clone(cfg.scan_config.include, allocator)
|
||||||
|
defer free(paths, allocator)
|
||||||
|
|
||||||
for &include in paths {
|
for &include in paths {
|
||||||
expanded, _ := strings.replace(include, "~", home, 1, allocator)
|
expanded, _ := strings.replace(include, "~", home, 1, allocator)
|
||||||
@@ -227,7 +227,8 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return paths^
|
result := paths^
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
envr_dir :: proc(config_path: string) -> string {
|
envr_dir :: proc(config_path: string) -> string {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
import "core:mem"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:path/filepath"
|
import "core:path/filepath"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
@@ -194,3 +195,14 @@ test_search_paths_expands_tilde :: proc(t: ^testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_search_paths_no_leak :: proc(t: ^testing.T) {
|
||||||
|
cfg := Config {
|
||||||
|
scan_config = ScanConfig{include = make([dynamic]string, 0, 1)},
|
||||||
|
}
|
||||||
|
defer delete(cfg.scan_config.include)
|
||||||
|
append(&cfg.scan_config.include, "/tmp")
|
||||||
|
|
||||||
|
_ = search_paths(cfg, context.allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import "core:mem"
|
|||||||
import "core:os"
|
import "core:os"
|
||||||
|
|
||||||
MAGIC :: "ENVR"
|
MAGIC :: "ENVR"
|
||||||
MAGIC_BYTES := [4]u8{u8('E'), u8('N'), u8('V'), u8('R')}
|
MAGIC_BYTES: [4]u8 = MAGIC
|
||||||
|
|
||||||
RECIPIENT_ENTRY_SIZE ::
|
RECIPIENT_ENTRY_SIZE ::
|
||||||
CRYPTO_BOX_PUBLICKEY_BYTES +
|
CRYPTO_BOX_PUBLICKEY_BYTES +
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import "base:runtime"
|
|||||||
import "core:crypto/hash"
|
import "core:crypto/hash"
|
||||||
import "core:encoding/hex"
|
import "core:encoding/hex"
|
||||||
import "core:encoding/ini"
|
import "core:encoding/ini"
|
||||||
import "core:encoding/json"
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:mem"
|
import "core:mem"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
@@ -222,7 +221,6 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
|
|||||||
allocator := db_allocator(db)
|
allocator := db_allocator(db)
|
||||||
results := make([dynamic]EnvFile, 0, 10, allocator)
|
results := make([dynamic]EnvFile, 0, 10, allocator)
|
||||||
|
|
||||||
migrate := false
|
|
||||||
for {
|
for {
|
||||||
rc = sqlite.step(stmt)
|
rc = sqlite.step(stmt)
|
||||||
if rc == sqlite.DONE {
|
if rc == sqlite.DONE {
|
||||||
@@ -233,24 +231,13 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
|
|||||||
#no_bounds_check return results[:], false
|
#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))
|
remotes_raw := string(sqlite.column_text(stmt, 1))
|
||||||
if len(remotes_raw) > 0 {
|
split := strings.split_lines(remotes_raw, context.temp_allocator)
|
||||||
if remotes_raw[0] == '[' {
|
remotes := make([dynamic]string, 0, len(split), allocator = allocator)
|
||||||
err := json.unmarshal_string(remotes_raw, &remotes, allocator = allocator)
|
for s in split {
|
||||||
if err != nil {
|
append(&remotes, strings.clone(s, allocator))
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
||||||
|
|
||||||
append(
|
append(
|
||||||
@@ -265,10 +252,6 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if migrate {
|
|
||||||
migrate_remotes(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
#no_bounds_check return results[:], true
|
#no_bounds_check return results[:], true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,33 +347,15 @@ db_fetch :: proc(db: ^Db, path: string) -> (EnvFile, bool) {
|
|||||||
return EnvFile{}, false
|
return EnvFile{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove json support after next major release
|
|
||||||
migrate := false
|
|
||||||
remotes: [dynamic]string = ---
|
|
||||||
remotes_raw := string(sqlite.column_text(stmt, 1))
|
remotes_raw := string(sqlite.column_text(stmt, 1))
|
||||||
if len(remotes_raw) > 0 {
|
split := strings.split_lines(remotes_raw, context.temp_allocator)
|
||||||
if remotes_raw[0] == '[' {
|
remotes := make([dynamic]string, 0, len(split), allocator = allocator)
|
||||||
err := json.unmarshal_string(remotes_raw, &remotes, allocator = allocator)
|
for s in split {
|
||||||
if err != nil {
|
append(&remotes, strings.clone(s, allocator))
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
||||||
|
|
||||||
if migrate {
|
|
||||||
migrate_remotes(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
return EnvFile {
|
return EnvFile {
|
||||||
path = file_path,
|
path = file_path,
|
||||||
dir = filepath.dir(file_path),
|
dir = filepath.dir(file_path),
|
||||||
@@ -525,29 +490,8 @@ db_persist :: proc(db: ^Db, f: ^EnvFile, old_path: string) -> bool {
|
|||||||
return db_insert(db, f^)
|
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) {
|
try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, SyncError) {
|
||||||
roots, ok := find_git_roots(db.cfg)
|
roots, ok := find_git_roots(db.cfg, context.allocator)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, .GitRootFailed
|
return false, .GitRootFailed
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -16,7 +16,14 @@ RepoPool :: struct {
|
|||||||
threads: []^thread.Thread,
|
threads: []^thread.Thread,
|
||||||
}
|
}
|
||||||
|
|
||||||
find_repos :: proc(roots: []string, results: ^[dynamic]string, thread_count: int) {
|
find_repos :: proc(
|
||||||
|
roots: []string,
|
||||||
|
results: ^[dynamic]string,
|
||||||
|
thread_count: int,
|
||||||
|
allocator := context.allocator,
|
||||||
|
) {
|
||||||
|
// TODO: This may be a code smell
|
||||||
|
context.allocator = allocator
|
||||||
if len(roots) == 0 do return
|
if len(roots) == 0 do return
|
||||||
|
|
||||||
pool := new(RepoPool)
|
pool := new(RepoPool)
|
||||||
@@ -126,3 +133,4 @@ process_repo_dir :: proc(pool: ^RepoPool, dir_path: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -78,7 +78,15 @@ collect_worker :: proc(t: ^thread.Thread) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
walk :: proc(roots: []string, results: ^[dynamic]string, opts: WalkOptions, thread_count: int) {
|
walk :: proc(
|
||||||
|
roots: []string,
|
||||||
|
results: ^[dynamic]string,
|
||||||
|
opts: WalkOptions,
|
||||||
|
thread_count: int,
|
||||||
|
allocator := context.allocator,
|
||||||
|
) {
|
||||||
|
// TODO: This may be a code smell
|
||||||
|
context.allocator = allocator
|
||||||
if len(roots) == 0 do return
|
if len(roots) == 0 do return
|
||||||
|
|
||||||
ch, _ := chan.create(chan.Chan([]u8), max(2 * thread_count, 2), context.allocator)
|
ch, _ := chan.create(chan.Chan([]u8), max(2 * thread_count, 2), context.allocator)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
packages.default = pkgs.stdenv.mkDerivation rec {
|
packages.default = pkgs.stdenv.mkDerivation rec {
|
||||||
pname = "envr";
|
pname = "envr";
|
||||||
version = "0.3.0";
|
version = nixpkgs.lib.trim (builtins.readFile ./version.txt);
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
# envr command extern definitions for Nushell
|
|
||||||
# A tool for managing environment files and backups
|
|
||||||
|
|
||||||
export def tracked-paths [] {
|
|
||||||
(
|
|
||||||
^envr list
|
|
||||||
| from json
|
|
||||||
| each {
|
|
||||||
[$in.directory $in.path] | path join
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export def untracked-paths [] {
|
|
||||||
(
|
|
||||||
^envr scan
|
|
||||||
| from json
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export extern envr [
|
|
||||||
...args: any
|
|
||||||
--help(-h) # Show help information
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr backup" [
|
|
||||||
--help(-h) # Show help for backup command
|
|
||||||
path: path@untracked-paths # Path to .env file to backup
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr check" [
|
|
||||||
--help(-h) # Show help for check command
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr edit-config" [
|
|
||||||
--help(-h) # Show help for edit-config command
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr help" [
|
|
||||||
command?: string # Show help for specific command
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr init" [
|
|
||||||
--help(-h) # Show help for init command
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr list" [
|
|
||||||
--help(-h) # Show help for list command
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr remove" [
|
|
||||||
--help(-h) # Show help for remove command
|
|
||||||
path: path@tracked-paths
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr restore" [
|
|
||||||
--help(-h) # Show help for restore command
|
|
||||||
path: path@tracked-paths
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr scan" [
|
|
||||||
--help(-h) # Show help for scan command
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr sync" [
|
|
||||||
--help(-h) # Show help for sync command
|
|
||||||
]
|
|
||||||
|
|
||||||
export extern "envr nushell-completion" [
|
|
||||||
--help(-h) # Show help for nushell-completion command
|
|
||||||
]
|
|
||||||
+76
-26
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
import "core:strings"
|
||||||
import "core:sys/posix"
|
import "core:sys/posix"
|
||||||
import "core:terminal/ansi"
|
import "core:terminal/ansi"
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ Key :: enum {
|
|||||||
Space,
|
Space,
|
||||||
Enter,
|
Enter,
|
||||||
Escape,
|
Escape,
|
||||||
|
Backspace,
|
||||||
|
Char,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,11 +41,17 @@ multi_select :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
selected = make([dynamic]bool, len(options))
|
selected = make([dynamic]bool, len(options))
|
||||||
|
filter: [dynamic]u8
|
||||||
|
defer delete(filter)
|
||||||
|
filtered: [dynamic]int
|
||||||
|
defer delete(filtered)
|
||||||
|
rebuild_filtered(options, filter[:], &filtered)
|
||||||
|
|
||||||
cursor: int = 0
|
cursor: int = 0
|
||||||
scroll_offset: int = 0
|
scroll_offset: int = 0
|
||||||
|
|
||||||
fmt.printf(ansi.CSI + ansi.DECTCEM_HIDE)
|
fmt.printf(ansi.CSI + ansi.DECTCEM_HIDE)
|
||||||
visible := render_options(prompt, options, selected[:], cursor, scroll_offset)
|
visible := render_options(prompt, options, selected[:], filtered[:], string(filter[:]), cursor, scroll_offset)
|
||||||
|
|
||||||
raw, ok := enable_raw_mode(posix.STDIN_FILENO)
|
raw, ok := enable_raw_mode(posix.STDIN_FILENO)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -52,19 +61,31 @@ multi_select :: proc(
|
|||||||
defer disable_raw_mode(&raw)
|
defer disable_raw_mode(&raw)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
key := read_key()
|
key, ch := read_key()
|
||||||
|
|
||||||
switch key {
|
switch key {
|
||||||
|
case .Char:
|
||||||
|
append(&filter, u8(ch))
|
||||||
|
rebuild_filtered(options, filter[:], &filtered)
|
||||||
|
cursor = clamp(cursor, 0, max(0, len(filtered) - 1))
|
||||||
|
case .Backspace:
|
||||||
|
if len(filter) > 0 {
|
||||||
|
pop(&filter)
|
||||||
|
rebuild_filtered(options, filter[:], &filtered)
|
||||||
|
cursor = clamp(cursor, 0, max(0, len(filtered) - 1))
|
||||||
|
}
|
||||||
case .Up:
|
case .Up:
|
||||||
if cursor > 0 {
|
if cursor > 0 {
|
||||||
cursor -= 1
|
cursor -= 1
|
||||||
}
|
}
|
||||||
case .Down:
|
case .Down:
|
||||||
if cursor < len(options) - 1 {
|
if cursor < len(filtered) - 1 {
|
||||||
cursor += 1
|
cursor += 1
|
||||||
}
|
}
|
||||||
case .Space:
|
case .Space:
|
||||||
selected[cursor] = !selected[cursor]
|
if len(filtered) > 0 {
|
||||||
|
selected[filtered[cursor]] = !selected[filtered[cursor]]
|
||||||
|
}
|
||||||
case .Enter:
|
case .Enter:
|
||||||
fmt.printf(
|
fmt.printf(
|
||||||
ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW,
|
ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW,
|
||||||
@@ -82,9 +103,21 @@ multi_select :: proc(
|
|||||||
case .Unknown:
|
case .Unknown:
|
||||||
}
|
}
|
||||||
|
|
||||||
scroll_offset = max(0, min(cursor - MAX_VISIBLE / 2, len(options) - MAX_VISIBLE))
|
scroll_offset = max(0, min(cursor - MAX_VISIBLE / 2, len(filtered) - MAX_VISIBLE))
|
||||||
fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.RESET + ansi.ED, visible + 1)
|
fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.RESET + ansi.ED, visible + 1)
|
||||||
visible = render_options(prompt, options, selected[:], cursor, scroll_offset)
|
visible = render_options(prompt, options, selected[:], filtered[:], string(filter[:]), cursor, scroll_offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuild_filtered :: proc(options: []string, filter: []u8, filtered: ^[dynamic]int) {
|
||||||
|
clear(filtered)
|
||||||
|
filter_str := string(filter)
|
||||||
|
filter_lower := strings.to_lower(filter_str, context.temp_allocator)
|
||||||
|
for opt, i in options {
|
||||||
|
opt_lower := strings.to_lower(opt, context.temp_allocator)
|
||||||
|
if strings.contains(opt_lower, filter_lower) {
|
||||||
|
append(filtered, i)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,22 +125,34 @@ render_options :: proc(
|
|||||||
prompt: string,
|
prompt: string,
|
||||||
options: []string,
|
options: []string,
|
||||||
selected: []bool,
|
selected: []bool,
|
||||||
|
filtered: []int,
|
||||||
|
filter_text: string,
|
||||||
cursor: int,
|
cursor: int,
|
||||||
scroll_offset: int,
|
scroll_offset: int,
|
||||||
) -> int {
|
) -> int {
|
||||||
fmt.printf(
|
fmt.printf(
|
||||||
"%s (↑/↓ move, space select, enter confirm)\r\n",
|
"%s (type to filter, ↑/↓ move, space select, enter confirm)\r\n",
|
||||||
colorize(.Option_Label, prompt),
|
colorize(.Option_Label, prompt),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fmt.printf("filter: %s\r\n", filter_text)
|
||||||
|
|
||||||
|
line_count := 1
|
||||||
|
|
||||||
|
if len(filtered) == 0 {
|
||||||
|
fmt.printf(" No matches\r\n")
|
||||||
|
return line_count + 1
|
||||||
|
}
|
||||||
|
|
||||||
end := scroll_offset + MAX_VISIBLE
|
end := scroll_offset + MAX_VISIBLE
|
||||||
if end > len(options) {
|
if end > len(filtered) {
|
||||||
end = len(options)
|
end = len(filtered)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in scroll_offset ..< end {
|
for i in scroll_offset ..< end {
|
||||||
|
original := filtered[i]
|
||||||
checkbox := " "
|
checkbox := " "
|
||||||
if selected[i] {
|
if selected[original] {
|
||||||
checkbox = "x"
|
checkbox = "x"
|
||||||
}
|
}
|
||||||
if i == cursor {
|
if i == cursor {
|
||||||
@@ -115,14 +160,14 @@ render_options :: proc(
|
|||||||
"%s [%s] %s\r\n",
|
"%s [%s] %s\r\n",
|
||||||
colorize(.Caret, ">"),
|
colorize(.Caret, ">"),
|
||||||
colorize(.Sucess, checkbox),
|
colorize(.Sucess, checkbox),
|
||||||
options[i],
|
options[original],
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
fmt.printf(" [%s] %s\r\n", colorize(.Sucess, checkbox), options[i])
|
fmt.printf(" [%s] %s\r\n", colorize(.Sucess, checkbox), options[original])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return end - scroll_offset
|
return line_count + (end - scroll_offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
enable_raw_mode :: proc(fd: posix.FD) -> (Raw_State, bool) {
|
enable_raw_mode :: proc(fd: posix.FD) -> (Raw_State, bool) {
|
||||||
@@ -152,21 +197,23 @@ disable_raw_mode :: proc(state: ^Raw_State) {
|
|||||||
posix.tcsetattr(state.fd, .TCSAFLUSH, &state.original)
|
posix.tcsetattr(state.fd, .TCSAFLUSH, &state.original)
|
||||||
}
|
}
|
||||||
|
|
||||||
read_key :: proc() -> Key {
|
read_key :: proc() -> (key: Key, ch: rune) {
|
||||||
buf: [3]u8
|
buf: [3]u8
|
||||||
|
|
||||||
n := posix.read(posix.STDIN_FILENO, &buf[0], 1)
|
n := posix.read(posix.STDIN_FILENO, &buf[0], 1)
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
return .Unknown
|
return .Unknown, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
switch buf[0] {
|
switch buf[0] {
|
||||||
case ' ':
|
case ' ':
|
||||||
return .Space
|
return .Space, 0
|
||||||
case '\n', '\r':
|
case '\n', '\r':
|
||||||
return .Enter
|
return .Enter, 0
|
||||||
case 0x03:
|
case 0x03:
|
||||||
return .Escape
|
return .Escape, 0
|
||||||
|
case 0x08, 0x7F:
|
||||||
|
return .Backspace, 0
|
||||||
case 0x1b:
|
case 0x1b:
|
||||||
tv: posix.timeval
|
tv: posix.timeval
|
||||||
tv.tv_sec = 0
|
tv.tv_sec = 0
|
||||||
@@ -178,12 +225,12 @@ read_key :: proc() -> Key {
|
|||||||
|
|
||||||
ready := posix.select(1, &set, nil, nil, &tv)
|
ready := posix.select(1, &set, nil, nil, &tv)
|
||||||
if ready <= 0 {
|
if ready <= 0 {
|
||||||
return .Escape
|
return .Escape, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
n2 := posix.read(posix.STDIN_FILENO, &buf[1], 1)
|
n2 := posix.read(posix.STDIN_FILENO, &buf[1], 1)
|
||||||
if n2 <= 0 || buf[1] != '[' {
|
if n2 <= 0 || buf[1] != '[' {
|
||||||
return .Escape
|
return .Escape, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
posix.FD_ZERO(&set)
|
posix.FD_ZERO(&set)
|
||||||
@@ -193,24 +240,27 @@ read_key :: proc() -> Key {
|
|||||||
|
|
||||||
ready = posix.select(1, &set, nil, nil, &tv)
|
ready = posix.select(1, &set, nil, nil, &tv)
|
||||||
if ready <= 0 {
|
if ready <= 0 {
|
||||||
return .Escape
|
return .Escape, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
n3 := posix.read(posix.STDIN_FILENO, &buf[2], 1)
|
n3 := posix.read(posix.STDIN_FILENO, &buf[2], 1)
|
||||||
if n3 <= 0 {
|
if n3 <= 0 {
|
||||||
return .Escape
|
return .Escape, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
switch buf[2] {
|
switch buf[2] {
|
||||||
case 'A':
|
case 'A':
|
||||||
return .Up
|
return .Up, 0
|
||||||
case 'B':
|
case 'B':
|
||||||
return .Down
|
return .Down, 0
|
||||||
case:
|
case:
|
||||||
return .Escape
|
return .Escape, 0
|
||||||
}
|
}
|
||||||
case:
|
case:
|
||||||
return .Unknown
|
if buf[0] >= 0x20 && buf[0] <= 0x7E {
|
||||||
|
return .Char, rune(buf[0])
|
||||||
|
}
|
||||||
|
return .Unknown, 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,18 +35,18 @@ parse_ssh_public_key :: proc(pub_path: string) -> (pub: [32]u8, ok: bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := 0
|
rest := decoded
|
||||||
key_type, type_ok := read_wire_string(decoded, &offset)
|
key_type, type_ok := read_wire_string(&rest)
|
||||||
if !type_ok || key_type != SSH_ED25519 {
|
if !type_ok || string(key_type) != SSH_ED25519 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pk_data, pk_ok := read_wire_string(decoded, &offset)
|
pk_data, pk_ok := read_wire_string(&rest)
|
||||||
if !pk_ok || len(pk_data) != 32 {
|
if !pk_ok || len(pk_data) != 32 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mem.copy_non_overlapping(&pub[0], raw_data(pk_data), 32)
|
mem.copy_non_overlapping(&pub[0], &pk_data[0], 32)
|
||||||
|
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
@@ -91,81 +91,63 @@ parse_ssh_private_key :: proc(priv_path: string) -> (kp: Ed25519Keypair, ok: boo
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := len(magic)
|
rest := decoded[len(magic):]
|
||||||
|
|
||||||
ciphername, cipher_ok := read_wire_string(decoded, &offset)
|
ciphername, cipher_ok := read_wire_string(&rest)
|
||||||
if !cipher_ok || ciphername != "none" {
|
if !cipher_ok || string(ciphername) != "none" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
kdfname, kdf_ok := read_wire_string(decoded, &offset)
|
kdfname, kdf_ok := read_wire_string(&rest)
|
||||||
if !kdf_ok || kdfname != "none" {
|
if !kdf_ok || string(kdfname) != "none" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, opts_ok := read_wire_string(decoded, &offset)
|
_, opts_ok := read_wire_string(&rest)
|
||||||
if !opts_ok {
|
if !opts_ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if offset + 4 > len(decoded) {
|
num_keys, nkeys_ok := read_wire_u32(&rest)
|
||||||
|
if !nkeys_ok || num_keys != 1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
num_keys := endian.get_u32(decoded[offset:offset + 4], .Big) or_return
|
_, pub_blob_ok := read_wire_string(&rest)
|
||||||
offset += 4
|
|
||||||
|
|
||||||
if num_keys != 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, pub_blob_ok := read_wire_string(decoded, &offset)
|
|
||||||
if !pub_blob_ok {
|
if !pub_blob_ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
priv_blob, priv_blob_ok := read_wire_string(decoded, &offset)
|
priv_blob, priv_blob_ok := read_wire_string(&rest)
|
||||||
if !priv_blob_ok {
|
if !priv_blob_ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
inner_offset := 0
|
inner := priv_blob
|
||||||
if inner_offset + 8 > len(priv_blob) {
|
|
||||||
|
check1, c1_ok := read_wire_u32(&inner)
|
||||||
|
check2, c2_ok := read_wire_u32(&inner)
|
||||||
|
if !c1_ok || !c2_ok || check1 != check2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
check1 := endian.get_u32(
|
priv_type, type_ok := read_wire_string(&inner)
|
||||||
transmute([]u8)(priv_blob)[inner_offset:inner_offset + 4],
|
if !type_ok || string(priv_type) != SSH_ED25519 {
|
||||||
.Big,
|
|
||||||
) or_return
|
|
||||||
inner_offset += 4
|
|
||||||
check2 := endian.get_u32(
|
|
||||||
transmute([]u8)(priv_blob)[inner_offset:inner_offset + 4],
|
|
||||||
.Big,
|
|
||||||
) or_return
|
|
||||||
inner_offset += 4
|
|
||||||
|
|
||||||
if check1 != check2 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
priv_type, type_ok := read_wire_string(transmute([]u8)priv_blob, &inner_offset)
|
pub_wire, pub_ok := read_wire_string(&inner)
|
||||||
if !type_ok || priv_type != SSH_ED25519 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pub_wire, pub_ok := read_wire_string(transmute([]u8)priv_blob, &inner_offset)
|
|
||||||
if !pub_ok || len(pub_wire) != 32 {
|
if !pub_ok || len(pub_wire) != 32 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mem.copy_non_overlapping(&kp.Public[0], raw_data(pub_wire), 32)
|
mem.copy_non_overlapping(&kp.Public[0], &pub_wire[0], 32)
|
||||||
|
|
||||||
priv_wire, priv_ok := read_wire_string(transmute([]u8)priv_blob, &inner_offset)
|
priv_wire, priv_ok := read_wire_string(&inner)
|
||||||
if !priv_ok || len(priv_wire) != 64 {
|
if !priv_ok || len(priv_wire) != 64 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mem.copy_non_overlapping(&kp.Private[0], raw_data(priv_wire), 32)
|
mem.copy_non_overlapping(&kp.Private[0], &priv_wire[0], 32)
|
||||||
|
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
@@ -182,19 +164,28 @@ is_ed25519_key :: proc(
|
|||||||
return ok, nil
|
return ok, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
read_wire_string :: proc(data: []u8, offset: ^int) -> (s: string, ok: bool) {
|
read_wire_string :: proc(data: ^[]u8) -> (s: []u8, ok: bool) #no_bounds_check {
|
||||||
if offset^ + 4 > len(data) {
|
if len(data^) < 4 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
length := endian.get_u32(data[offset^:offset^ + 4], .Big) or_return
|
length := endian.get_u32(data^[:4], .Big) or_return
|
||||||
offset^ += 4
|
data^ = data^[4:]
|
||||||
|
|
||||||
if offset^ + int(length) > len(data) {
|
if len(data^) < int(length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s = data^[:int(length)]
|
||||||
|
data^ = data^[int(length):]
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s = string(data[offset^:offset^ + int(length)])
|
read_wire_u32 :: proc(data: ^[]u8) -> (v: u32, ok: bool) #no_bounds_check {
|
||||||
offset^ += int(length)
|
if len(data^) < 4 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v = endian.get_u32(data^[:4], .Big) or_return
|
||||||
|
data^ = data^[4:]
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -52,15 +52,15 @@ test_private_key_pub_matches_public_key :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_read_wire_string :: proc(t: ^testing.T) {
|
test_read_wire_string :: proc(t: ^testing.T) {
|
||||||
data := []u8{0, 0, 0, 5, u8('h'), u8('e'), u8('l'), u8('l'), u8('o'), 0, 0, 0, 0}
|
data := []u8{0, 0, 0, 5, u8('h'), u8('e'), u8('l'), u8('l'), u8('o'), 0, 0, 0, 0}
|
||||||
offset := 0
|
|
||||||
|
|
||||||
s, ok := read_wire_string(data, &offset)
|
buf := data
|
||||||
|
s, ok := read_wire_string(&buf)
|
||||||
testing.expect(t, ok, "expected read_wire_string to succeed")
|
testing.expect(t, ok, "expected read_wire_string to succeed")
|
||||||
testing.expect_value(t, s, "hello")
|
testing.expect_value(t, string(s), "hello")
|
||||||
testing.expect_value(t, offset, 9)
|
testing.expect_value(t, len(data) - len(buf), 9)
|
||||||
|
|
||||||
s2, ok2 := read_wire_string(data, &offset)
|
s2, ok2 := read_wire_string(&buf)
|
||||||
testing.expect(t, ok2, "expected second read to succeed")
|
testing.expect(t, ok2, "expected second read to succeed")
|
||||||
testing.expect_value(t, s2, "")
|
testing.expect_value(t, len(s2), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ test_ansi_aware_width_multiple_escape_sequences :: proc(t: ^testing.T) {
|
|||||||
colorize(.Heading, "b", disable = false),
|
colorize(.Heading, "b", disable = false),
|
||||||
colorize(.Heading, "c", disable = false),
|
colorize(.Heading, "c", disable = false),
|
||||||
},
|
},
|
||||||
|
context.temp_allocator,
|
||||||
)
|
)
|
||||||
testing.expect_value(t, ansi_aware_width(colored), 3)
|
testing.expect_value(t, ansi_aware_width(colored), 3)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user