envedit program to edit nul delimited environment files

This commit is contained in:
sawka
2024-11-21 18:42:52 -08:00
parent 3f393f9f4b
commit 712d306944
2 changed files with 590 additions and 0 deletions
+387
View File
@@ -0,0 +1,387 @@
package main
import (
"context"
_ "embed"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/wavetermdev/waveterm/pkg/util/envutil"
"github.com/wavetermdev/waveterm/pkg/vdom"
"github.com/wavetermdev/waveterm/pkg/waveapp"
)
//go:embed style.css
var styleCSS []byte
var envPath string
var AppClient = waveapp.MakeClient(waveapp.AppOpts{
CloseOnCtrlC: true,
GlobalStyles: styleCSS,
})
type EnvItemProps struct {
Key string `json:"key"`
Value string `json:"value"`
OnEdit func() `json:"onEdit"`
OnDelete func() `json:"onDelete"`
OnSave func(string, string) `json:"onSave"` // Add this
OnCancel func() `json:"onCancel"` // Add this
IsEditing bool `json:"isEditing"`
Highlight bool `json:"highlight"`
}
var EnvItem = waveapp.DefineComponent[EnvItemProps](AppClient, "EnvItem",
func(ctx context.Context, props EnvItemProps) any {
if props.IsEditing {
return EditForm(EditFormProps{
Key: props.Key,
Value: props.Value,
IsNew: false,
OnSave: props.OnSave, // Use the passed save handler
OnCancel: props.OnCancel, // Use the passed cancel handler
})
}
return vdom.H("div", map[string]any{
"className": vdom.Classes(
"env-item",
vdom.If(props.Highlight, "highlight"),
),
},
vdom.H("div", map[string]any{
"className": "env-item-key",
}, props.Key),
vdom.H("div", map[string]any{
"className": "env-item-value",
}, props.Value),
vdom.H("div", map[string]any{
"className": "env-item-actions",
},
vdom.H("button", map[string]any{
"className": "env-item-edit",
"onClick": props.OnEdit,
"title": "Edit",
},
vdom.H("i", map[string]any{
"className": "fa fa-pencil",
}),
),
vdom.H("button", map[string]any{
"className": "env-item-delete",
"onClick": props.OnDelete,
"title": "Delete",
},
vdom.H("i", map[string]any{
"className": "fa fa-times",
}),
),
),
)
},
)
type EditFormProps struct {
Key string `json:"key"`
Value string `json:"value"`
IsNew bool `json:"isNew"`
OnSave func(string, string) `json:"onSave"`
OnCancel func() `json:"onCancel"`
}
var EditForm = waveapp.DefineComponent[EditFormProps](AppClient, "EditForm",
func(ctx context.Context, props EditFormProps) any {
key, setKey := vdom.UseState(ctx, props.Key)
value, setValue := vdom.UseState(ctx, props.Value)
error, setError := vdom.UseState(ctx, "")
handleSave := func() {
if props.IsNew && key == "" {
setError("Key cannot be empty")
return
}
if strings.ContainsAny(key, "=\x00") {
setError("Key cannot contain '=' or null character")
return
}
if strings.Contains(value, "\x00") {
setError("Value cannot contain null character")
return
}
props.OnSave(key, value)
}
keyHandler := &vdom.VDomFunc{
Type: vdom.ObjectType_Func,
Fn: func(e vdom.VDomEvent) {
switch e.KeyData.Key {
case "Enter":
if !e.KeyData.Shift {
handleSave()
}
case "Escape":
props.OnCancel()
}
},
Keys: []string{"Enter", "Escape"},
PreventDefault: true,
}
return vdom.H("div", map[string]any{
"className": "env-item env-item-editing",
},
vdom.H("div", map[string]any{
"className": "env-item-key",
},
vdom.IfElse(props.IsNew,
vdom.H("input", map[string]any{
"className": "env-edit-key",
"value": key,
"onChange": func(e vdom.VDomEvent) { setKey(e.TargetValue) },
"placeholder": "Key",
"onKeyDown": keyHandler,
}),
vdom.H("div", map[string]any{
"className": "env-key-static",
}, props.Key),
),
),
vdom.H("div", map[string]any{
"className": "env-item-value",
},
vdom.H("textarea", map[string]any{
"className": "env-edit-value",
"value": value,
"onChange": func(e vdom.VDomEvent) { setValue(e.TargetValue) },
"placeholder": "Value",
"onKeyDown": keyHandler,
"rows": 3,
}),
),
vdom.If(error != "",
vdom.H("div", map[string]any{
"className": "env-edit-error",
}, error),
),
vdom.H("div", map[string]any{
"className": "env-edit-actions",
},
vdom.H("button", map[string]any{
"className": "env-edit-cancel",
"onClick": props.OnCancel,
}, "Cancel"),
vdom.H("button", map[string]any{
"className": "env-edit-save",
"onClick": handleSave,
},
vdom.H("i", map[string]any{
"className": "fa fa-check",
}),
" Save",
),
),
)
},
)
// HeaderProps breaks out all the header functionality
type HeaderProps struct {
Path string `json:"path"`
OnAddNew func() `json:"onAddNew"`
IsEditing bool `json:"isEditing"`
}
var Header = waveapp.DefineComponent[HeaderProps](AppClient, "Header",
func(ctx context.Context, props HeaderProps) any {
return vdom.H("div", map[string]any{
"className": "env-header",
},
vdom.H("h1", nil, "Environment Editor"),
vdom.H("div", map[string]any{
"className": "env-path",
}, "Path: ", props.Path),
vdom.H("button", map[string]any{
"className": vdom.Classes(
"env-add",
vdom.If(props.IsEditing, "env-add-active"),
),
"onClick": props.OnAddNew,
"title": vdom.IfElse(props.IsEditing, "Close editor", "Add variable"),
},
vdom.H("i", map[string]any{
"className": vdom.Classes(
"fa",
vdom.IfElse(props.IsEditing, "fa-times", "fa-plus"),
),
}),
vdom.IfElse(props.IsEditing, " Close", " Add Variable"),
),
)
},
)
var App = waveapp.DefineComponent(AppClient, "App",
func(ctx context.Context, _ any) any {
envMap, setEnvMap := vdom.UseState(ctx, map[string]string{})
editingKey, setEditingKey := vdom.UseState(ctx, "")
error, setError := vdom.UseState(ctx, "")
highlightKey, setHighlightKey := vdom.UseState(ctx, "")
// Clear highlight after delay
vdom.UseEffect(ctx, func() func() {
if highlightKey != "" {
timer := time.AfterFunc(1500*time.Millisecond, func() {
setHighlightKey("")
AppClient.SendAsyncInitiation()
})
return func() {
timer.Stop()
}
}
return nil
}, []any{highlightKey})
// Load environment file on mount
vdom.UseEffect(ctx, func() func() {
content, err := os.ReadFile(envPath)
if err != nil && !os.IsNotExist(err) {
setError(fmt.Sprintf("Error reading file: %v", err))
return nil
}
if len(content) > 0 {
setEnvMap(envutil.EnvToMap(string(content)))
}
return nil
}, []any{})
// Save environment to file
saveToFile := func(newMap map[string]string) {
content := envutil.MapToEnv(newMap)
err := os.WriteFile(envPath, []byte(content), 0644)
if err != nil {
setError(fmt.Sprintf("Error saving file: %v", err))
return
}
setEnvMap(newMap)
}
handleAdd := func() {
if editingKey != "" {
setEditingKey("")
} else {
setEditingKey("__new__")
}
}
handleEdit := func(key string) {
if editingKey == key {
setEditingKey("")
} else {
setEditingKey(key)
}
}
handleDelete := func(key string) {
newMap := make(map[string]string)
for k, v := range envMap {
if k != key {
newMap[k] = v
}
}
saveToFile(newMap)
}
handleSave := func(key, value string) {
newMap := make(map[string]string)
for k, v := range envMap {
newMap[k] = v
}
newMap[key] = value
saveToFile(newMap)
setEditingKey("")
setHighlightKey(key)
}
handleCancel := func() {
setEditingKey("")
}
// Get sorted keys
var keys []string
for k := range envMap {
keys = append(keys, k)
}
sort.Strings(keys)
return vdom.H("div", map[string]any{
"className": "env-editor",
},
Header(HeaderProps{
Path: envPath,
OnAddNew: handleAdd,
IsEditing: editingKey != "",
}),
vdom.If(error != "",
vdom.H("div", map[string]any{
"className": "env-error",
}, error),
),
vdom.H("div", map[string]any{
"className": "env-list",
},
vdom.ForEach(keys, func(key string) any {
return EnvItem(EnvItemProps{
Key: key,
Value: envMap[key],
OnEdit: func() { handleEdit(key) },
OnDelete: func() { handleDelete(key) },
OnSave: handleSave, // Add this
OnCancel: handleCancel, // Add this
IsEditing: key == editingKey,
Highlight: key == highlightKey,
})
}),
// Add new item form at bottom
vdom.If(editingKey == "__new__",
EditForm(EditFormProps{
Key: "",
Value: "",
IsNew: true,
OnSave: handleSave,
OnCancel: handleCancel,
}),
),
),
)
},
)
func main() {
AppClient.RegisterDefaultFlags()
flag.Parse()
if flag.NArg() != 1 {
fmt.Fprintf(os.Stderr, "Usage: env-editor [flags] <env-file-path>\n")
flag.PrintDefaults()
os.Exit(1)
}
envPath = flag.Arg(0)
// Verify directory exists
dir := filepath.Dir(envPath)
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
fmt.Fprintf(os.Stderr, "Directory does not exist: %s\n", dir)
os.Exit(1)
}
AppClient.RunMain()
}
+203
View File
@@ -0,0 +1,203 @@
.env-editor {
padding: 1rem;
max-width: 800px;
margin: 0 auto;
}
.env-header {
margin-bottom: 2rem;
display: flex;
align-items: center;
gap: 1rem;
}
.env-header h1 {
margin: 0;
margin-right: auto;
}
.env-path {
color: #888;
font-family: monospace;
}
.env-error {
background-color: #441111;
color: #ff6666;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
}
.env-list {
margin-bottom: 1rem;
}
.env-item {
display: grid;
grid-template-columns: 200px 1fr auto;
gap: 1rem;
padding: 0.5rem;
margin-bottom: 0.5rem;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 4px;
min-height: 2rem;
align-items: center;
transition: background-color 0.3s ease;
}
.env-item.highlight {
background-color: rgba(102, 255, 102, 0.15);
}
.env-item-editing {
align-items: start;
}
.env-item-key {
font-family: monospace;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
}
.env-item-value {
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
}
.env-item-actions {
display: flex;
gap: 0.5rem;
}
.env-item button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
background: none;
border: 1px solid #666;
border-radius: 3px;
color: #ccc;
cursor: pointer;
}
.env-item button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.env-item-delete:hover {
color: #ff6666;
border-color: #ff6666;
}
.env-edit-key {
width: 100%;
padding: 0.5rem;
font-family: monospace;
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid #666;
border-radius: 3px;
color: #fff;
}
.env-key-static {
padding: 0.5rem;
font-family: monospace;
font-weight: bold;
}
.env-edit-value {
width: 100%;
padding: 0.5rem;
font-family: monospace;
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid #666;
border-radius: 3px;
color: #fff;
resize: vertical;
min-height: 4.5rem;
}
.env-edit-error {
color: #ff6666;
grid-column: 1 / -1;
margin-top: 0.5rem;
}
/* Updated button styles */
.env-edit-actions {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
min-height: 32px;
}
.env-edit-save,
.env-edit-cancel {
height: 32px;
padding: 0 1rem;
background: none;
border: 1px solid #666;
border-radius: 3px;
color: #ccc;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 14px;
min-width: 80px;
}
.env-edit-save {
border-color: #66ff66;
color: #66ff66;
}
.env-edit-save:hover {
background-color: rgba(100, 255, 100, 0.1);
}
.env-edit-cancel:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.env-edit-save i {
font-size: 12px;
}
.env-add {
padding: 0.5rem 1rem;
background: none;
border: 1px solid #66ff66;
border-radius: 3px;
color: #66ff66;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.env-add:hover {
background-color: rgba(100, 255, 100, 0.1);
}
.env-add i {
font-size: 0.875em;
}
.env-add-active {
border-color: #666;
color: #ccc;
}
.env-add-active:hover {
background-color: rgba(255, 255, 255, 0.1);
}