Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions tsunami/app/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ package app
import (
"context"
"fmt"
"log"
"time"

"github.com/google/uuid"
"github.com/wavetermdev/waveterm/tsunami/engine"
"github.com/wavetermdev/waveterm/tsunami/rpctypes"
"github.com/wavetermdev/waveterm/tsunami/util"
"github.com/wavetermdev/waveterm/tsunami/vdom"
)
Expand Down Expand Up @@ -186,3 +189,113 @@ func UseAfter(duration time.Duration, timeoutFn func(), deps []any) {
}
}, deps)
}

// ModalConfig contains all configuration options for modals
type ModalConfig struct {
Icon string `json:"icon,omitempty"` // Optional icon to display (emoji or icon name)
Title string `json:"title"` // Modal title
Text string `json:"text,omitempty"` // Optional body text
OkText string `json:"oktext,omitempty"` // Optional OK button text (defaults to "OK")
CancelText string `json:"canceltext,omitempty"` // Optional Cancel button text for confirm modals (defaults to "Cancel")
OnClose func() `json:"-"` // Optional callback for alert modals when dismissed
OnResult func(bool) `json:"-"` // Optional callback for confirm modals with the result (true = confirmed, false = cancelled)
}

// UseAlertModal returns a boolean indicating if the modal is open and a function to trigger it
func UseAlertModal() (modalOpen bool, triggerAlert func(config ModalConfig)) {
isOpen := UseLocal(false)

trigger := func(config ModalConfig) {
if isOpen.Get() {
log.Printf("warning: UseAlertModal trigger called while modal is already open")
if config.OnClose != nil {
go func() {
defer func() {
util.PanicHandler("UseAlertModal callback goroutine", recover())
}()
time.Sleep(10 * time.Millisecond)
config.OnClose()
}()
}
return
}
isOpen.Set(true)

// Create modal config for backend
modalId := uuid.New().String()
backendConfig := rpctypes.ModalConfig{
ModalId: modalId,
ModalType: "alert",
Icon: config.Icon,
Title: config.Title,
Text: config.Text,
OkText: config.OkText,
CancelText: config.CancelText,
}

// Show modal and wait for result in a goroutine
go func() {
defer func() {
util.PanicHandler("UseAlertModal goroutine", recover())
}()
resultChan := engine.GetDefaultClient().ShowModal(backendConfig)
<-resultChan // Wait for result (always dismissed for alerts)
isOpen.Set(false)
if config.OnClose != nil {
config.OnClose()
}
}()
}

return isOpen.Get(), trigger
}

// UseConfirmModal returns a boolean indicating if the modal is open and a function to trigger it
func UseConfirmModal() (modalOpen bool, triggerConfirm func(config ModalConfig)) {
isOpen := UseLocal(false)

trigger := func(config ModalConfig) {
if isOpen.Get() {
log.Printf("warning: UseConfirmModal trigger called while modal is already open")
if config.OnResult != nil {
go func() {
defer func() {
util.PanicHandler("UseConfirmModal callback goroutine", recover())
}()
time.Sleep(10 * time.Millisecond)
config.OnResult(false)
}()
}
return
}
isOpen.Set(true)

// Create modal config for backend
modalId := uuid.New().String()
backendConfig := rpctypes.ModalConfig{
ModalId: modalId,
ModalType: "confirm",
Icon: config.Icon,
Title: config.Title,
Text: config.Text,
OkText: config.OkText,
CancelText: config.CancelText,
}

// Show modal and wait for result in a goroutine
go func() {
defer func() {
util.PanicHandler("UseConfirmModal goroutine", recover())
}()
resultChan := engine.GetDefaultClient().ShowModal(backendConfig)
result := <-resultChan
isOpen.Set(false)
if config.OnResult != nil {
config.OnResult(result)
}
}()
}

return isOpen.Get(), trigger
}

156 changes: 156 additions & 0 deletions tsunami/demo/modaltest/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package main

import (
"github.com/wavetermdev/waveterm/tsunami/app"
"github.com/wavetermdev/waveterm/tsunami/vdom"
)

const AppTitle = "Modal Test (Tsunami Demo)"
const AppShortDesc = "Test alert and confirm modals in Tsunami"

var App = app.DefineComponent("App", func(_ struct{}) any {
// State to track modal results
alertResult := app.UseLocal("")
confirmResult := app.UseLocal("")

// Hook for alert modal
alertOpen, triggerAlert := app.UseAlertModal()

// Hook for confirm modal
confirmOpen, triggerConfirm := app.UseConfirmModal()

// Event handlers for alert
handleShowAlert := func() {
triggerAlert(app.ModalConfig{
Icon: "⚠️",
Title: "Alert Message",
Text: "This is an alert modal. Click OK to dismiss.",
OnClose: func() {
alertResult.Set("Alert dismissed")
},
})
}

handleShowAlertSimple := func() {
triggerAlert(app.ModalConfig{
Title: "Simple Alert",
Text: "This alert has no icon and custom OK text.",
OkText: "Got it!",
OnClose: func() {
alertResult.Set("Simple alert dismissed")
},
})
}

// Event handlers for confirm
handleShowConfirm := func() {
triggerConfirm(app.ModalConfig{
Icon: "❓",
Title: "Confirm Action",
Text: "Do you want to proceed with this action?",
OnResult: func(confirmed bool) {
if confirmed {
confirmResult.Set("User confirmed the action")
} else {
confirmResult.Set("User cancelled the action")
}
},
})
}

handleShowConfirmCustom := func() {
triggerConfirm(app.ModalConfig{
Icon: "🗑️",
Title: "Delete Item",
Text: "Are you sure you want to delete this item? This action cannot be undone.",
OkText: "Delete",
CancelText: "Keep",
OnResult: func(confirmed bool) {
if confirmed {
confirmResult.Set("Item deleted")
} else {
confirmResult.Set("Item kept")
}
},
})
}

// Read state values
currentAlertResult := alertResult.Get()
currentConfirmResult := confirmResult.Get()

return vdom.H("div", map[string]any{
"className": "max-w-4xl mx-auto p-8",
},
vdom.H("h1", map[string]any{
"className": "text-3xl font-bold mb-6 text-white",
}, "Tsunami Modal Test"),

// Alert Modal Section
vdom.H("div", map[string]any{
"className": "mb-8 p-6 bg-gray-800 rounded-lg border border-gray-700",
},
vdom.H("h2", map[string]any{
"className": "text-2xl font-semibold mb-4 text-white",
}, "Alert Modals"),
vdom.H("div", map[string]any{
"className": "flex gap-4 mb-4",
},
vdom.H("button", map[string]any{
"className": "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed",
"onClick": handleShowAlert,
"disabled": alertOpen,
}, "Show Alert with Icon"),
vdom.H("button", map[string]any{
"className": "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed",
"onClick": handleShowAlertSimple,
"disabled": alertOpen,
}, "Show Simple Alert"),
),
vdom.If(currentAlertResult != "", vdom.H("div", map[string]any{
"className": "mt-4 p-3 bg-gray-700 rounded text-gray-200",
}, "Result: ", currentAlertResult)),
),

// Confirm Modal Section
vdom.H("div", map[string]any{
"className": "mb-8 p-6 bg-gray-800 rounded-lg border border-gray-700",
},
vdom.H("h2", map[string]any{
"className": "text-2xl font-semibold mb-4 text-white",
}, "Confirm Modals"),
vdom.H("div", map[string]any{
"className": "flex gap-4 mb-4",
},
vdom.H("button", map[string]any{
"className": "px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed",
"onClick": handleShowConfirm,
"disabled": confirmOpen,
}, "Show Confirm Modal"),
vdom.H("button", map[string]any{
"className": "px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed",
"onClick": handleShowConfirmCustom,
"disabled": confirmOpen,
}, "Show Delete Confirm"),
),
vdom.If(currentConfirmResult != "", vdom.H("div", map[string]any{
"className": "mt-4 p-3 bg-gray-700 rounded text-gray-200",
}, "Result: ", currentConfirmResult)),
),

// Status info
vdom.H("div", map[string]any{
"className": "p-6 bg-gray-800 rounded-lg border border-gray-700",
},
vdom.H("h2", map[string]any{
"className": "text-2xl font-semibold mb-4 text-white",
}, "Modal Status"),
vdom.H("div", map[string]any{
"className": "text-gray-300",
},
vdom.H("div", nil, "Alert Modal Open: ", vdom.IfElse(alertOpen, "Yes", "No")),
vdom.H("div", nil, "Confirm Modal Open: ", vdom.IfElse(confirmOpen, "Yes", "No")),
),
),
)
})
12 changes: 12 additions & 0 deletions tsunami/demo/modaltest/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module github.com/wavetermdev/waveterm/tsunami/demo/modaltest

go 1.24.6

require github.com/wavetermdev/waveterm/tsunami v0.0.0-00010101000000-000000000000

require (
github.com/google/uuid v1.6.0 // indirect
github.com/outrigdev/goid v0.3.0 // indirect
)

replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Replace directive uses hardcoded absolute path that breaks portability.

The replace directive references /Users/mike/work/waveterm/tsunami, which is specific to one developer's machine. This will fail on other machines and in CI/CD pipelines.

Use a relative path instead:

-replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami
+replace github.com/wavetermdev/waveterm/tsunami => ../../../tsunami

Alternatively, if the repository uses Go workspaces (go.work at the repository root), consider removing this replace directive and relying on the workspace configuration instead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami
replace github.com/wavetermdev/waveterm/tsunami => ../../../tsunami
🤖 Prompt for AI Agents
In tsunami/demo/modaltest/go.mod around line 12, the replace directive points to
an absolute path (/Users/mike/work/waveterm/tsunami) which breaks portability;
change this to a relative path from the module (e.g., a relative path that
reaches the local waveterm/tsunami module) so other devs and CI can resolve it,
or if your repository uses a go.work at the repo root, remove this replace
directive and rely on the workspace configuration instead.

4 changes: 4 additions & 0 deletions tsunami/demo/modaltest/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws=
github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE=
Loading
Loading