fix(fn-run): propagar stdout/stderr de bash functions library-style #1
@@ -0,0 +1,215 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// ScaffoldWailsAppConfig configura la generación del proyecto Wails.
|
||||
type ScaffoldWailsAppConfig struct {
|
||||
Name string // Nombre del proyecto
|
||||
Dir string // Directorio destino
|
||||
Title string // Título de la ventana
|
||||
Width int // Ancho de la ventana (default 1024)
|
||||
Height int // Alto de la ventana (default 768)
|
||||
Author string // Nombre del autor
|
||||
FrontendLib string // Path a la frontend library (default ~/.local_agentes/frontend/frontend)
|
||||
}
|
||||
|
||||
const mainGoTpl = `package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
app := NewApp()
|
||||
|
||||
err := wails.Run(&options.App{
|
||||
Title: "{{.Title}}",
|
||||
Width: {{.Width}},
|
||||
Height: {{.Height}},
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const appGoTpl = `package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct — cada método público se expone como binding IPC al frontend.
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
// EmitEvent emite un evento al frontend.
|
||||
func (a *App) EmitEvent(eventName string, data interface{}) {
|
||||
runtime.EventsEmit(a.ctx, eventName, data)
|
||||
}
|
||||
|
||||
// Ping verifica que el IPC funciona.
|
||||
func (a *App) Ping() string {
|
||||
return "pong"
|
||||
}
|
||||
`
|
||||
|
||||
const wailsJSONTpl = `{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "{{.Name}}",
|
||||
"outputfilename": "{{.Name}}",
|
||||
"frontend:dir": "./frontend",
|
||||
"frontend:install": "pnpm install",
|
||||
"frontend:build": "pnpm run build",
|
||||
"frontend:dev:watcher": "pnpm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"wailsjsdir": "./frontend/src/wailsjs",
|
||||
"author": {
|
||||
"name": "{{.Author}}"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const goModTpl = `module {{.Name}}
|
||||
|
||||
go 1.23
|
||||
|
||||
require github.com/wailsapp/wails/v2 v2.11.0
|
||||
`
|
||||
|
||||
// ScaffoldWailsApp genera la estructura base de un proyecto Wails con frontend vinculado.
|
||||
func ScaffoldWailsApp(ctx context.Context, cfg ScaffoldWailsAppConfig) error {
|
||||
if cfg.Name == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
if cfg.Dir == "" {
|
||||
cfg.Dir = cfg.Name
|
||||
}
|
||||
if cfg.Title == "" {
|
||||
cfg.Title = cfg.Name
|
||||
}
|
||||
if cfg.Width == 0 {
|
||||
cfg.Width = 1024
|
||||
}
|
||||
if cfg.Height == 0 {
|
||||
cfg.Height = 768
|
||||
}
|
||||
if cfg.Author == "" {
|
||||
cfg.Author = "Agent"
|
||||
}
|
||||
if cfg.FrontendLib == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
cfg.FrontendLib = filepath.Join(home, ".local_agentes", "frontend", "frontend")
|
||||
}
|
||||
|
||||
// Crear directorio
|
||||
if err := os.MkdirAll(cfg.Dir, 0755); err != nil {
|
||||
return fmt.Errorf("creating dir: %w", err)
|
||||
}
|
||||
|
||||
// Generar archivos Go
|
||||
files := map[string]string{
|
||||
"main.go": mainGoTpl,
|
||||
"app.go": appGoTpl,
|
||||
"wails.json": wailsJSONTpl,
|
||||
"go.mod": goModTpl,
|
||||
}
|
||||
|
||||
for name, tpl := range files {
|
||||
path := filepath.Join(cfg.Dir, name)
|
||||
t, err := template.New(name).Parse(tpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing template %s: %w", name, err)
|
||||
}
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating %s: %w", name, err)
|
||||
}
|
||||
if err := t.Execute(f, cfg); err != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("executing template %s: %w", name, err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// Crear frontend como link simbólico o copiar template
|
||||
frontendDir := filepath.Join(cfg.Dir, "frontend")
|
||||
createProjectScript := filepath.Join(filepath.Dir(cfg.FrontendLib), "..", "scripts", "create-project.sh")
|
||||
|
||||
if _, err := os.Stat(createProjectScript); err == nil {
|
||||
// Usar el script de Frontend_Library
|
||||
cmd := exec.CommandContext(ctx, "bash", createProjectScript, cfg.Name, frontendDir, "--wails")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("creating frontend: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Fallback: crear frontend mínimo
|
||||
if err := os.MkdirAll(filepath.Join(frontendDir, "src"), 0755); err != nil {
|
||||
return fmt.Errorf("creating frontend dir: %w", err)
|
||||
}
|
||||
pkgJSON := fmt.Sprintf(`{"name":"%s-frontend","private":true,"scripts":{"dev":"vite","build":"vite build"}}`, cfg.Name)
|
||||
os.WriteFile(filepath.Join(frontendDir, "package.json"), []byte(pkgJSON), 0644)
|
||||
}
|
||||
|
||||
// go mod tidy
|
||||
cmd := exec.CommandContext(ctx, "go", "mod", "tidy")
|
||||
cmd.Dir = cfg.Dir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
_ = cmd.Run() // No fatal si falla
|
||||
|
||||
fmt.Printf("Wails app scaffolded at %s\n", cfg.Dir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateAppBinding genera el código Go para un método de binding.
|
||||
func GenerateAppBinding(name string, params []string, returnType string, body string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("func (a *App) %s(", name))
|
||||
sb.WriteString(strings.Join(params, ", "))
|
||||
sb.WriteString(")")
|
||||
if returnType != "" {
|
||||
sb.WriteString(" " + returnType)
|
||||
}
|
||||
sb.WriteString(" {\n")
|
||||
sb.WriteString("\t" + body + "\n")
|
||||
sb.WriteString("}\n")
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: scaffold_wails_app
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "ScaffoldWailsApp(ctx context.Context, cfg ScaffoldWailsAppConfig) error"
|
||||
description: "Genera proyecto Wails completo: main.go con embed, app.go con bindings base, wails.json, go.mod, y frontend vinculado a Frontend_Library."
|
||||
tags: [wails, scaffold, desktop, project, generator]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [os, os/exec, path/filepath, text/template, fmt, strings, context]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/scaffold_wails_app.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
ScaffoldWailsApp(ctx, ScaffoldWailsAppConfig{
|
||||
Name: "my-dashboard",
|
||||
Dir: "/home/user/projects/my-dashboard",
|
||||
Title: "My Dashboard",
|
||||
Width: 1280,
|
||||
Height: 800,
|
||||
Author: "Lucas",
|
||||
})
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Pipeline que compone: generación de templates Go + creación de frontend via create-project.sh de Frontend_Library + go mod tidy. Incluye GenerateAppBinding para crear métodos de binding programáticamente.
|
||||
@@ -0,0 +1,85 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WailsCRUDSpec define la especificación para generar bindings CRUD.
|
||||
type WailsCRUDSpec struct {
|
||||
EntityName string // Nombre de la entidad (PascalCase, ej: "User")
|
||||
Fields []string // Campos de la entidad (ej: ["Name string", "Email string"])
|
||||
WithList bool // Generar List method
|
||||
WithGet bool // Generar Get method
|
||||
WithCreate bool // Generar Create method
|
||||
WithUpdate bool // Generar Update method
|
||||
WithDelete bool // Generar Delete method
|
||||
}
|
||||
|
||||
// GenerateWailsCRUD genera código Go de bindings CRUD para una entidad.
|
||||
// Retorna el código Go como string — función pura.
|
||||
func GenerateWailsCRUD(spec WailsCRUDSpec) string {
|
||||
var sb strings.Builder
|
||||
lower := strings.ToLower(spec.EntityName)
|
||||
plural := lower + "s"
|
||||
|
||||
// Tipo
|
||||
sb.WriteString(fmt.Sprintf("// %s entity\n", spec.EntityName))
|
||||
sb.WriteString(fmt.Sprintf("type %s struct {\n", spec.EntityName))
|
||||
sb.WriteString("\tID string `json:\"id\"`\n")
|
||||
for _, field := range spec.Fields {
|
||||
parts := strings.SplitN(field, " ", 2)
|
||||
if len(parts) == 2 {
|
||||
jsonTag := strings.ToLower(parts[0])
|
||||
sb.WriteString(fmt.Sprintf("\t%s %s `json:\"%s\"`\n", parts[0], parts[1], jsonTag))
|
||||
}
|
||||
}
|
||||
sb.WriteString("}\n\n")
|
||||
|
||||
// List
|
||||
if spec.WithList {
|
||||
sb.WriteString(fmt.Sprintf("// List%s retorna todos los %s.\n", spec.EntityName+"s", plural))
|
||||
sb.WriteString(fmt.Sprintf("func (a *App) List%s() ([]%s, error) {\n", spec.EntityName+"s", spec.EntityName))
|
||||
sb.WriteString(fmt.Sprintf("\t// TODO: implement List%s\n", spec.EntityName+"s"))
|
||||
sb.WriteString(fmt.Sprintf("\treturn nil, fmt.Errorf(\"not implemented\")\n"))
|
||||
sb.WriteString("}\n\n")
|
||||
}
|
||||
|
||||
// Get
|
||||
if spec.WithGet {
|
||||
sb.WriteString(fmt.Sprintf("// Get%s retorna un %s por ID.\n", spec.EntityName, lower))
|
||||
sb.WriteString(fmt.Sprintf("func (a *App) Get%s(id string) (%s, error) {\n", spec.EntityName, spec.EntityName))
|
||||
sb.WriteString(fmt.Sprintf("\t// TODO: implement Get%s\n", spec.EntityName))
|
||||
sb.WriteString(fmt.Sprintf("\treturn %s{}, fmt.Errorf(\"not implemented\")\n", spec.EntityName))
|
||||
sb.WriteString("}\n\n")
|
||||
}
|
||||
|
||||
// Create
|
||||
if spec.WithCreate {
|
||||
sb.WriteString(fmt.Sprintf("// Create%s crea un nuevo %s.\n", spec.EntityName, lower))
|
||||
sb.WriteString(fmt.Sprintf("func (a *App) Create%s(%s %s) (%s, error) {\n", spec.EntityName, lower, spec.EntityName, spec.EntityName))
|
||||
sb.WriteString(fmt.Sprintf("\t// TODO: implement Create%s\n", spec.EntityName))
|
||||
sb.WriteString(fmt.Sprintf("\treturn %s, fmt.Errorf(\"not implemented\")\n", lower))
|
||||
sb.WriteString("}\n\n")
|
||||
}
|
||||
|
||||
// Update
|
||||
if spec.WithUpdate {
|
||||
sb.WriteString(fmt.Sprintf("// Update%s actualiza un %s existente.\n", spec.EntityName, lower))
|
||||
sb.WriteString(fmt.Sprintf("func (a *App) Update%s(%s %s) (%s, error) {\n", spec.EntityName, lower, spec.EntityName, spec.EntityName))
|
||||
sb.WriteString(fmt.Sprintf("\t// TODO: implement Update%s\n", spec.EntityName))
|
||||
sb.WriteString(fmt.Sprintf("\treturn %s, fmt.Errorf(\"not implemented\")\n", lower))
|
||||
sb.WriteString("}\n\n")
|
||||
}
|
||||
|
||||
// Delete
|
||||
if spec.WithDelete {
|
||||
sb.WriteString(fmt.Sprintf("// Delete%s elimina un %s por ID.\n", spec.EntityName, lower))
|
||||
sb.WriteString(fmt.Sprintf("func (a *App) Delete%s(id string) error {\n", spec.EntityName))
|
||||
sb.WriteString(fmt.Sprintf("\t// TODO: implement Delete%s\n", spec.EntityName))
|
||||
sb.WriteString("\treturn fmt.Errorf(\"not implemented\")\n")
|
||||
sb.WriteString("}\n\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: wails_bind_crud
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "GenerateWailsCRUD(spec WailsCRUDSpec) string"
|
||||
description: "Genera código Go de bindings CRUD para Wails: struct + métodos List/Get/Create/Update/Delete con stubs not-implemented."
|
||||
tags: [wails, crud, generator, bindings, codegen, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: [fmt, strings]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/wails_bind_crud.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
code := GenerateWailsCRUD(WailsCRUDSpec{
|
||||
EntityName: "User",
|
||||
Fields: []string{"Name string", "Email string", "Role string"},
|
||||
WithList: true,
|
||||
WithGet: true,
|
||||
WithCreate: true,
|
||||
WithUpdate: true,
|
||||
WithDelete: true,
|
||||
})
|
||||
// Genera: type User struct + ListUsers + GetUser + CreateUser + UpdateUser + DeleteUser
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Función pura — genera código como string. Los métodos son stubs con `return fmt.Errorf("not implemented")`. Un agente puede generar el código, insertarlo en app.go, y luego implementar los TODOs.
|
||||
@@ -0,0 +1,65 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WailsBuildConfig configura la compilación de un proyecto Wails.
|
||||
type WailsBuildConfig struct {
|
||||
Dir string // Directorio del proyecto Wails
|
||||
Platform string // linux, windows, darwin (default: current)
|
||||
Output string // Nombre del binario de salida
|
||||
Debug bool // Build con debug info
|
||||
}
|
||||
|
||||
// WailsBuild compila un proyecto Wails para la plataforma especificada.
|
||||
func WailsBuild(ctx context.Context, cfg WailsBuildConfig) error {
|
||||
if cfg.Dir == "" {
|
||||
return fmt.Errorf("dir is required")
|
||||
}
|
||||
|
||||
args := []string{"build"}
|
||||
|
||||
if cfg.Platform != "" {
|
||||
args = append(args, "-platform", cfg.Platform)
|
||||
}
|
||||
if cfg.Output != "" {
|
||||
args = append(args, "-o", cfg.Output)
|
||||
}
|
||||
if cfg.Debug {
|
||||
args = append(args, "-debug")
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "wails", args...)
|
||||
cmd.Dir = cfg.Dir
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("wails build failed: %w\n%s", err, string(output))
|
||||
}
|
||||
|
||||
fmt.Printf("Built successfully: %s\n", strings.TrimSpace(string(output)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// WailsDev lanza un proyecto Wails en modo desarrollo con hot reload.
|
||||
func WailsDev(ctx context.Context, dir string, browser bool) error {
|
||||
if dir == "" {
|
||||
return fmt.Errorf("dir is required")
|
||||
}
|
||||
|
||||
args := []string{"dev"}
|
||||
if browser {
|
||||
args = append(args, "-browser")
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "wails", args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Stdout = nil // inherit
|
||||
cmd.Stderr = nil
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: wails_build
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "WailsBuild(ctx context.Context, cfg WailsBuildConfig) error"
|
||||
description: "Compila un proyecto Wails para linux/windows/darwin. Incluye WailsDev para modo desarrollo con hot reload."
|
||||
tags: [wails, build, compile, desktop, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [os/exec, fmt, strings, context]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/wails_build.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Build para linux
|
||||
WailsBuild(ctx, WailsBuildConfig{
|
||||
Dir: "/home/user/my-app",
|
||||
Platform: "linux",
|
||||
})
|
||||
|
||||
// Desarrollo con hot reload + browser
|
||||
WailsDev(ctx, "/home/user/my-app", true)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Requiere `wails` CLI instalado. WailsDev bloquea el proceso (es un servidor de desarrollo).
|
||||
@@ -0,0 +1,40 @@
|
||||
//go:build ignore
|
||||
// NOTE: requires wails dependency in target project — use this file by copying into a Wails project
|
||||
|
||||
package infra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// WailsEventPayload es un envelope para eventos tipados.
|
||||
type WailsEventPayload struct {
|
||||
Type string `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// WailsEmitEvent emite un evento tipado al frontend.
|
||||
func WailsEmitEvent(ctx context.Context, eventName string, data interface{}) {
|
||||
payload := WailsEventPayload{
|
||||
Type: eventName,
|
||||
Data: data,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
runtime.EventsEmit(ctx, eventName, payload)
|
||||
}
|
||||
|
||||
// WailsEmitJSON emite datos como JSON string al frontend.
|
||||
func WailsEmitJSON(ctx context.Context, eventName string, data interface{}) error {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling event data: %w", err)
|
||||
}
|
||||
runtime.EventsEmit(ctx, eventName, string(b))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: wails_emit_event
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "WailsEmitEvent(ctx context.Context, eventName string, data interface{})"
|
||||
description: "Emite eventos tipados de Go al frontend con timestamp automático. Incluye WailsEmitJSON para serialización explícita."
|
||||
tags: [wails, event, emit, ipc, realtime, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [context, encoding/json, fmt, time, github.com/wailsapp/wails/v2/pkg/runtime]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/wails_emit_event.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Emitir evento tipado
|
||||
WailsEmitEvent(ctx, "price:update", PriceData{Symbol: "BTC", Price: 67500.0})
|
||||
|
||||
// El frontend lo recibe con useWailsEvent:
|
||||
// useWailsEvent({ eventName: 'price:update', onEvent: (data) => ... })
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
El evento llega al frontend como WailsEventPayload con `type`, `data` y `timestamp`. Complementa `use_wails_event` del lado TS. Requiere la dependencia `github.com/wailsapp/wails/v2` en el proyecto destino.
|
||||
@@ -0,0 +1,70 @@
|
||||
//go:build ignore
|
||||
// NOTE: requires wails dependency in target project — use this file by copying into a Wails project
|
||||
|
||||
package infra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// WailsStreamConfig configura un stream de datos hacia el frontend.
|
||||
type WailsStreamConfig struct {
|
||||
StreamName string // Nombre del stream (evento base)
|
||||
ChunkDelay time.Duration // Delay entre chunks (default 0)
|
||||
}
|
||||
|
||||
// WailsStreamData envía un slice de datos como stream al frontend.
|
||||
// Emite cada elemento como chunk en {streamName}, y {streamName}:complete al terminar.
|
||||
func WailsStreamData[T any](ctx context.Context, cfg WailsStreamConfig, data []T) error {
|
||||
if cfg.StreamName == "" {
|
||||
return fmt.Errorf("stream name is required")
|
||||
}
|
||||
|
||||
for _, chunk := range data {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
runtime.EventsEmit(ctx, cfg.StreamName+":error", map[string]string{
|
||||
"message": "stream cancelled",
|
||||
})
|
||||
return ctx.Err()
|
||||
default:
|
||||
runtime.EventsEmit(ctx, cfg.StreamName, chunk)
|
||||
if cfg.ChunkDelay > 0 {
|
||||
time.Sleep(cfg.ChunkDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtime.EventsEmit(ctx, cfg.StreamName+":complete", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WailsStreamFunc ejecuta una función generadora y envía resultados como stream.
|
||||
// La función generadora envía datos por el canal, y esta función los retransmite al frontend.
|
||||
func WailsStreamFunc[T any](ctx context.Context, streamName string, generator func(ctx context.Context, ch chan<- T) error) error {
|
||||
ch := make(chan T, 100)
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
errCh <- generator(ctx, ch)
|
||||
}()
|
||||
|
||||
for chunk := range ch {
|
||||
runtime.EventsEmit(ctx, streamName, chunk)
|
||||
}
|
||||
|
||||
if err := <-errCh; err != nil {
|
||||
runtime.EventsEmit(ctx, streamName+":error", map[string]string{
|
||||
"message": err.Error(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
runtime.EventsEmit(ctx, streamName+":complete", nil)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: wails_stream_data
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "WailsStreamData[T any](ctx context.Context, cfg WailsStreamConfig, data []T) error"
|
||||
description: "Envía datos como stream Go→TS con protocolo {name}/{name}:complete/{name}:error. Incluye WailsStreamFunc para generadores."
|
||||
tags: [wails, stream, ipc, realtime, chunks, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [context, fmt, time, github.com/wailsapp/wails/v2/pkg/runtime]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/wails_stream_data.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Stream de slice
|
||||
WailsStreamData(ctx, WailsStreamConfig{
|
||||
StreamName: "logs",
|
||||
ChunkDelay: 10 * time.Millisecond,
|
||||
}, logLines)
|
||||
|
||||
// Stream con generador
|
||||
WailsStreamFunc(ctx, "metrics", func(ctx context.Context, ch chan<- Metric) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(time.Second):
|
||||
ch <- collectMetric()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Frontend lo recibe con useWailsStream:
|
||||
// useWailsStream({ streamName: 'metrics', autoStart: true })
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Protocolo: chunks en `{streamName}`, fin en `{streamName}:complete`, error en `{streamName}:error`. Compatible con `use_wails_stream` del lado TS. WailsStreamFunc usa goroutine + canal para datos asíncronos. Requiere Go 1.18+ (generics) y la dependencia `github.com/wailsapp/wails/v2` en el proyecto destino.
|
||||
Reference in New Issue
Block a user