mirror of
https://github.com/sbrow/envr.git
synced 2026-08-26 18:03:32 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d62123e831 | |||
| c50d490d9b | |||
| a4c96265f8 | |||
| 3813719c84 | |||
| 18e6b0b5b1 | |||
| 149902cf91 |
@@ -1,5 +1,38 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.0.0](https://github.com/sbrow/envr/compare/v0.4.0...v1.0.0) (2026-07-27)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* Renamed `nushell-completion` command to `completion nushell`.
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Added `uninstall` command. ([222a0a3](https://github.com/sbrow/envr/commit/222a0a3452b978d9bd14d6d2b91e40cd8f4959e3))
|
||||||
|
* Added bash completion support. ([e0a339e](https://github.com/sbrow/envr/commit/e0a339e8ec996c0e91e27357854abd9a78bd695a))
|
||||||
|
* Brought back the docs generator. ([86a4da6](https://github.com/sbrow/envr/commit/86a4da6cbaae698379c73b8345e6d238b2a11a46))
|
||||||
|
* Improved nushell wrapper output. ([209ec89](https://github.com/sbrow/envr/commit/209ec89addbed4718c5aed845ab20b386d1e425d))
|
||||||
|
* Scan results can now be filtered during selection. ([ae42d53](https://github.com/sbrow/envr/commit/ae42d53f029dd185143a645fb3f3a787051c1bb4))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Removed irrelevent flags from `nushell-completion` command help text. ([af1860c](https://github.com/sbrow/envr/commit/af1860c970604470992182bbfe9ecb15609a0acf))
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* Added `#no_bounds_check` to ssh parsers. ([f5624e9](https://github.com/sbrow/envr/commit/f5624e91f1236eecb263f28600ce7f4b2fda149f))
|
||||||
|
* Added a pre-alloc opportunity. ([997f11b](https://github.com/sbrow/envr/commit/997f11b257f1c981f73c6ae97d6e821147b5e2fc))
|
||||||
|
* Removed support for JSON formatted remotes. ([5282a44](https://github.com/sbrow/envr/commit/5282a4414b9a0211604553f2cb9434025aa5296b))
|
||||||
|
|
||||||
|
|
||||||
|
### Code Refactoring
|
||||||
|
|
||||||
|
* Renamed `nushell-completion` command to `completion nushell`. ([b0635e0](https://github.com/sbrow/envr/commit/b0635e035c44fb20f9b02265cff1ed8db30aa9d0))
|
||||||
|
|
||||||
## [0.4.0](https://github.com/sbrow/envr/compare/v0.3.0...v0.4.0) (2026-06-29)
|
## [0.4.0](https://github.com/sbrow/envr/compare/v0.3.0...v0.4.0) (2026-06-29)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ in an encyrpted sqlite database. Changes can be effortlessly synced with
|
|||||||
`envr` puts all your .env files in one safe place, so you can back them up with
|
`envr` puts all your .env files in one safe place, so you can back them up with
|
||||||
the tool [of your choosing](#backup-options).
|
the tool [of your choosing](#backup-options).
|
||||||
|
|
||||||
|
Please read the [AI Disclosure](#ai-disclosure) if you're concerned about AI code.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Encrypted Storage**: All `.env` files are encrypted using your ssh key and
|
- **Encrypted Storage**: All `.env` files are encrypted using your ssh key and
|
||||||
@@ -125,6 +127,22 @@ location.
|
|||||||
|
|
||||||
[restic](https://restic.readthedocs.io/en/latest/010_introduction.html).
|
[restic](https://restic.readthedocs.io/en/latest/010_introduction.html).
|
||||||
|
|
||||||
|
## AI Disclosure
|
||||||
|
|
||||||
|
This project was largely built by AI. The original project (written in go) was
|
||||||
|
written almost entirely with AI, using libraries that I requested (i.e. cobra).
|
||||||
|
|
||||||
|
I wrote it at work because I needed the tool and wanted it fast. The Odin version
|
||||||
|
was also primarily written by AI, but I stepped in a lot more, and took care to
|
||||||
|
architect the project in the way that I wanted, and made sure tests were written
|
||||||
|
to catch leaks.
|
||||||
|
|
||||||
|
Most of the documentation was written by hand, but I think the original `README.md`
|
||||||
|
was at least fleshed out by AI.
|
||||||
|
|
||||||
|
It is by no means perfect, but I have spent a lot of time doing code review and
|
||||||
|
testing to make sure the program (and documentation) meet my personal standards.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the [MIT License](./LICENSE).
|
This project is licensed under the [MIT License](./LICENSE).
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
7. Add purge command?
|
7. Add purge command?
|
||||||
|
|
||||||
|
8. Add docs for "building from source"
|
||||||
|
|
||||||
|
|
||||||
## Double-check AI output
|
## Double-check AI output
|
||||||
|
|
||||||
|
|||||||
@@ -97,14 +97,18 @@ key somewhere, otherwise your data could be lost forever.`,
|
|||||||
short = "Import a .env file into envr",
|
short = "Import a .env file into envr",
|
||||||
aliases = {"add"},
|
aliases = {"add"},
|
||||||
flags = GLOBAL_FLAGS,
|
flags = GLOBAL_FLAGS,
|
||||||
args = {{name = "path", completion = "untracked-paths", desc = "Path to .env file to backup"}},
|
args = {
|
||||||
|
{name = "path", completion = "untracked-paths", desc = "Path to .env file to backup"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name = "restore",
|
name = "restore",
|
||||||
usage = "envr restore <path>",
|
usage = "envr restore <path>",
|
||||||
short = "Restore a .env file from the database",
|
short = "Restore a .env file from the database",
|
||||||
flags = GLOBAL_FLAGS,
|
flags = GLOBAL_FLAGS,
|
||||||
args = {{name = "path", completion = "tracked-paths", desc = "Path to .env file to restore"}},
|
args = {
|
||||||
|
{name = "path", completion = "tracked-paths", desc = "Path to .env file to restore"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name = "list",
|
name = "list",
|
||||||
@@ -117,14 +121,22 @@ key somewhere, otherwise your data could be lost forever.`,
|
|||||||
usage = "envr remove <path>",
|
usage = "envr remove <path>",
|
||||||
short = "Remove a .env file from your database",
|
short = "Remove a .env file from your database",
|
||||||
flags = GLOBAL_FLAGS,
|
flags = GLOBAL_FLAGS,
|
||||||
args = {{name = "path", completion = "tracked-paths", desc = "Path to .env file to remove"}},
|
args = {
|
||||||
|
{name = "path", completion = "tracked-paths", desc = "Path to .env file to remove"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name = "check",
|
name = "check",
|
||||||
usage = "envr check [path]",
|
usage = "envr check [path]",
|
||||||
short = "Check if files are backed up",
|
short = "Check if files are backed up",
|
||||||
flags = GLOBAL_FLAGS,
|
flags = GLOBAL_FLAGS,
|
||||||
args = {{name = "path", optional = true, desc = "Path to check (defaults to current directory)"}},
|
args = {
|
||||||
|
{
|
||||||
|
name = "path",
|
||||||
|
optional = true,
|
||||||
|
desc = "Path to check (defaults to current directory)",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{name = "version", usage = "envr version", short = "Show envr's version", flags = {.Help}},
|
{name = "version", usage = "envr version", short = "Show envr's version", flags = {.Help}},
|
||||||
{
|
{
|
||||||
@@ -142,7 +154,14 @@ key somewhere, otherwise your data could be lost forever.`,
|
|||||||
nushell
|
nushell
|
||||||
bash`,
|
bash`,
|
||||||
flags = {.Help},
|
flags = {.Help},
|
||||||
args = {{name = "shell", ntype = "string", completion = "shells", desc = "Shell to generate completions for"}},
|
args = {
|
||||||
|
{
|
||||||
|
name = "shell",
|
||||||
|
ntype = "string",
|
||||||
|
completion = "shells",
|
||||||
|
desc = "Shell to generate completions for",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name = "uninstall",
|
name = "uninstall",
|
||||||
@@ -271,48 +290,22 @@ Flag_Field :: struct {
|
|||||||
completion: string,
|
completion: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
flag_field :: proc(ft: Flag_Type) -> Flag_Field {
|
write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) {
|
||||||
field := reflect.struct_field_at(Flags, int(ft))
|
table.caption(tbl, "Flags:")
|
||||||
|
for ft in Flag_Type {
|
||||||
args_tag := reflect.struct_tag_get(field.tag, "args")
|
if ft not_in flags do continue
|
||||||
long_name, _ := strings.replace(field.name, "_", "-", -1, context.temp_allocator)
|
names, hint, desc := flag_field_info(ft)
|
||||||
if n, ok := get_subtag(args_tag, "name"); ok {
|
if len(hint) > 0 {
|
||||||
long_name = n
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
short, has_short := get_subtag(args_tag, "short")
|
|
||||||
|
|
||||||
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 {
|
|
||||||
kind = .Bool
|
|
||||||
} else if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
|
|
||||||
kind = .String
|
|
||||||
} 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)
|
|
||||||
for name in enum_ti.names {
|
|
||||||
lower := strings.to_lower(name, context.temp_allocator)
|
|
||||||
append(&parts, fmt.tprintf("'%s'", lower))
|
|
||||||
}
|
|
||||||
enum_values = 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")
|
|
||||||
completion := reflect.struct_tag_get(field.tag, "completion")
|
|
||||||
|
|
||||||
return {
|
|
||||||
long_name = long_name,
|
|
||||||
short_name = has_short ? short : "",
|
|
||||||
kind = kind,
|
|
||||||
usage = usage,
|
|
||||||
default_val = default_val,
|
|
||||||
enum_values = enum_values,
|
|
||||||
completion = completion,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,22 +348,48 @@ flag_field_info :: proc(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) {
|
flag_field :: proc(ft: Flag_Type) -> Flag_Field {
|
||||||
table.caption(tbl, "Flags:")
|
field := reflect.struct_field_at(Flags, int(ft))
|
||||||
for ft in Flag_Type {
|
|
||||||
if ft not_in flags do continue
|
args_tag := reflect.struct_tag_get(field.tag, "args")
|
||||||
names, hint, desc := flag_field_info(ft)
|
long_name, _ := strings.replace(field.name, "_", "-", -1, context.temp_allocator)
|
||||||
if len(hint) > 0 {
|
if n, ok := get_subtag(args_tag, "name"); ok {
|
||||||
display := table.format(
|
long_name = n
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
short, has_short := get_subtag(args_tag, "short")
|
||||||
|
|
||||||
|
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 {
|
||||||
|
kind = .Bool
|
||||||
|
} else if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
|
||||||
|
kind = .String
|
||||||
|
} 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)
|
||||||
|
for name in enum_ti.names {
|
||||||
|
lower := strings.to_lower(name, context.temp_allocator)
|
||||||
|
append(&parts, fmt.tprintf("'%s'", lower))
|
||||||
|
}
|
||||||
|
enum_values = 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")
|
||||||
|
completion := reflect.struct_tag_get(field.tag, "completion")
|
||||||
|
|
||||||
|
return {
|
||||||
|
long_name = long_name,
|
||||||
|
short_name = has_short ? short : "",
|
||||||
|
kind = kind,
|
||||||
|
usage = usage,
|
||||||
|
default_val = default_val,
|
||||||
|
enum_values = enum_values,
|
||||||
|
completion = completion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:terminal/ansi"
|
|
||||||
|
|
||||||
cmd_init :: proc(cmd: ^Command) {
|
cmd_init :: proc(cmd: ^Command) {
|
||||||
force := cmd.flags.force
|
force := cmd.flags.force
|
||||||
|
|||||||
+6
-5
@@ -221,7 +221,8 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st
|
|||||||
include = expanded
|
include = expanded
|
||||||
} else {
|
} else {
|
||||||
// TODO: show errors?
|
// TODO: show errors?
|
||||||
resolved, err := filepath.abs(expanded, allocator)
|
resolved: string
|
||||||
|
resolved, err = filepath.abs(expanded, allocator)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
include = resolved
|
include = resolved
|
||||||
}
|
}
|
||||||
@@ -231,10 +232,6 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
envr_dir :: proc(config_path: string) -> string {
|
|
||||||
return filepath.dir(config_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// User is responsible for freeing the path
|
// User is responsible for freeing the path
|
||||||
data_path :: proc(
|
data_path :: proc(
|
||||||
config_path: string,
|
config_path: string,
|
||||||
@@ -246,3 +243,7 @@ data_path :: proc(
|
|||||||
return filepath.join([]string{envr_dir(config_path), "data.envr"}, allocator)
|
return filepath.join([]string{envr_dir(config_path), "data.envr"}, allocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
envr_dir :: proc(config_path: string) -> string {
|
||||||
|
return filepath.dir(config_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
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"
|
||||||
|
|||||||
+57
-52
@@ -4,58 +4,6 @@ import "base:runtime"
|
|||||||
import "core:reflect"
|
import "core:reflect"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
get_subtag :: proc(tag: string, id: string) -> (value: string, ok: bool) {
|
|
||||||
parts := strings.split(tag, ",", context.temp_allocator)
|
|
||||||
for part in parts {
|
|
||||||
trimmed := strings.trim_space(part)
|
|
||||||
if strings.has_prefix(trimmed, id) && len(trimmed) > len(id) && trimmed[len(id)] == '=' {
|
|
||||||
return trimmed[len(id) + 1:], true
|
|
||||||
}
|
|
||||||
if trimmed == id {
|
|
||||||
return "", true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
is_bool_type :: proc(field: reflect.Struct_Field) -> bool {
|
|
||||||
base_ti := runtime.type_info_base(field.type)
|
|
||||||
_, is_bool := base_ti.variant.(runtime.Type_Info_Boolean)
|
|
||||||
return is_bool
|
|
||||||
}
|
|
||||||
|
|
||||||
set_field :: proc(model: rawptr, field: reflect.Struct_Field, value: string) -> bool {
|
|
||||||
ptr := rawptr(uintptr(model) + field.offset)
|
|
||||||
base_ti := runtime.type_info_base(field.type)
|
|
||||||
|
|
||||||
if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool {
|
|
||||||
(cast(^bool)ptr)^ = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
|
|
||||||
(cast(^string)ptr)^ = value
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
|
|
||||||
for name, i in enum_ti.names {
|
|
||||||
if strings.equal_fold(value, name) {
|
|
||||||
v := enum_ti.values[i]
|
|
||||||
switch base_ti.size {
|
|
||||||
case 1: (cast(^u8)ptr)^ = cast(u8)v
|
|
||||||
case 2: (cast(^u16)ptr)^ = cast(u16)v
|
|
||||||
case 4: (cast(^u32)ptr)^ = cast(u32)v
|
|
||||||
case 8: (cast(^u64)ptr)^ = cast(u64)v
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_flags :: proc(model: ^$T, args: []string) -> (overflow: []string) {
|
parse_flags :: proc(model: ^$T, args: []string) -> (overflow: []string) {
|
||||||
field_count := reflect.struct_field_count(T)
|
field_count := reflect.struct_field_count(T)
|
||||||
long_map := make(map[string]reflect.Struct_Field, field_count, context.temp_allocator)
|
long_map := make(map[string]reflect.Struct_Field, field_count, context.temp_allocator)
|
||||||
@@ -132,3 +80,60 @@ parse_flags :: proc(model: ^$T, args: []string) -> (overflow: []string) {
|
|||||||
|
|
||||||
return overflow_dyn[:]
|
return overflow_dyn[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_subtag :: proc(tag: string, id: string) -> (value: string, ok: bool) {
|
||||||
|
parts := strings.split(tag, ",", context.temp_allocator)
|
||||||
|
for part in parts {
|
||||||
|
trimmed := strings.trim_space(part)
|
||||||
|
if strings.has_prefix(trimmed, id) && len(trimmed) > len(id) && trimmed[len(id)] == '=' {
|
||||||
|
return trimmed[len(id) + 1:], true
|
||||||
|
}
|
||||||
|
if trimmed == id {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
is_bool_type :: proc(field: reflect.Struct_Field) -> bool {
|
||||||
|
base_ti := runtime.type_info_base(field.type)
|
||||||
|
_, is_bool := base_ti.variant.(runtime.Type_Info_Boolean)
|
||||||
|
return is_bool
|
||||||
|
}
|
||||||
|
|
||||||
|
set_field :: proc(model: rawptr, field: reflect.Struct_Field, value: string) -> bool {
|
||||||
|
ptr := rawptr(uintptr(model) + field.offset)
|
||||||
|
base_ti := runtime.type_info_base(field.type)
|
||||||
|
|
||||||
|
if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool {
|
||||||
|
(cast(^bool)ptr)^ = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
|
||||||
|
(cast(^string)ptr)^ = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
|
||||||
|
for name, i in enum_ti.names {
|
||||||
|
if strings.equal_fold(value, name) {
|
||||||
|
v := enum_ti.values[i]
|
||||||
|
switch base_ti.size {
|
||||||
|
case 1:
|
||||||
|
(cast(^u8)ptr)^ = cast(u8)v
|
||||||
|
case 2:
|
||||||
|
(cast(^u16)ptr)^ = cast(u16)v
|
||||||
|
case 4:
|
||||||
|
(cast(^u32)ptr)^ = cast(u32)v
|
||||||
|
case 8:
|
||||||
|
(cast(^u64)ptr)^ = cast(u64)v
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-15
@@ -1,6 +1,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:io"
|
import "core:io"
|
||||||
import "core:text/table"
|
import "core:text/table"
|
||||||
|
|
||||||
@@ -18,20 +17,6 @@ decorations := table.Decorations {
|
|||||||
"─",
|
"─",
|
||||||
}
|
}
|
||||||
|
|
||||||
ansi_aware_width :: proc(str: string) -> int #no_bounds_check {
|
|
||||||
width := 0
|
|
||||||
for i := 0; i < len(str); {
|
|
||||||
if i + 1 < len(str) && str[i] == 0x1b && str[i + 1] == '[' {
|
|
||||||
i += 2
|
|
||||||
for i < len(str) {c := str[i]; i += 1; if c >= 0x40 && c <= 0x7E {break}}
|
|
||||||
} else {
|
|
||||||
width += 1
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return width
|
|
||||||
}
|
|
||||||
|
|
||||||
write_borderless_table :: proc(w: io.Writer, t: ^table.Table) {
|
write_borderless_table :: proc(w: io.Writer, t: ^table.Table) {
|
||||||
table.build(t, ansi_aware_width)
|
table.build(t, ansi_aware_width)
|
||||||
|
|
||||||
@@ -64,6 +49,21 @@ write_borderless_table :: proc(w: io.Writer, t: ^table.Table) {
|
|||||||
write_table_separator(w, t)
|
write_table_separator(w, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ansi_aware_width :: proc(str: string) -> int #no_bounds_check {
|
||||||
|
width := 0
|
||||||
|
for i := 0; i < len(str); {
|
||||||
|
if i + 1 < len(str) && str[i] == 0x1b && str[i + 1] == '[' {
|
||||||
|
i += 2
|
||||||
|
for i < len(str) {c := str[i]; i += 1; if c >= 0x40 && c <= 0x7E {break}}
|
||||||
|
} else {
|
||||||
|
width += 1
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
table_reset :: proc(t: ^table.Table) {
|
table_reset :: proc(t: ^table.Table) {
|
||||||
clear(&t.cells)
|
clear(&t.cells)
|
||||||
clear(&t.colw)
|
clear(&t.colw)
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
0.4.0
|
1.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user