From 0a3dadb6285b2752f036c377a017a849b6ba7fe5 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 17 Dec 2024 14:11:40 -0800 Subject: [PATCH] Add `wsh wavepath` command for getting Wave paths (#1545) --- cmd/wsh/cmd/wshcmd-wavepath.go | 135 +++++++++++++++++++++++++++++ docs/docs/wsh-reference.mdx | 48 ++++++++++ frontend/app/store/wshclientapi.ts | 5 ++ frontend/layout/lib/layoutModel.ts | 5 ++ frontend/types/gotypes.d.ts | 10 +++ go.mod | 1 + go.sum | 2 + pkg/waveobj/wtype.go | 1 + pkg/wshrpc/wshclient/wshclient.go | 6 ++ pkg/wshrpc/wshrpctypes.go | 9 ++ pkg/wshrpc/wshserver/wshserver.go | 39 +++++++++ 11 files changed, 261 insertions(+) create mode 100644 cmd/wsh/cmd/wshcmd-wavepath.go diff --git a/cmd/wsh/cmd/wshcmd-wavepath.go b/cmd/wsh/cmd/wshcmd-wavepath.go new file mode 100644 index 000000000..fcfcbc7f3 --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-wavepath.go @@ -0,0 +1,135 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "bytes" + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" +) + +var wavepathCmd = &cobra.Command{ + Use: "wavepath {config|data|log}", + Short: "Get paths to various waveterm files and directories", + RunE: wavepathRun, + PreRunE: preRunSetupRpcClient, +} + +func init() { + wavepathCmd.Flags().BoolP("open", "o", false, "Open the path in a new block") + wavepathCmd.Flags().BoolP("open-external", "O", false, "Open the path in the default external application") + wavepathCmd.Flags().BoolP("tail", "t", false, "Tail the last 100 lines of the log") + rootCmd.AddCommand(wavepathCmd) +} + +func wavepathRun(cmd *cobra.Command, args []string) (rtnErr error) { + defer func() { + sendActivity("wavepath", rtnErr == nil) + }() + + if len(args) == 0 { + OutputHelpMessage(cmd) + return fmt.Errorf("no arguments. wsh wavepath requires a type argument (config, data, or log)") + } + if len(args) > 1 { + OutputHelpMessage(cmd) + return fmt.Errorf("too many arguments. wsh wavepath requires exactly one argument") + } + + pathType := args[0] + if pathType != "config" && pathType != "data" && pathType != "log" { + OutputHelpMessage(cmd) + return fmt.Errorf("invalid path type %q. must be one of: config, data, log", pathType) + } + + tail, _ := cmd.Flags().GetBool("tail") + if tail && pathType != "log" { + return fmt.Errorf("--tail can only be used with the log path type") + } + + open, _ := cmd.Flags().GetBool("open") + openExternal, _ := cmd.Flags().GetBool("open-external") + + path, err := wshclient.PathCommand(RpcClient, wshrpc.PathCommandData{ + PathType: pathType, + Open: open, + OpenExternal: openExternal, + }, nil) + if err != nil { + return fmt.Errorf("getting path: %w", err) + } + + if tail && pathType == "log" { + err = tailLogFile(path) + if err != nil { + return fmt.Errorf("tailing log file: %w", err) + } + return nil + } + + WriteStdout("%s\n", path) + return nil +} + +func tailLogFile(path string) error { + file, err := os.Open(path) + if err != nil { + return fmt.Errorf("opening log file: %w", err) + } + defer file.Close() + + // Get file size + stat, err := file.Stat() + if err != nil { + return fmt.Errorf("getting file stats: %w", err) + } + + // Read last 16KB or whole file if smaller + readSize := int64(16 * 1024) + var offset int64 + if stat.Size() > readSize { + offset = stat.Size() - readSize + } + + _, err = file.Seek(offset, 0) + if err != nil { + return fmt.Errorf("seeking file: %w", err) + } + + buf := make([]byte, readSize) + n, err := file.Read(buf) + if err != nil && err != io.EOF { + return fmt.Errorf("reading file: %w", err) + } + buf = buf[:n] + + // Skip partial line at start if we're not at beginning of file + if offset > 0 { + idx := bytes.IndexByte(buf, '\n') + if idx >= 0 { + buf = buf[idx+1:] + } + } + + // Split into lines + lines := bytes.Split(buf, []byte{'\n'}) + + // Take last 100 lines if we have more + startIdx := 0 + if len(lines) > 100 { + startIdx = len(lines) - 100 + } + + // Print lines + for _, line := range lines[startIdx:] { + WriteStdout("%s\n", string(line)) + } + + return nil +} diff --git a/docs/docs/wsh-reference.mdx b/docs/docs/wsh-reference.mdx index b5c095ed5..2931c4683 100644 --- a/docs/docs/wsh-reference.mdx +++ b/docs/docs/wsh-reference.mdx @@ -693,4 +693,52 @@ wsh setvar -b client MYVAR=value Variables set with these commands persist across sessions and can be used to store configuration values, secrets, or any other string data that needs to be accessible across blocks or tabs. +## wavepath + +The `wavepath` command lets you get the paths to various Wave Terminal directories and files, including configuration, data storage, and logs. + +```bash +wsh wavepath {config|data|log} +``` + +This command returns the full path to the requested Wave Terminal system directory or file. It's useful for accessing Wave's configuration files, data storage, or checking logs. + +Flags: + +- `-o, --open` - open the path in a new block +- `-O, --open-external` - open the path in the default external application +- `-t, --tail` - show the last ~100 lines of the log file (only valid for log path) + +Examples: + +```bash +# Get path to config directory +wsh wavepath config + +# Get path to data directory +wsh wavepath data + +# Get path to log file +wsh wavepath log + +# Open log file in a new block +wsh wavepath -o log + +# Open config directory in system file explorer +wsh wavepath -O config + +# View recent log entries +wsh wavepath -t log +``` + +The command will show you the full path to: + +- `config` - Where Wave Terminal stores its configuration files +- `data` - Where Wave Terminal stores its persistent data +- `log` - The main Wave Terminal log file + +:::tip +Use the `-t` flag with the log path to quickly view recent log entries without having to open the full file. This is particularly useful for troubleshooting. +::: + diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index a5e75774d..846091596 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -202,6 +202,11 @@ class RpcApiType { return client.wshRpcCall("notify", data, opts); } + // command "path" [call] + PathCommand(client: WshClient, data: PathCommandData, opts?: RpcOpts): Promise { + return client.wshRpcCall("path", data, opts); + } + // command "remotefiledelete" [call] RemoteFileDeleteCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { return client.wshRpcCall("remotefiledelete", data, opts); diff --git a/frontend/layout/lib/layoutModel.ts b/frontend/layout/lib/layoutModel.ts index 1a70fc22d..7f8479c0a 100644 --- a/frontend/layout/lib/layoutModel.ts +++ b/frontend/layout/lib/layoutModel.ts @@ -412,6 +412,11 @@ export class LayoutModel { for (const action of actions) { switch (action.actiontype) { case LayoutTreeActionType.InsertNode: { + if (action.ephemeral) { + this.newEphemeralNode(action.blockid); + break; + } + const insertNodeAction: LayoutTreeInsertNodeAction = { type: LayoutTreeActionType.InsertNode, node: newLayoutNode(undefined, undefined, undefined, { diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index afd817690..895494bda 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -139,6 +139,7 @@ declare global { blockdef: BlockDef; rtopts?: RuntimeOpts; magnified?: boolean; + ephemeral?: boolean; }; // wshrpc.CommandCreateSubBlockData @@ -400,6 +401,7 @@ declare global { indexarr?: number[]; focused: boolean; magnified: boolean; + ephemeral: boolean; }; // waveobj.LayoutState @@ -562,6 +564,14 @@ declare global { prompt: OpenAIPromptMessageType[]; }; + // wshrpc.PathCommandData + type PathCommandData = { + pathtype: string; + open: boolean; + openexternal: boolean; + tabid: string; + }; + // waveobj.Point type Point = { x: number; diff --git a/go.mod b/go.mod index c713a76c6..772c34c62 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/sawka/txwrap v0.2.0 github.com/shirou/gopsutil/v4 v4.24.11 github.com/skeema/knownhosts v1.3.0 + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/spf13/cobra v1.8.1 github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b github.com/wavetermdev/htmltoken v0.2.0 diff --git a/go.sum b/go.sum index b59c00510..8429144ec 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/pkg/waveobj/wtype.go b/pkg/waveobj/wtype.go index 9fa4bb3d1..af775e019 100644 --- a/pkg/waveobj/wtype.go +++ b/pkg/waveobj/wtype.go @@ -208,6 +208,7 @@ type LayoutActionData struct { IndexArr *[]int `json:"indexarr,omitempty"` Focused bool `json:"focused"` Magnified bool `json:"magnified"` + Ephemeral bool `json:"ephemeral"` } type LeafOrderEntry struct { diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index d8aa04748..3dce286c4 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -247,6 +247,12 @@ func NotifyCommand(w *wshutil.WshRpc, data wshrpc.WaveNotificationOptions, opts return err } +// command "path", wshserver.PathCommand +func PathCommand(w *wshutil.WshRpc, data wshrpc.PathCommandData, opts *wshrpc.RpcOpts) (string, error) { + resp, err := sendRpcRequestCallHelper[string](w, "path", data, opts) + return resp, err +} + // command "remotefiledelete", wshserver.RemoteFileDeleteCommand func RemoteFileDeleteCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "remotefiledelete", data, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index ed5aaa7bb..71806f6b0 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -149,6 +149,7 @@ type WshRpcInterface interface { ActivityCommand(ctx context.Context, data ActivityUpdate) error GetVarCommand(ctx context.Context, data CommandVarData) (*CommandVarResponseData, error) SetVarCommand(ctx context.Context, data CommandVarData) error + PathCommand(ctx context.Context, data PathCommandData) (string, error) // connection functions ConnStatusCommand(ctx context.Context) ([]ConnStatus, error) @@ -290,6 +291,7 @@ type CommandCreateBlockData struct { BlockDef *waveobj.BlockDef `json:"blockdef"` RtOpts *waveobj.RuntimeOpts `json:"rtopts,omitempty"` Magnified bool `json:"magnified,omitempty"` + Ephemeral bool `json:"ephemeral,omitempty"` } type CommandCreateSubBlockData struct { @@ -604,6 +606,13 @@ type CommandVarResponseData struct { Exists bool `json:"exists"` } +type PathCommandData struct { + PathType string `json:"pathtype"` + Open bool `json:"open"` + OpenExternal bool `json:"openexternal"` + TabId string `json:"tabid" wshcontext:"TabId"` +} + type ActivityDisplayType struct { Width int `json:"width"` Height int `json:"height"` diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 9e0cd1702..b4e2acfec 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -12,10 +12,12 @@ import ( "fmt" "io/fs" "log" + "path/filepath" "regexp" "strings" "time" + "github.com/skratchdot/open-golang/open" "github.com/wavetermdev/waveterm/pkg/blockcontroller" "github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/panichandler" @@ -183,6 +185,7 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command ActionType: wcore.LayoutActionDataType_Insert, BlockId: blockData.OID, Magnified: data.Magnified, + Ephemeral: data.Ephemeral, Focused: true, }) if err != nil { @@ -829,3 +832,39 @@ func (ws *WshServer) SetVarCommand(ctx context.Context, data wshrpc.CommandVarDa envStr := envutil.MapToEnv(envMap) return filestore.WFS.WriteFile(ctx, data.ZoneId, data.FileName, []byte(envStr)) } + +func (ws *WshServer) PathCommand(ctx context.Context, data wshrpc.PathCommandData) (string, error) { + pathType := data.PathType + openInternal := data.Open + openExternal := data.OpenExternal + var path string + switch pathType { + case "config": + path = wavebase.GetWaveConfigDir() + case "data": + path = wavebase.GetWaveDataDir() + case "log": + path = filepath.Join(wavebase.GetWaveDataDir(), "waveapp.log") + } + + if openInternal && openExternal { + return "", fmt.Errorf("open and openExternal cannot both be true") + } + + if openInternal { + _, err := ws.CreateBlockCommand(ctx, wshrpc.CommandCreateBlockData{BlockDef: &waveobj.BlockDef{Meta: map[string]any{ + waveobj.MetaKey_View: "preview", + waveobj.MetaKey_File: path, + }}, Ephemeral: true, TabId: data.TabId}) + + if err != nil { + return path, fmt.Errorf("error opening path: %w", err) + } + } else if openExternal { + err := open.Run(path) + if err != nil { + return path, fmt.Errorf("error opening path: %w", err) + } + } + return path, nil +}