diff --git a/cmd/wsh/cmd/wshcmd-aww.go b/cmd/wsh/cmd/wshcmd-aww.go
new file mode 100644
index 000000000..6a0943e21
--- /dev/null
+++ b/cmd/wsh/cmd/wshcmd-aww.go
@@ -0,0 +1,308 @@
+package cmd
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+
+ "image"
+ _ "image/gif"
+ "image/jpeg"
+ _ "image/png"
+
+ "github.com/nfnt/resize"
+ "github.com/spf13/cobra"
+ "github.com/wavetermdev/waveterm/pkg/vdom"
+ "github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
+)
+
+type RedditListing struct {
+ Data struct {
+ Children []struct {
+ Data struct {
+ Title string `json:"title"`
+ URL string `json:"url"`
+ Permalink string `json:"permalink"`
+ } `json:"data"`
+ } `json:"children"`
+ } `json:"data"`
+}
+
+var postPosition int
+
+var awwCmd = &cobra.Command{
+ Use: "aww [position]",
+ Short: "show today's top r/aww image",
+ RunE: awwRun,
+}
+
+func init() {
+ awwCmd.Flags().IntVarP(&postPosition, "position", "p", 0, "position of post (0-based)")
+ rootCmd.AddCommand(awwCmd)
+}
+
+func AwwStyleTag(ctx context.Context, props map[string]any) any {
+ return vdom.Bind(`
+
+ `, nil)
+}
+
+func AwwContentTag(ctx context.Context, props map[string]any) any {
+ isLoading := GlobalVDomClient.GetAtomVal("isLoading").(bool)
+ errorMsg := GlobalVDomClient.GetAtomVal("errorMsg").(string)
+ imageData := GlobalVDomClient.GetAtomVal("imageData")
+ title := GlobalVDomClient.GetAtomVal("title")
+ postURL := GlobalVDomClient.GetAtomVal("postURL")
+
+ if isLoading {
+ return vdom.Bind(`
+
+
Finding today's cutest image...
+
+ `, nil)
+ }
+
+ if errorMsg != "" {
+ return vdom.Bind(`
+
+ `, map[string]any{
+ "error": errorMsg,
+ })
+ }
+
+ return vdom.Bind(`
+
+ `, map[string]any{
+ "title": title,
+ "imageData": imageData,
+ "postURL": postURL,
+ })
+}
+
+// downloadImage downloads an image from URL and returns its bytes
+func downloadImage(url string) ([]byte, error) {
+ resp, err := http.Get(url)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("bad status: %s", resp.Status)
+ }
+
+ return io.ReadAll(resp.Body)
+}
+
+// resizeImage resizes the image to be under 64KB
+func resizeImage(imgBytes []byte) (string, error) {
+ // Decode image
+ img, _, err := image.Decode(strings.NewReader(string(imgBytes)))
+ if err != nil {
+ return "", err
+ }
+
+ // Start with original size
+ width := uint(img.Bounds().Dx())
+ height := uint(img.Bounds().Dy())
+ scaleFactor := 1.0
+
+ // Keep trying until we get under 64KB
+ for {
+ // Resize image
+ newWidth := uint(float64(width) * scaleFactor)
+ newHeight := uint(float64(height) * scaleFactor)
+ resized := resize.Resize(newWidth, newHeight, img, resize.Lanczos3)
+
+ // Encode to JPEG with quality 85
+ var buf strings.Builder
+ jpeg.Encode(base64.NewEncoder(base64.StdEncoding, &buf), resized, &jpeg.Options{Quality: 85})
+ encoded := buf.String()
+
+ // Check if we're under 64KB
+ if len(encoded) < 64*1024 {
+ return fmt.Sprintf("data:image/jpeg;base64,%s", encoded), nil
+ }
+
+ // Reduce size by 10% and try again
+ scaleFactor *= 0.9
+ }
+}
+
+func fetchTopAwwPost(position int) (string, string, string, error) {
+ // Get a few extra posts in case some aren't images
+ log.Printf("Fetching top %d posts from r/aww, pos:%d\n", position+5, position)
+ limit := position + 5
+ resp, err := http.Get(fmt.Sprintf("https://www.reddit.com/r/aww/top.json?t=day&limit=%d", limit))
+ if err != nil {
+ return "", "", "", err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return "", "", "", fmt.Errorf("bad status: %s", resp.Status)
+ }
+
+ var listing RedditListing
+ if err := json.NewDecoder(resp.Body).Decode(&listing); err != nil {
+ return "", "", "", err
+ }
+ log.Printf("got listings num:%d\n", len(listing.Data.Children))
+
+ if len(listing.Data.Children) <= position {
+ return "", "", "", fmt.Errorf("no post found at position %d", position)
+ }
+
+ post := listing.Data.Children[position].Data
+ return post.Title, post.URL, "https://reddit.com" + post.Permalink, nil
+}
+
+func awwRun(cmd *cobra.Command, args []string) error {
+ if postPosition < 0 {
+ return fmt.Errorf("position must be non-negative")
+ }
+
+ client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
+ if err != nil {
+ return err
+ }
+ GlobalVDomClient = client
+
+ // Register components
+ client.RegisterComponent("AwwStyleTag", AwwStyleTag)
+ client.RegisterComponent("AwwContentTag", AwwContentTag)
+
+ // Initialize state
+ client.SetAtomVal("isLoading", true)
+ client.SetAtomVal("errorMsg", "")
+ client.SetAtomVal("imageData", "")
+ client.SetAtomVal("title", "")
+ client.SetAtomVal("postURL", "")
+
+ // Set root element
+ client.SetRootElem(vdom.Bind(`
+
+ `, nil))
+
+ // Create VDOM context
+ err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true})
+ if err != nil {
+ return err
+ }
+
+ // Start fetching the image
+ go func() {
+ // Fetch top post
+ title, imageURL, postURL, err := fetchTopAwwPost(postPosition)
+ if err != nil {
+ client.SetAtomVal("errorMsg", fmt.Sprintf("Error fetching post: %v", err))
+ client.SetAtomVal("isLoading", false)
+ client.SendAsyncInitiation()
+ return
+ }
+
+ // Download image
+ imgBytes, err := downloadImage(imageURL)
+ if err != nil {
+ client.SetAtomVal("errorMsg", fmt.Sprintf("Error downloading image: %v", err))
+ client.SetAtomVal("isLoading", false)
+ client.SendAsyncInitiation()
+ return
+ }
+
+ // Resize and encode image
+ base64Image, err := resizeImage(imgBytes)
+ if err != nil {
+ client.SetAtomVal("errorMsg", fmt.Sprintf("Error processing image: %v", err))
+ client.SetAtomVal("isLoading", false)
+ client.SendAsyncInitiation()
+ return
+ }
+
+ // Update state with results
+ client.SetAtomVal("title", title)
+ client.SetAtomVal("imageData", base64Image)
+ client.SetAtomVal("postURL", postURL)
+ client.SetAtomVal("isLoading", false)
+ client.SendAsyncInitiation()
+ }()
+
+ <-client.DoneCh
+ return nil
+}
diff --git a/cmd/wsh/cmd/wshcmd-ls.go b/cmd/wsh/cmd/wshcmd-ls.go
new file mode 100644
index 000000000..3a44bcb3f
--- /dev/null
+++ b/cmd/wsh/cmd/wshcmd-ls.go
@@ -0,0 +1,258 @@
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/spf13/cobra"
+ "github.com/wavetermdev/waveterm/pkg/vdom"
+ "github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
+)
+
+var lsCmd = &cobra.Command{
+ Use: "ls [directory]",
+ Short: "list directory contents with details",
+ RunE: lsRun,
+}
+
+func init() {
+ rootCmd.AddCommand(lsCmd)
+}
+
+type FileInfo struct {
+ Name string
+ Size int64
+ Mode os.FileMode
+ ModTime time.Time
+ IsDir bool
+ Extension string
+}
+
+func LsStyleTag(ctx context.Context, props map[string]any) any {
+ return vdom.Bind(`
+
+ `, nil)
+}
+
+// formatSize converts bytes to human readable format
+func formatSize(size int64) string {
+ if size < 1024 {
+ return fmt.Sprintf("%d B", size)
+ }
+ units := []string{"B", "KB", "MB", "GB", "TB"}
+ div := float64(1)
+ unitIndex := 0
+ for size/int64(div) >= 1024 && unitIndex < len(units)-1 {
+ div *= 1024
+ unitIndex++
+ }
+ return fmt.Sprintf("%.1f %s", float64(size)/div, units[unitIndex])
+}
+
+// formatMode formats file permissions in a Unix-like style
+func formatMode(mode os.FileMode) string {
+ output := ""
+ if mode.IsDir() {
+ output += "d"
+ } else {
+ output += "-"
+ }
+
+ // User permissions
+ output += formatPerm(mode, 6)
+ // Group permissions
+ output += formatPerm(mode, 3)
+ // Others permissions
+ output += formatPerm(mode, 0)
+
+ return output
+}
+
+// formatPerm formats a permission triplet
+func formatPerm(mode os.FileMode, shift uint) string {
+ output := ""
+ output += map[bool]string{true: "r", false: "-"}[(mode>>(shift+2))&1 == 1]
+ output += map[bool]string{true: "w", false: "-"}[(mode>>(shift+1))&1 == 1]
+ output += map[bool]string{true: "x", false: "-"}[(mode>>shift)&1 == 1]
+ return output
+}
+
+func LsContentTag(ctx context.Context, props map[string]any) any {
+ path := props["path"].(string)
+
+ entries, err := os.ReadDir(path)
+ if err != nil {
+ return vdom.Bind(`
+ Error reading directory:
+ `, map[string]any{
+ "error": err.Error(),
+ })
+ }
+
+ // Convert to FileInfo and sort
+ files := make([]FileInfo, 0, len(entries))
+ for _, entry := range entries {
+ info, err := entry.Info()
+ if err != nil {
+ continue
+ }
+ files = append(files, FileInfo{
+ Name: info.Name(),
+ Size: info.Size(),
+ Mode: info.Mode(),
+ ModTime: info.ModTime(),
+ IsDir: info.IsDir(),
+ Extension: strings.ToLower(filepath.Ext(info.Name())),
+ })
+ }
+
+ // Sort: directories first, then by name
+ sort.Slice(files, func(i, j int) bool {
+ if files[i].IsDir != files[j].IsDir {
+ return files[i].IsDir
+ }
+ return files[i].Name < files[j].Name
+ })
+
+ template := `
+
+
+
+ Mode |
+ Size |
+ Modified |
+ Name |
+
`
+
+ for _, file := range files {
+ nameClass := "file-name"
+ if file.IsDir {
+ nameClass = "dir-name"
+ }
+
+ template += fmt.Sprintf(`
+
+ %s |
+ %s |
+ %s |
+ %s |
+
`,
+ formatMode(file.Mode),
+ formatSize(file.Size),
+ file.ModTime.Format("Jan 02 15:04"),
+ nameClass,
+ file.Name)
+ }
+
+ template += `
+
+
`
+
+ return vdom.Bind(template, nil)
+}
+
+func lsRun(cmd *cobra.Command, args []string) error {
+ path := "."
+ if len(args) > 0 {
+ path = args[0]
+ }
+
+ absPath, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
+
+ client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
+ if err != nil {
+ return err
+ }
+ GlobalVDomClient = client
+
+ // Register components
+ client.RegisterComponent("LsStyleTag", LsStyleTag)
+ client.RegisterComponent("LsContentTag", LsContentTag)
+
+ // Set root element
+ client.SetRootElem(vdom.Bind(`
+
+
+
+
+ `, map[string]any{
+ "path": absPath,
+ }))
+
+ // Create VDOM context
+ err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true})
+ if err != nil {
+ return err
+ }
+
+ <-client.DoneCh
+ return nil
+}
diff --git a/cmd/wsh/cmd/wshcmd-rhyme.go b/cmd/wsh/cmd/wshcmd-rhyme.go
new file mode 100644
index 000000000..34518a223
--- /dev/null
+++ b/cmd/wsh/cmd/wshcmd-rhyme.go
@@ -0,0 +1,320 @@
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "strings"
+
+ "github.com/sashabaranov/go-openai"
+ "github.com/spf13/cobra"
+ "github.com/wavetermdev/waveterm/pkg/vdom"
+ "github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
+)
+
+const OPENAI_API_KEY = ""
+
+var rhymeCmd = &cobra.Command{
+ Use: "rhyme [word]",
+ Short: "find rhyming words using ChatGPT",
+ RunE: rhymeRun,
+}
+
+func init() {
+ rootCmd.AddCommand(rhymeCmd)
+}
+
+type RhymeCategory struct {
+ Type string
+ Words []string
+}
+
+func RhymeStyleTag(ctx context.Context, props map[string]any) any {
+ return vdom.Bind(`
+
+ `, nil)
+}
+func RhymeContentTag(ctx context.Context, props map[string]any) any {
+ word := props["word"].(string)
+ isLoading := GlobalVDomClient.GetAtomVal("isLoading").(bool)
+ errorMsg := GlobalVDomClient.GetAtomVal("errorMsg").(string)
+ rhymeData := GlobalVDomClient.GetAtomVal("rhymeData")
+
+ if isLoading {
+ return vdom.Bind(`
+
+ `, map[string]any{
+ "word": word,
+ })
+ }
+
+ if errorMsg != "" {
+ return vdom.Bind(`
+
+ `, map[string]any{
+ "word": word,
+ "error": errorMsg,
+ })
+ }
+
+ if rhymeData == nil {
+ return vdom.Bind(`
+
+ `, map[string]any{
+ "word": word,
+ })
+ }
+
+ categories := rhymeData.([]RhymeCategory)
+ if len(categories) == 0 {
+ return vdom.Bind(`
+
+ `, map[string]any{
+ "word": word,
+ })
+ }
+
+ template := `
+
+
Rhymes for ""
`
+
+ for _, category := range categories {
+ template += fmt.Sprintf(`
+
+
%s
+
`, category.Type)
+
+ for _, rhyme := range category.Words {
+ template += fmt.Sprintf(`
+
%s
`, rhyme)
+ }
+
+ template += `
+
+
`
+ }
+
+ template += `
`
+
+ return vdom.Bind(template, map[string]any{
+ "word": word,
+ })
+}
+
+func findRhymes(client *openai.Client, word string) ([]RhymeCategory, error) {
+ prompt := fmt.Sprintf(`For the word "%s", provide rhyming words in these categories:
+1. Perfect rhymes (same ending sound)
+2. Near rhymes (similar ending sound)
+3. Family rhymes (same word ending)
+
+Format the response as a simple list with categories and words like this:
+Perfect rhymes: word1, word2, word3
+Near rhymes: word1, word2, word3
+Family rhymes: word1, word2, word3
+
+Only include words that actually exist in English.`, word)
+
+ resp, err := client.CreateChatCompletion(
+ context.Background(),
+ openai.ChatCompletionRequest{
+ Model: openai.GPT3Dot5Turbo,
+ Messages: []openai.ChatCompletionMessage{
+ {
+ Role: openai.ChatMessageRoleUser,
+ Content: prompt,
+ },
+ },
+ Temperature: 0.7,
+ },
+ )
+
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse the response
+ response := resp.Choices[0].Message.Content
+ categories := []RhymeCategory{}
+
+ // Split into lines and parse each category
+ lines := strings.Split(response, "\n")
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+
+ parts := strings.Split(line, ":")
+ if len(parts) != 2 {
+ continue
+ }
+
+ categoryType := strings.TrimSpace(parts[0])
+ wordList := strings.Split(strings.TrimSpace(parts[1]), ",")
+
+ // Clean up the words
+ words := make([]string, 0)
+ for _, word := range wordList {
+ word = strings.TrimSpace(word)
+ if word != "" {
+ words = append(words, word)
+ }
+ }
+
+ if len(words) > 0 {
+ categories = append(categories, RhymeCategory{
+ Type: categoryType,
+ Words: words,
+ })
+ }
+ }
+
+ return categories, nil
+}
+
+func rhymeRun(cmd *cobra.Command, args []string) error {
+ if len(args) < 1 {
+ return fmt.Errorf("please provide a word to find rhymes for")
+ }
+ word := args[0]
+
+ client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
+ if err != nil {
+ return err
+ }
+ GlobalVDomClient = client
+
+ // Register components
+ client.RegisterComponent("RhymeStyleTag", RhymeStyleTag)
+ client.RegisterComponent("RhymeContentTag", RhymeContentTag)
+
+ // Initialize state
+ client.SetAtomVal("isLoading", true)
+ client.SetAtomVal("errorMsg", "")
+ client.SetAtomVal("rhymeData", nil)
+
+ // Set root element
+ client.SetRootElem(vdom.Bind(`
+
+
+
+
+ `, map[string]any{
+ "word": word,
+ }))
+
+ // Create VDOM context
+ err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true})
+ if err != nil {
+ return err
+ }
+
+ // Start fetching rhymes
+ go func() {
+ openaiClient := openai.NewClient(OPENAI_API_KEY)
+ rhymes, err := findRhymes(openaiClient, word)
+
+ if err != nil {
+ log.Printf("Error finding rhymes: %v", err)
+ client.SetAtomVal("errorMsg", fmt.Sprintf("Error finding rhymes: %v", err))
+ } else {
+ client.SetAtomVal("rhymeData", rhymes)
+ }
+
+ client.SetAtomVal("isLoading", false)
+ client.SendAsyncInitiation()
+ }()
+
+ <-client.DoneCh
+ return nil
+}
diff --git a/cmd/wsh/cmd/wshcmd-vdom.go b/cmd/wsh/cmd/wshcmd-vdom.go
new file mode 100644
index 000000000..a576fd016
--- /dev/null
+++ b/cmd/wsh/cmd/wshcmd-vdom.go
@@ -0,0 +1,255 @@
+package cmd
+
+import (
+ "context"
+ "log"
+ "math/rand"
+ "strconv"
+ "time"
+
+ "github.com/spf13/cobra"
+ "github.com/wavetermdev/waveterm/pkg/vdom"
+ "github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
+)
+
+var moleCmd = &cobra.Command{
+ Use: "mole",
+ Hidden: true,
+ Short: "launch whack-a-mole game",
+ RunE: moleRun,
+}
+
+func init() {
+ rootCmd.AddCommand(moleCmd)
+}
+func MoleCellTag(ctx context.Context, props map[string]any) any {
+ index := props["index"].(int)
+ molePosition := GlobalVDomClient.GetAtomVal("molePosition").(int)
+
+ className := "cell"
+ showMole := false
+ if molePosition == index {
+ className += " mole"
+ showMole = true
+ }
+
+ template := `
+ `
+
+ return vdom.Bind(template, map[string]any{
+ "className": className,
+ "clickHandler": props["onCellClick"],
+ })
+}
+
+func MoleStyleTag(ctx context.Context, props map[string]any) any {
+ return vdom.Bind(`
+
+ `, nil)
+}
+
+var moleScore int = 0
+
+func MoleGameTag(ctx context.Context, props map[string]any) any {
+ makeHandleCellClick := func(index int) func() {
+ return func() {
+ currentScore := GlobalVDomClient.GetAtomVal("moleScore").(int)
+ molePosition := GlobalVDomClient.GetAtomVal("molePosition").(int)
+ isActive := GlobalVDomClient.GetAtomVal("moleGameActive").(bool)
+
+ log.Printf("cell clicked: %d (active:%v)\n", index, isActive)
+
+ if !isActive {
+ return
+ }
+
+ // Check if we clicked the right mole
+ if molePosition == index {
+ GlobalVDomClient.SetAtomVal("moleScore", currentScore+1)
+ moleScore++
+
+ // Move mole to new random position
+ newPosition := rand.Intn(9)
+ GlobalVDomClient.SetAtomVal("molePosition", newPosition)
+ }
+ }
+ }
+
+ toggleGame := func() {
+ isActive := GlobalVDomClient.GetAtomVal("moleGameActive").(bool)
+ if isActive {
+ GlobalVDomClient.SetAtomVal("moleGameActive", false)
+ GlobalVDomClient.SetAtomVal("moleScore", 0)
+ moleScore = 0
+ GlobalVDomClient.SetAtomVal("molePosition", -1)
+ } else {
+ GlobalVDomClient.SetAtomVal("moleGameActive", true)
+ GlobalVDomClient.SetAtomVal("molePosition", rand.Intn(9))
+ }
+ }
+
+ isActive := GlobalVDomClient.GetAtomVal("moleGameActive").(bool)
+ buttonText := "Start Game"
+ if isActive {
+ buttonText = "Stop Game"
+ }
+
+ return vdom.Bind(`
+
+
+
Score:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `, map[string]any{
+ "toggleGame": toggleGame,
+ "buttonText": buttonText,
+ "index0": 0,
+ "index1": 1,
+ "index2": 2,
+ "index3": 3,
+ "index4": 4,
+ "index5": 5,
+ "index6": 6,
+ "index7": 7,
+ "index8": 8,
+ "click0": makeHandleCellClick(0),
+ "click1": makeHandleCellClick(1),
+ "click2": makeHandleCellClick(2),
+ "click3": makeHandleCellClick(3),
+ "click4": makeHandleCellClick(4),
+ "click5": makeHandleCellClick(5),
+ "click6": makeHandleCellClick(6),
+ "click7": makeHandleCellClick(7),
+ "click8": makeHandleCellClick(8),
+ "moleScore": strconv.Itoa(moleScore),
+ })
+}
+
+func moleRun(cmd *cobra.Command, args []string) error {
+ client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
+ if err != nil {
+ return err
+ }
+ GlobalVDomClient = client
+
+ // Initialize random seed
+ rand.Seed(time.Now().UnixNano())
+
+ // Register components
+ client.RegisterComponent("MoleStyleTag", MoleStyleTag)
+ client.RegisterComponent("MoleCellTag", MoleCellTag)
+ client.RegisterComponent("MoleGameTag", MoleGameTag)
+
+ // Initialize state
+ client.SetAtomVal("moleScore", 0)
+ client.SetAtomVal("molePosition", -1)
+ client.SetAtomVal("moleGameActive", false)
+
+ // Set root element
+ client.SetRootElem(vdom.Bind(``, nil))
+
+ // Create VDOM context
+ err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true})
+ if err != nil {
+ return err
+ }
+
+ // Start game loop when active
+ go func() {
+ for {
+ time.Sleep(1 * time.Second)
+ if client.GetAtomVal("moleGameActive").(bool) {
+ newPos := rand.Intn(9)
+ client.SetAtomVal("molePosition", newPos)
+ log.Printf("new mole position: %d\n", newPos)
+ client.SendAsyncInitiation()
+ }
+ }
+ }()
+
+ <-client.DoneCh
+ return nil
+}
diff --git a/cmd/wsh/cmd/wshcmd-word.go b/cmd/wsh/cmd/wshcmd-word.go
new file mode 100644
index 000000000..fb01d4534
--- /dev/null
+++ b/cmd/wsh/cmd/wshcmd-word.go
@@ -0,0 +1,282 @@
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/spf13/cobra"
+ "github.com/wavetermdev/waveterm/pkg/vdom"
+ "github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
+)
+
+// DictionaryResponse represents the API response structure
+type DictionaryResponse []struct {
+ Word string `json:"word"`
+ Phonetic string `json:"phonetic"`
+ Meanings []Meaning `json:"meanings"`
+}
+
+type Meaning struct {
+ PartOfSpeech string `json:"partOfSpeech"`
+ Definitions []struct {
+ Definition string `json:"definition"`
+ Example string `json:"example"`
+ Synonyms []string `json:"synonyms"`
+ } `json:"definitions"`
+}
+
+var wordCmd = &cobra.Command{
+ Use: "word",
+ Hidden: true,
+ Short: "show word of the day",
+ RunE: wordRun,
+}
+
+func init() {
+ rootCmd.AddCommand(wordCmd)
+}
+
+func WordStyleTag(ctx context.Context, props map[string]any) any {
+ return vdom.Bind(`
+
+ `, nil)
+}
+func WordContentTag(ctx context.Context, props map[string]any) any {
+ wordData := GlobalVDomClient.GetAtomVal("wordData")
+ isLoading := GlobalVDomClient.GetAtomVal("isLoading").(bool)
+ errorMsg := GlobalVDomClient.GetAtomVal("errorMsg").(string)
+
+ if isLoading {
+ return vdom.Bind(`
+ Loading word of the day...
+ `, nil)
+ }
+
+ if errorMsg != "" {
+ return vdom.Bind(`
+
+ `, map[string]any{
+ "error": errorMsg,
+ })
+ }
+
+ if wordData == nil {
+ return vdom.Bind(`
+ Initializing...
+ `, nil)
+ }
+
+ response := wordData.(DictionaryResponse)
+ if len(response) == 0 {
+ return vdom.Bind(`
+ No word data found
+ `, nil)
+ }
+
+ word := response[0]
+
+ template := `
+
+
+
+ `
+
+ for _, meaning := range word.Meanings {
+ template += fmt.Sprintf(`
+
%s
+ `, meaning.PartOfSpeech)
+
+ for _, def := range meaning.Definitions {
+ template += fmt.Sprintf(`
+
%s
+ `, def.Definition)
+
+ if def.Example != "" {
+ template += fmt.Sprintf(`
+
"%s"
+ `, def.Example)
+ }
+
+ if len(def.Synonyms) > 0 {
+ template += `
`
+ for _, syn := range def.Synonyms[:min(5, len(def.Synonyms))] {
+ template += fmt.Sprintf(`
+ %s
+ `, syn)
+ }
+ template += `
`
+ }
+ }
+ }
+
+ template += `
`
+
+ return vdom.Bind(template, map[string]any{
+ "word": word.Word,
+ "phonetic": word.Phonetic,
+ })
+}
+
+// min returns the minimum of two integers
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+// getRandomWord returns a random word to look up
+func getRandomWord() string {
+ words := []string{
+ "serendipity", "ephemeral", "mellifluous", "ineffable", "surreptitious",
+ "ethereal", "melancholy", "perspicacious", "sanguine", "vociferous",
+ "arduous", "mundane", "pragmatic", "resilient", "tenacious",
+ }
+ return words[time.Now().Unix()%int64(len(words))]
+}
+
+func fetchWordDefinition(word string) (DictionaryResponse, error) {
+ url := fmt.Sprintf("https://api.dictionaryapi.dev/api/v2/entries/en/%s", word)
+ resp, err := http.Get(url)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("API returned status code: %d", resp.StatusCode)
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ var result DictionaryResponse
+ if err := json.Unmarshal(body, &result); err != nil {
+ return nil, err
+ }
+
+ return result, nil
+}
+
+func wordRun(cmd *cobra.Command, args []string) error {
+ client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
+ if err != nil {
+ return err
+ }
+ GlobalVDomClient = client
+
+ // Register components
+ client.RegisterComponent("WordStyleTag", WordStyleTag)
+ client.RegisterComponent("WordContentTag", WordContentTag)
+
+ // Initialize state
+ client.SetAtomVal("wordData", nil)
+ client.SetAtomVal("isLoading", true)
+ client.SetAtomVal("errorMsg", "")
+
+ // Set root element
+ client.SetRootElem(vdom.Bind(`
+
+
+
+
+ `, nil))
+
+ // Create VDOM context
+ err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: true})
+ if err != nil {
+ return err
+ }
+
+ // Fetch word data
+ go func() {
+ word := getRandomWord()
+ data, err := fetchWordDefinition(word)
+ if err != nil {
+ client.SetAtomVal("errorMsg", fmt.Sprintf("Error fetching word: %v", err))
+ } else {
+ client.SetAtomVal("wordData", data)
+ }
+ client.SetAtomVal("isLoading", false)
+ client.SendAsyncInitiation()
+ }()
+
+ <-client.DoneCh
+ return nil
+}
diff --git a/frontend/app/view/vdom/vdom.tsx b/frontend/app/view/vdom/vdom.tsx
index 08adfee7e..0974a752e 100644
--- a/frontend/app/view/vdom/vdom.tsx
+++ b/frontend/app/view/vdom/vdom.tsx
@@ -294,6 +294,9 @@ function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
return childrenComps;
}
props.key = "e-" + elem.waveid;
+ if (elem.tag == "img") {
+ childrenComps = null;
+ }
return React.createElement(elem.tag, props, childrenComps);
}
diff --git a/go.mod b/go.mod
index 7645166bc..4bb37ef19 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ require (
github.com/kevinburke/ssh_config v1.2.0
github.com/mattn/go-sqlite3 v1.14.24
github.com/mitchellh/mapstructure v1.5.0
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/sashabaranov/go-openai v1.32.3
github.com/sawka/txwrap v0.2.0
github.com/shirou/gopsutil/v4 v4.24.9
diff --git a/go.sum b/go.sum
index cb329ca3f..5d2fc691b 100644
--- a/go.sum
+++ b/go.sum
@@ -51,6 +51,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b h1:cLGKfKb1uk0hxI0Q8L83UAJPpeJ+gSpn3cCU/tjd3eg=
github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b/go.mod h1:KO+FcPtyLAiRC0hJwreJVvfwc7vnNz77UxBTIGHdPVk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go
index 79ee5d743..2c1524c0f 100644
--- a/pkg/vdom/vdomclient/vdomclient.go
+++ b/pkg/vdom/vdomclient/vdomclient.go
@@ -65,7 +65,7 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData)
}
}
- if feUpdate.Resync {
+ if feUpdate.Resync || true {
return impl.Client.fullRender()
}
return impl.Client.incrementalRender()