mirror of
https://github.com/wavetermdev/waveapps.git
synced 2026-08-05 13:48:00 -07:00
envedit program to edit nul delimited environment files
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user