|
| 1 | +// Copyright 2025, Command Line Inc. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +package cmd |
| 5 | + |
| 6 | +import ( |
| 7 | + "encoding/json" |
| 8 | + "fmt" |
| 9 | + "sort" |
| 10 | + "strings" |
| 11 | + "text/tabwriter" |
| 12 | + |
| 13 | + "github.com/spf13/cobra" |
| 14 | + "github.com/wavetermdev/waveterm/pkg/waveobj" |
| 15 | + "github.com/wavetermdev/waveterm/pkg/wshrpc" |
| 16 | + "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" |
| 17 | +) |
| 18 | + |
| 19 | +// Command-line flags for the blocks commands |
| 20 | +var ( |
| 21 | + blocksWindowId string // Window ID to filter blocks by |
| 22 | + blocksWorkspaceId string // Workspace ID to filter blocks by |
| 23 | + blocksTabId string // Tab ID to filter blocks by |
| 24 | + blocksView string // View type to filter blocks by (term, web, etc.) |
| 25 | + blocksJSON bool // Whether to output as JSON |
| 26 | + blocksTimeout int // Timeout in milliseconds for RPC calls |
| 27 | +) |
| 28 | + |
| 29 | +// BlockDetails represents the information about a block returned by the list command |
| 30 | +type BlockDetails struct { |
| 31 | + BlockId string `json:"blockid"` // Unique identifier for the block |
| 32 | + WorkspaceId string `json:"workspaceid"` // ID of the workspace containing the block |
| 33 | + TabId string `json:"tabid"` // ID of the tab containing the block |
| 34 | + View string `json:"view"` // Canonical view type (term, web, preview, edit, sysinfo, waveai) |
| 35 | + Meta waveobj.MetaMapType `json:"meta"` // Block metadata including view type |
| 36 | +} |
| 37 | + |
| 38 | +// blocksListCmd represents the 'blocks list' command |
| 39 | +var blocksListCmd = &cobra.Command{ |
| 40 | + Use: "list", |
| 41 | + Aliases: []string{"ls", "get"}, |
| 42 | + Short: "List blocks in workspaces/windows", |
| 43 | + Long: `List blocks with optional filtering by workspace, window, tab, or view type. |
| 44 | +
|
| 45 | +Examples: |
| 46 | + # List blocks from all workspaces |
| 47 | + wsh blocks list |
| 48 | +
|
| 49 | + # List only terminal blocks |
| 50 | + wsh blocks list --view=term |
| 51 | +
|
| 52 | + # Filter by window ID (get IDs from 'wsh workspace list') |
| 53 | + wsh blocks list --window=dbca23b5-f89b-4780-a0fe-452f5bc7d900 |
| 54 | +
|
| 55 | + # Filter by workspace ID |
| 56 | + wsh blocks list --workspace=12d0c067-378e-454c-872e-77a314248114 |
| 57 | +
|
| 58 | + # Filter by tab ID |
| 59 | + wsh blocks list --tab=a0459921-cc1a-48cc-ae7b-5f4821e1c9e1 |
| 60 | +
|
| 61 | + # Output as JSON for scripting |
| 62 | + wsh blocks list --json |
| 63 | +
|
| 64 | + # Set a different timeout (in milliseconds) |
| 65 | + wsh blocks list --timeout=10000`, |
| 66 | + RunE: blocksListRun, |
| 67 | + PreRunE: preRunSetupRpcClient, |
| 68 | + SilenceUsage: true, |
| 69 | +} |
| 70 | + |
| 71 | +// init registers the blocks commands with the root command |
| 72 | +// It configures all the flags and command options |
| 73 | +func init() { |
| 74 | + blocksListCmd.Flags().StringVar(&blocksWindowId, "window", "", "restrict to window id") |
| 75 | + blocksListCmd.Flags().StringVar(&blocksWorkspaceId, "workspace", "", "restrict to workspace id") |
| 76 | + blocksListCmd.Flags().StringVar(&blocksTabId, "tab", "", "restrict to specific tab id") |
| 77 | + blocksListCmd.Flags().StringVar(&blocksView, "view", "", "restrict to view type (term/terminal, web/browser, preview/edit, sysinfo, waveai)") |
| 78 | + blocksListCmd.Flags().BoolVar(&blocksJSON, "json", false, "output as JSON") |
| 79 | + blocksListCmd.Flags().IntVar(&blocksTimeout, "timeout", 5000, "timeout in milliseconds for RPC calls (default: 5000)") |
| 80 | + |
| 81 | + for _, cmd := range rootCmd.Commands() { |
| 82 | + if cmd.Use == "blocks" { |
| 83 | + cmd.AddCommand(blocksListCmd) |
| 84 | + return |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + blocksCmd := &cobra.Command{ |
| 89 | + Use: "blocks", |
| 90 | + Short: "Manage blocks", |
| 91 | + Long: "Commands for working with blocks", |
| 92 | + } |
| 93 | + |
| 94 | + blocksCmd.AddCommand(blocksListCmd) |
| 95 | + rootCmd.AddCommand(blocksCmd) |
| 96 | +} |
| 97 | + |
| 98 | +// blocksListRun implements the 'blocks list' command |
| 99 | +// It retrieves and displays blocks with optional filtering by workspace, window, tab, or view type |
| 100 | +func blocksListRun(cmd *cobra.Command, args []string) error { |
| 101 | + if v := strings.TrimSpace(blocksView); v != "" { |
| 102 | + if !isKnownViewFilter(v) { |
| 103 | + return fmt.Errorf("unknown --view %q; try one of: term, web, preview, edit, sysinfo, waveai", v) |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + var allBlocks []BlockDetails |
| 108 | + |
| 109 | + workspaces, err := wshclient.WorkspaceListCommand(RpcClient, &wshrpc.RpcOpts{Timeout: int64(blocksTimeout)}) |
| 110 | + if err != nil { |
| 111 | + return fmt.Errorf("failed to list workspaces: %v", err) |
| 112 | + } |
| 113 | + |
| 114 | + if len(workspaces) == 0 { |
| 115 | + return fmt.Errorf("no workspaces found") |
| 116 | + } |
| 117 | + |
| 118 | + var workspaceIdsToQuery []string |
| 119 | + |
| 120 | + // Determine which workspaces to query |
| 121 | + if blocksWorkspaceId != "" && blocksWindowId != "" { |
| 122 | + return fmt.Errorf("--workspace and --window are mutually exclusive; specify only one") |
| 123 | + } |
| 124 | + if blocksWorkspaceId != "" { |
| 125 | + workspaceIdsToQuery = []string{blocksWorkspaceId} |
| 126 | + } else if blocksWindowId != "" { |
| 127 | + // Find workspace ID for this window |
| 128 | + windowFound := false |
| 129 | + for _, ws := range workspaces { |
| 130 | + if ws.WindowId == blocksWindowId { |
| 131 | + workspaceIdsToQuery = []string{ws.WorkspaceData.OID} |
| 132 | + windowFound = true |
| 133 | + break |
| 134 | + } |
| 135 | + } |
| 136 | + if !windowFound { |
| 137 | + return fmt.Errorf("window %s not found", blocksWindowId) |
| 138 | + } |
| 139 | + } else { |
| 140 | + // Default to all workspaces |
| 141 | + for _, ws := range workspaces { |
| 142 | + workspaceIdsToQuery = append(workspaceIdsToQuery, ws.WorkspaceData.OID) |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + // Query each selected workspace |
| 147 | + hadSuccess := false |
| 148 | + for _, wsId := range workspaceIdsToQuery { |
| 149 | + req := wshrpc.BlocksListRequest{WorkspaceId: wsId} |
| 150 | + if blocksWindowId != "" { |
| 151 | + req.WindowId = blocksWindowId |
| 152 | + } |
| 153 | + |
| 154 | + blocks, err := wshclient.BlocksListCommand(RpcClient, req, &wshrpc.RpcOpts{Timeout: int64(blocksTimeout)}) |
| 155 | + if err != nil { |
| 156 | + WriteStderr("Warning: couldn't list blocks for workspace %s: %v\n", wsId, err) |
| 157 | + continue |
| 158 | + } |
| 159 | + hadSuccess = true |
| 160 | + |
| 161 | + // Apply filters |
| 162 | + for _, b := range blocks { |
| 163 | + if blocksTabId != "" && b.TabId != blocksTabId { |
| 164 | + continue |
| 165 | + } |
| 166 | + |
| 167 | + if blocksView != "" { |
| 168 | + view := b.Meta.GetString(waveobj.MetaKey_View, "") |
| 169 | + |
| 170 | + // Support view type aliases |
| 171 | + if !matchesViewType(view, blocksView) { |
| 172 | + continue |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + v := b.Meta.GetString(waveobj.MetaKey_View, "") |
| 177 | + allBlocks = append(allBlocks, BlockDetails{ |
| 178 | + BlockId: b.BlockId, |
| 179 | + WorkspaceId: b.WorkspaceId, |
| 180 | + TabId: b.TabId, |
| 181 | + View: v, |
| 182 | + Meta: b.Meta, |
| 183 | + }) |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + // No blocks found check |
| 188 | + if len(allBlocks) == 0 { |
| 189 | + if !hadSuccess { |
| 190 | + return fmt.Errorf("failed to list blocks from all %d workspace(s)", len(workspaceIdsToQuery)) |
| 191 | + } |
| 192 | + WriteStdout("No blocks found\n") |
| 193 | + return nil |
| 194 | + } |
| 195 | + |
| 196 | + // Stable ordering for both JSON and table output |
| 197 | + sort.SliceStable(allBlocks, func(i, j int) bool { |
| 198 | + if allBlocks[i].WorkspaceId != allBlocks[j].WorkspaceId { |
| 199 | + return allBlocks[i].WorkspaceId < allBlocks[j].WorkspaceId |
| 200 | + } |
| 201 | + if allBlocks[i].TabId != allBlocks[j].TabId { |
| 202 | + return allBlocks[i].TabId < allBlocks[j].TabId |
| 203 | + } |
| 204 | + return allBlocks[i].BlockId < allBlocks[j].BlockId |
| 205 | + }) |
| 206 | + |
| 207 | + // Output results |
| 208 | + if blocksJSON { |
| 209 | + bytes, err := json.MarshalIndent(allBlocks, "", " ") |
| 210 | + if err != nil { |
| 211 | + return fmt.Errorf("failed to marshal JSON: %v", err) |
| 212 | + } |
| 213 | + WriteStdout("%s\n", string(bytes)) |
| 214 | + return nil |
| 215 | + } |
| 216 | + w := tabwriter.NewWriter(WrappedStdout, 0, 0, 2, ' ', 0) |
| 217 | + defer w.Flush() |
| 218 | + fmt.Fprintf(w, "BLOCK ID\tWORKSPACE\tTAB ID\tVIEW\tCONTENT\n") |
| 219 | + |
| 220 | + for _, b := range allBlocks { |
| 221 | + blockID := b.BlockId |
| 222 | + if len(blockID) > 36 { |
| 223 | + blockID = blockID[:34] + ".." |
| 224 | + } |
| 225 | + view := strings.ToLower(b.View) |
| 226 | + if view == "" { |
| 227 | + view = "<unknown>" |
| 228 | + } |
| 229 | + var content string |
| 230 | + |
| 231 | + switch view { |
| 232 | + case "preview", "edit": |
| 233 | + content = b.Meta.GetString(waveobj.MetaKey_File, "<no file>") |
| 234 | + case "web": |
| 235 | + content = b.Meta.GetString(waveobj.MetaKey_Url, "<no url>") |
| 236 | + case "term": |
| 237 | + content = b.Meta.GetString(waveobj.MetaKey_CmdCwd, "<no cwd>") |
| 238 | + default: |
| 239 | + content = "" |
| 240 | + } |
| 241 | + |
| 242 | + wsID := b.WorkspaceId |
| 243 | + if len(wsID) > 36 { |
| 244 | + wsID = wsID[:34] + ".." |
| 245 | + } |
| 246 | + |
| 247 | + tabID := b.TabId |
| 248 | + if len(tabID) > 36 { |
| 249 | + tabID = tabID[0:34] + ".." |
| 250 | + } |
| 251 | + |
| 252 | + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", blockID, wsID, tabID, view, content) |
| 253 | + } |
| 254 | + |
| 255 | + return nil |
| 256 | +} |
| 257 | + |
| 258 | +// matchesViewType checks if a view type matches a filter, supporting aliases |
| 259 | +func matchesViewType(actual, filter string) bool { |
| 260 | + // Direct match (case insensitive) |
| 261 | + if strings.EqualFold(actual, filter) { |
| 262 | + return true |
| 263 | + } |
| 264 | + |
| 265 | + // Handle aliases |
| 266 | + switch strings.ToLower(filter) { |
| 267 | + case "preview", "edit": |
| 268 | + return strings.EqualFold(actual, "preview") || strings.EqualFold(actual, "edit") |
| 269 | + case "terminal", "term", "shell", "console": |
| 270 | + return strings.EqualFold(actual, "term") |
| 271 | + case "web", "browser", "url": |
| 272 | + return strings.EqualFold(actual, "web") |
| 273 | + case "ai", "waveai", "assistant": |
| 274 | + return strings.EqualFold(actual, "waveai") |
| 275 | + case "sys", "sysinfo", "system": |
| 276 | + return strings.EqualFold(actual, "sysinfo") |
| 277 | + } |
| 278 | + |
| 279 | + return false |
| 280 | +} |
| 281 | + |
| 282 | +// isKnownViewFilter checks if a filter value is recognized |
| 283 | +func isKnownViewFilter(f string) bool { |
| 284 | + switch strings.ToLower(strings.TrimSpace(f)) { |
| 285 | + case "term", "terminal", "shell", "console", |
| 286 | + "web", "browser", "url", |
| 287 | + "preview", "edit", |
| 288 | + "sysinfo", "sys", "system", |
| 289 | + "waveai", "ai", "assistant": |
| 290 | + return true |
| 291 | + default: |
| 292 | + return false |
| 293 | + } |
| 294 | +} |
0 commit comments