mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-17 20:51:55 +01:00
merge main
This commit is contained in:
commit
7782d6e59f
@ -4,7 +4,7 @@ root = true
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.{js,jsx,ts,tsx,cjs,json,yml,yaml,css,less}]
|
[*.{js,jsx,ts,tsx,cjs,json,yml,yaml,css,less,scss}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
@ -19,7 +19,6 @@ import (
|
|||||||
var aiCmd = &cobra.Command{
|
var aiCmd = &cobra.Command{
|
||||||
Use: "ai [-] [message...]",
|
Use: "ai [-] [message...]",
|
||||||
Short: "Send a message to an AI block",
|
Short: "Send a message to an AI block",
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
RunE: aiRun,
|
RunE: aiRun,
|
||||||
PreRunE: preRunSetupRpcClient,
|
PreRunE: preRunSetupRpcClient,
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
@ -53,6 +52,11 @@ func aiRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
|||||||
sendActivity("ai", rtnErr == nil)
|
sendActivity("ai", rtnErr == nil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
OutputHelpMessage(cmd)
|
||||||
|
return fmt.Errorf("no message provided")
|
||||||
|
}
|
||||||
|
|
||||||
var stdinUsed bool
|
var stdinUsed bool
|
||||||
var message strings.Builder
|
var message strings.Builder
|
||||||
|
|
||||||
|
@ -21,13 +21,12 @@ var editMagnified bool
|
|||||||
var editorCmd = &cobra.Command{
|
var editorCmd = &cobra.Command{
|
||||||
Use: "editor",
|
Use: "editor",
|
||||||
Short: "edit a file (blocks until editor is closed)",
|
Short: "edit a file (blocks until editor is closed)",
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: editorRun,
|
RunE: editorRun,
|
||||||
PreRunE: preRunSetupRpcClient,
|
PreRunE: preRunSetupRpcClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
editCmd.Flags().BoolVarP(&editMagnified, "magnified", "m", false, "open view in magnified mode")
|
editorCmd.Flags().BoolVarP(&editMagnified, "magnified", "m", false, "open view in magnified mode")
|
||||||
rootCmd.AddCommand(editorCmd)
|
rootCmd.AddCommand(editorCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +34,14 @@ func editorRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
sendActivity("editor", rtnErr == nil)
|
sendActivity("editor", rtnErr == nil)
|
||||||
}()
|
}()
|
||||||
|
if len(args) == 0 {
|
||||||
|
OutputHelpMessage(cmd)
|
||||||
|
return fmt.Errorf("no arguments. wsh editor requires a file or URL as an argument argument")
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
OutputHelpMessage(cmd)
|
||||||
|
return fmt.Errorf("too many arguments. wsh editor requires exactly one argument")
|
||||||
|
}
|
||||||
fileArg := args[0]
|
fileArg := args[0]
|
||||||
absFile, err := filepath.Abs(fileArg)
|
absFile, err := filepath.Abs(fileArg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -82,6 +82,7 @@ func getVarRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
// Single variable case - existing logic
|
// Single variable case - existing logic
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
|
OutputHelpMessage(cmd)
|
||||||
return fmt.Errorf("requires a key argument")
|
return fmt.Errorf("requires a key argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
@ -27,26 +26,58 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var WrappedStdin io.Reader = os.Stdin
|
var WrappedStdin io.Reader = os.Stdin
|
||||||
|
var WrappedStdout io.Writer = &WrappedWriter{dest: os.Stdout}
|
||||||
|
var WrappedStderr io.Writer = &WrappedWriter{dest: os.Stderr}
|
||||||
var RpcClient *wshutil.WshRpc
|
var RpcClient *wshutil.WshRpc
|
||||||
var RpcContext wshrpc.RpcContext
|
var RpcContext wshrpc.RpcContext
|
||||||
var UsingTermWshMode bool
|
var UsingTermWshMode bool
|
||||||
var blockArg string
|
var blockArg string
|
||||||
var WshExitCode int
|
var WshExitCode int
|
||||||
|
|
||||||
func WriteStderr(fmtStr string, args ...interface{}) {
|
type WrappedWriter struct {
|
||||||
output := fmt.Sprintf(fmtStr, args...)
|
dest io.Writer
|
||||||
if UsingTermWshMode {
|
}
|
||||||
output = strings.ReplaceAll(output, "\n", "\r\n")
|
|
||||||
|
func (w *WrappedWriter) Write(p []byte) (n int, err error) {
|
||||||
|
if !UsingTermWshMode {
|
||||||
|
return w.dest.Write(p)
|
||||||
}
|
}
|
||||||
fmt.Fprint(os.Stderr, output)
|
count := 0
|
||||||
|
for _, b := range p {
|
||||||
|
if b == '\n' {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return w.dest.Write(p)
|
||||||
|
}
|
||||||
|
buf := make([]byte, len(p)+count) // Each '\n' adds one extra byte for '\r'
|
||||||
|
writeIdx := 0
|
||||||
|
for _, b := range p {
|
||||||
|
if b == '\n' {
|
||||||
|
buf[writeIdx] = '\r'
|
||||||
|
buf[writeIdx+1] = '\n'
|
||||||
|
writeIdx += 2
|
||||||
|
} else {
|
||||||
|
buf[writeIdx] = b
|
||||||
|
writeIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w.dest.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteStderr(fmtStr string, args ...interface{}) {
|
||||||
|
WrappedStderr.Write([]byte(fmt.Sprintf(fmtStr, args...)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteStdout(fmtStr string, args ...interface{}) {
|
func WriteStdout(fmtStr string, args ...interface{}) {
|
||||||
output := fmt.Sprintf(fmtStr, args...)
|
WrappedStdout.Write([]byte(fmt.Sprintf(fmtStr, args...)))
|
||||||
if UsingTermWshMode {
|
}
|
||||||
output = strings.ReplaceAll(output, "\n", "\r\n")
|
|
||||||
}
|
func OutputHelpMessage(cmd *cobra.Command) {
|
||||||
fmt.Print(output)
|
cmd.SetOutput(WrappedStderr)
|
||||||
|
cmd.Help()
|
||||||
|
WriteStderr("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func preRunSetupRpcClient(cmd *cobra.Command, args []string) error {
|
func preRunSetupRpcClient(cmd *cobra.Command, args []string) error {
|
||||||
@ -64,6 +95,15 @@ func getIsTty() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getThisBlockMeta() (waveobj.MetaMapType, error) {
|
||||||
|
blockORef := waveobj.ORef{OType: waveobj.OType_Block, OID: RpcContext.BlockId}
|
||||||
|
resp, err := wshclient.GetMetaCommand(RpcClient, wshrpc.CommandGetMetaData{ORef: blockORef}, &wshrpc.RpcOpts{Timeout: 2000})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting metadata: %w", err)
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
type RunEFnType = func(*cobra.Command, []string) error
|
type RunEFnType = func(*cobra.Command, []string) error
|
||||||
|
|
||||||
func activityWrap(activityStr string, origRunE RunEFnType) RunEFnType {
|
func activityWrap(activityStr string, origRunE RunEFnType) RunEFnType {
|
||||||
|
154
cmd/wsh/cmd/wshcmd-run.go
Normal file
154
cmd/wsh/cmd/wshcmd-run.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/util/envutil"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
var runCmd = &cobra.Command{
|
||||||
|
Use: "run [flags] -- command [args...]",
|
||||||
|
Short: "run a command in a new block",
|
||||||
|
RunE: runRun,
|
||||||
|
PreRunE: preRunSetupRpcClient,
|
||||||
|
TraverseChildren: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flags := runCmd.Flags()
|
||||||
|
flags.BoolP("magnified", "m", false, "open view in magnified mode")
|
||||||
|
flags.StringP("command", "c", "", "run command string in shell")
|
||||||
|
flags.BoolP("exit", "x", false, "close block if command exits successfully (will stay open if there was an error)")
|
||||||
|
flags.BoolP("forceexit", "X", false, "close block when command exits, regardless of exit status")
|
||||||
|
flags.IntP("delay", "", 2000, "if -x, delay in milliseconds before closing block")
|
||||||
|
flags.BoolP("paused", "p", false, "create block in paused state")
|
||||||
|
flags.String("cwd", "", "set working directory for command")
|
||||||
|
flags.BoolP("append", "a", false, "append output on restart instead of clearing")
|
||||||
|
rootCmd.AddCommand(runCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||||
|
defer func() {
|
||||||
|
sendActivity("run", rtnErr == nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
magnified, _ := flags.GetBool("magnified")
|
||||||
|
commandArg, _ := flags.GetString("command")
|
||||||
|
exit, _ := flags.GetBool("exit")
|
||||||
|
forceExit, _ := flags.GetBool("forceexit")
|
||||||
|
paused, _ := flags.GetBool("paused")
|
||||||
|
cwd, _ := flags.GetString("cwd")
|
||||||
|
delayMs, _ := flags.GetInt("delay")
|
||||||
|
appendOutput, _ := flags.GetBool("append")
|
||||||
|
var cmdArgs []string
|
||||||
|
var useShell bool
|
||||||
|
var shellCmd string
|
||||||
|
|
||||||
|
for i, arg := range os.Args {
|
||||||
|
if arg == "--" {
|
||||||
|
if i+1 >= len(os.Args) {
|
||||||
|
OutputHelpMessage(cmd)
|
||||||
|
return fmt.Errorf("no command provided after --")
|
||||||
|
}
|
||||||
|
shellCmd = os.Args[i+1]
|
||||||
|
cmdArgs = os.Args[i+2:]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shellCmd != "" && commandArg != "" {
|
||||||
|
OutputHelpMessage(cmd)
|
||||||
|
return fmt.Errorf("cannot specify both -c and command arguments")
|
||||||
|
}
|
||||||
|
if shellCmd == "" && commandArg == "" {
|
||||||
|
OutputHelpMessage(cmd)
|
||||||
|
return fmt.Errorf("command must be specified after -- or with -c")
|
||||||
|
}
|
||||||
|
if commandArg != "" {
|
||||||
|
shellCmd = commandArg
|
||||||
|
useShell = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current working directory
|
||||||
|
if cwd == "" {
|
||||||
|
var err error
|
||||||
|
cwd, err = os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting current directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cwd, err := filepath.Abs(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting absolute path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current environment and convert to map
|
||||||
|
envMap := make(map[string]string)
|
||||||
|
for _, envStr := range os.Environ() {
|
||||||
|
env := strings.SplitN(envStr, "=", 2)
|
||||||
|
if len(env) == 2 {
|
||||||
|
envMap[env[0]] = env[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to null-terminated format
|
||||||
|
envContent := envutil.MapToEnv(envMap)
|
||||||
|
createMeta := map[string]any{
|
||||||
|
waveobj.MetaKey_View: "term",
|
||||||
|
waveobj.MetaKey_CmdCwd: cwd,
|
||||||
|
waveobj.MetaKey_Controller: "cmd",
|
||||||
|
waveobj.MetaKey_CmdClearOnStart: true,
|
||||||
|
}
|
||||||
|
createMeta[waveobj.MetaKey_Cmd] = shellCmd
|
||||||
|
createMeta[waveobj.MetaKey_CmdArgs] = cmdArgs
|
||||||
|
createMeta[waveobj.MetaKey_CmdShell] = useShell
|
||||||
|
if paused {
|
||||||
|
createMeta[waveobj.MetaKey_CmdRunOnStart] = false
|
||||||
|
} else {
|
||||||
|
createMeta[waveobj.MetaKey_CmdRunOnce] = true
|
||||||
|
createMeta[waveobj.MetaKey_CmdRunOnStart] = true
|
||||||
|
}
|
||||||
|
if forceExit {
|
||||||
|
createMeta[waveobj.MetaKey_CmdCloseOnExitForce] = true
|
||||||
|
} else if exit {
|
||||||
|
createMeta[waveobj.MetaKey_CmdCloseOnExit] = true
|
||||||
|
}
|
||||||
|
createMeta[waveobj.MetaKey_CmdCloseOnExitDelay] = float64(delayMs)
|
||||||
|
if appendOutput {
|
||||||
|
createMeta[waveobj.MetaKey_CmdClearOnStart] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if RpcContext.Conn != "" {
|
||||||
|
createMeta[waveobj.MetaKey_Connection] = RpcContext.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
createBlockData := wshrpc.CommandCreateBlockData{
|
||||||
|
BlockDef: &waveobj.BlockDef{
|
||||||
|
Meta: createMeta,
|
||||||
|
Files: map[string]*waveobj.FileDef{
|
||||||
|
"env": {
|
||||||
|
Content: envContent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Magnified: magnified,
|
||||||
|
}
|
||||||
|
|
||||||
|
oref, err := wshclient.CreateBlockCommand(RpcClient, createBlockData, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating new run block: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteStdout("run block created: %s\n", oref)
|
||||||
|
return nil
|
||||||
|
}
|
@ -55,18 +55,19 @@ func termRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getting absolute path: %w", err)
|
return fmt.Errorf("getting absolute path: %w", err)
|
||||||
}
|
}
|
||||||
createBlockData := wshrpc.CommandCreateBlockData{
|
createMeta := map[string]any{
|
||||||
BlockDef: &waveobj.BlockDef{
|
waveobj.MetaKey_View: "term",
|
||||||
Meta: map[string]interface{}{
|
waveobj.MetaKey_CmdCwd: cwd,
|
||||||
waveobj.MetaKey_View: "term",
|
waveobj.MetaKey_Controller: "shell",
|
||||||
waveobj.MetaKey_CmdCwd: cwd,
|
|
||||||
waveobj.MetaKey_Controller: "shell",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Magnified: termMagnified,
|
|
||||||
}
|
}
|
||||||
if RpcContext.Conn != "" {
|
if RpcContext.Conn != "" {
|
||||||
createBlockData.BlockDef.Meta[waveobj.MetaKey_Connection] = RpcContext.Conn
|
createMeta[waveobj.MetaKey_Connection] = RpcContext.Conn
|
||||||
|
}
|
||||||
|
createBlockData := wshrpc.CommandCreateBlockData{
|
||||||
|
BlockDef: &waveobj.BlockDef{
|
||||||
|
Meta: createMeta,
|
||||||
|
},
|
||||||
|
Magnified: termMagnified,
|
||||||
}
|
}
|
||||||
oref, err := wshclient.CreateBlockCommand(RpcClient, createBlockData, nil)
|
oref, err := wshclient.CreateBlockCommand(RpcClient, createBlockData, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -20,7 +20,6 @@ var viewMagnified bool
|
|||||||
var viewCmd = &cobra.Command{
|
var viewCmd = &cobra.Command{
|
||||||
Use: "view {file|directory|URL}",
|
Use: "view {file|directory|URL}",
|
||||||
Short: "preview/edit a file or directory",
|
Short: "preview/edit a file or directory",
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: viewRun,
|
RunE: viewRun,
|
||||||
PreRunE: preRunSetupRpcClient,
|
PreRunE: preRunSetupRpcClient,
|
||||||
}
|
}
|
||||||
@ -28,7 +27,6 @@ var viewCmd = &cobra.Command{
|
|||||||
var editCmd = &cobra.Command{
|
var editCmd = &cobra.Command{
|
||||||
Use: "edit {file}",
|
Use: "edit {file}",
|
||||||
Short: "edit a file",
|
Short: "edit a file",
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: viewRun,
|
RunE: viewRun,
|
||||||
PreRunE: preRunSetupRpcClient,
|
PreRunE: preRunSetupRpcClient,
|
||||||
}
|
}
|
||||||
@ -40,9 +38,18 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func viewRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
func viewRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||||
|
cmdName := cmd.Name()
|
||||||
defer func() {
|
defer func() {
|
||||||
sendActivity("view", rtnErr == nil)
|
sendActivity(cmdName, rtnErr == nil)
|
||||||
}()
|
}()
|
||||||
|
if len(args) == 0 {
|
||||||
|
OutputHelpMessage(cmd)
|
||||||
|
return fmt.Errorf("no arguments. wsh %s requires a file or URL as an argument argument", cmdName)
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
OutputHelpMessage(cmd)
|
||||||
|
return fmt.Errorf("too many arguments. wsh %s requires exactly one argument", cmdName)
|
||||||
|
}
|
||||||
fileArg := args[0]
|
fileArg := args[0]
|
||||||
conn := RpcContext.Conn
|
conn := RpcContext.Conn
|
||||||
var wshCmd *wshrpc.CommandCreateBlockData
|
var wshCmd *wshrpc.CommandCreateBlockData
|
||||||
@ -81,7 +88,7 @@ func viewRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
|||||||
},
|
},
|
||||||
Magnified: viewMagnified,
|
Magnified: viewMagnified,
|
||||||
}
|
}
|
||||||
if cmd.Use == "edit" {
|
if cmdName == "edit" {
|
||||||
wshCmd.BlockDef.Meta[waveobj.MetaKey_Edit] = true
|
wshCmd.BlockDef.Meta[waveobj.MetaKey_Edit] = true
|
||||||
}
|
}
|
||||||
if conn != "" {
|
if conn != "" {
|
||||||
|
@ -45,6 +45,7 @@ wsh editconfig
|
|||||||
| term:scrollback | int | size of terminal scrollback buffer, max is 10000 |
|
| term:scrollback | int | size of terminal scrollback buffer, max is 10000 |
|
||||||
| editor:minimapenabled | bool | set to false to disable editor minimap |
|
| editor:minimapenabled | bool | set to false to disable editor minimap |
|
||||||
| editor:stickscrollenabled | bool | |
|
| editor:stickscrollenabled | bool | |
|
||||||
|
| editor:wordwrap | bool | set to true to enable word wrapping in the editor (defaults to false) |
|
||||||
| web:openlinksinternally | bool | set to false to open web links in external browser |
|
| web:openlinksinternally | bool | set to false to open web links in external browser |
|
||||||
| web:defaulturl | string | default web page to open in the web widget when no url is provided (homepage) |
|
| web:defaulturl | string | default web page to open in the web widget when no url is provided (homepage) |
|
||||||
| web:defaultsearch | string | search template for web searches. e.g. `https://www.google.com/search?q={query}`. "\{query}" gets replaced by search term |
|
| web:defaultsearch | string | search template for web searches. e.g. `https://www.google.com/search?q={query}`. "\{query}" gets replaced by search term |
|
||||||
|
@ -90,21 +90,24 @@ A terminal widget, or CLI widget, is a widget that simply opens a terminal and r
|
|||||||
|
|
||||||
The `WidgetConfigType` takes the usual options common to all widgets. The `MetaTSType` can include the keys listed below:
|
The `WidgetConfigType` takes the usual options common to all widgets. The `MetaTSType` can include the keys listed below:
|
||||||
|
|
||||||
| Key | Description |
|
| Key | Description |
|
||||||
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| "view" | A string that specifies the general type of widget. In the case of custom terminal widgets, this must be set to `"term"`. |
|
| "view" | A string that specifies the general type of widget. In the case of custom terminal widgets, this must be set to `"term"`. |
|
||||||
| "controller" | A string that specifies the type of command being used. For more persistent shell sessions, set it to "shell". For one off commands, set it to `"cmd"`. When `"cmd"` is set, the widget has an additional refresh button in its header that allows the command to be re-run. |
|
| "controller" | A string that specifies the type of command being used. For more persistent shell sessions, set it to "shell". For one off commands, set it to `"cmd"`. When `"cmd"` is set, the widget has an additional refresh button in its header that allows the command to be re-run. |
|
||||||
| "cmd" | (optional) When the `"controller"` is set to `"cmd"`, this option provides the actual command to be run. Note that because it is run as a command, there is no shell session unless you are launching a command that contains a shell session itself. Defaults to an empty string. |
|
| "cmd" | (optional) When the `"controller"` is set to `"cmd"`, this option provides the actual command to be run. Note that because it is run as a command, there is no shell session unless you are launching a command that contains a shell session itself. Defaults to an empty string. |
|
||||||
| "cmd:interactive" | (optional) When the `"controller"` is set to `"term", this boolean adds the interactive flag to the launched terminal. Defaults to false. |
|
| "cmd:interactive" | (optional) When the `"controller"` is set to `"term", this boolean adds the interactive flag to the launched terminal. Defaults to false. |
|
||||||
| "cmd:login" | (optional) When the `"controller"` is set to `"term"`, this boolean adds the login flag to the term command. Defaults to false. |
|
| "cmd:login" | (optional) When the `"controller"` is set to `"term"`, this boolean adds the login flag to the term command. Defaults to false. |
|
||||||
| "cmd:runonstart" | (optional) The command will rerun when the app is started. Without it, you must manually run the command. Defaults to true. |
|
| "cmd:runonstart" | (optional) The command will rerun when the app is started. Without it, you must manually run the command. Defaults to true. |
|
||||||
| "cmd:clearonstart" | (optional) When the cmd starts, the contents of the block are cleared out. Defaults to false. |
|
| "cmd:runonce" | (optional) Runs on start, but then sets "cmd:runonce" and "cmd:runonstart" to false (so future runs require manual restarts) |
|
||||||
| "cmd:clearonrestart" | (optional) When the app restarts, the contents of the block are cleared out. Defaults to false. |
|
| "cmd:clearonstart" | (optional) When the cmd runs, the contents of the block are cleared out. Defaults to false. |
|
||||||
| "cmd:env" | (optional) A key-value object represting environment variables to be run with the command. Currently only works locally. Defaults to an empty object. |
|
| "cmd:closeonexit" | (optional) Automatically closes the block if the command successfully exits (exit code = 0) |
|
||||||
| "cmd:cwd" | (optional) A string representing the current working directory to be run with the command. Currently only works locally. Defaults to the home directory. |
|
| "cmd:closeonexitforce" | (optional) Automatically closes the block if when the command exits (success or failure) |
|
||||||
| "cmd:nowsh" | (optional) A boolean that will turn off wsh integration for the command. Defaults to false. |
|
| "cmd:closeonexitdelay | (optional) Change the delay between when the command exits and when the block gets closed, in milliseconds, default 2000 |
|
||||||
| "term:localshellpath" | (optional) Sets the shell used for running your widget command. Only works locally. If left blank, wave will determine your system default instead. |
|
| "cmd:env" | (optional) A key-value object represting environment variables to be run with the command. Currently only works locally. Defaults to an empty object. |
|
||||||
| "term:localshellopts" | (optional) Sets the shell options meant to be used with `"term:localshellpath"`. This is useful if you are using a nonstandard shell and need to provide a specific option that we do not cover. Only works locally. Defaults to an empty string. |
|
| "cmd:cwd" | (optional) A string representing the current working directory to be run with the command. Currently only works locally. Defaults to the home directory. |
|
||||||
|
| "cmd:nowsh" | (optional) A boolean that will turn off wsh integration for the command. Defaults to false. |
|
||||||
|
| "term:localshellpath" | (optional) Sets the shell used for running your widget command. Only works locally. If left blank, wave will determine your system default instead. |
|
||||||
|
| "term:localshellopts" | (optional) Sets the shell options meant to be used with `"term:localshellpath"`. This is useful if you are using a nonstandard shell and need to provide a specific option that we do not cover. Only works locally. Defaults to an empty string. |
|
||||||
|
|
||||||
## Example Terminal Widgets
|
## Example Terminal Widgets
|
||||||
|
|
||||||
|
@ -145,6 +145,75 @@ wsh editconfig widgets.json
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## run
|
||||||
|
|
||||||
|
The `run` command creates a new terminal command block and executes a specified command within it. The command can be provided either as arguments after `--` or using the `-c` flag. Unless the `-x` or `-X` flags are passed, commands can be re-executed by pressing `Enter` once the command has finished running.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run a command specified after --
|
||||||
|
wsh run -- ls -la
|
||||||
|
|
||||||
|
# Run a command using -c flag
|
||||||
|
wsh run -c "ls -la"
|
||||||
|
|
||||||
|
# Run with working directory specified
|
||||||
|
wsh run --cwd /path/to/dir -- ./script.sh
|
||||||
|
|
||||||
|
# Run in magnified mode
|
||||||
|
wsh run -m -- make build
|
||||||
|
|
||||||
|
# Run and auto-close on successful completion
|
||||||
|
wsh run -x -- npm test
|
||||||
|
|
||||||
|
# Run and auto-close regardless of exit status
|
||||||
|
wsh run -X -- ./long-running-task.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The command inherits the current environment variables and working directory by default.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
|
||||||
|
- `-m, --magnified` - open the block in magnified mode
|
||||||
|
- `-c, --command string` - run a command string in _shell_
|
||||||
|
- `-x, --exit` - close block if command exits successfully (stays open if there was an error)
|
||||||
|
- `-X, --forceexit` - close block when command exits, regardless of exit status
|
||||||
|
- `--delay int` - if using -x/-X, delay in milliseconds before closing block (default 2000)
|
||||||
|
- `-p, --paused` - create block in paused state
|
||||||
|
- `-a, --append` - append output on command restart instead of clearing
|
||||||
|
- `--cwd string` - set working directory for command
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run a build command in magnified mode
|
||||||
|
wsh run -m -- npm run build
|
||||||
|
|
||||||
|
# Execute a script and auto-close after success
|
||||||
|
wsh run -x -- ./backup-script.sh
|
||||||
|
|
||||||
|
# Run a command in a specific directory
|
||||||
|
wsh run --cwd ./project -- make test
|
||||||
|
|
||||||
|
# Run a shell command and force close after completion
|
||||||
|
wsh run -X -c "find . -name '*.log' -delete"
|
||||||
|
|
||||||
|
# Start a command in paused state
|
||||||
|
wsh run -p -- ./server --dev
|
||||||
|
|
||||||
|
# Run with custom close delay
|
||||||
|
wsh run -x --delay 5000 -- ./deployment.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
When using the `-x` or `-X` flags, the block will automatically close after the command completes. The `-x` flag only closes on successful completion (exit code 0), while `-X` closes regardless of exit status. The `--delay` flag controls how long to wait before closing (default 2000ms).
|
||||||
|
|
||||||
|
The `-p` flag creates the block in a paused state, allowing you to review the command before execution.
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
You can use either `--` followed by your command and arguments, or the `-c` flag with a quoted command string. The `--` method is preferred when you want to preserve argument handling, while `-c` is useful for shell commands with pipes or redirections.
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## deleteblock
|
## deleteblock
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -36,15 +36,15 @@
|
|||||||
"@docusaurus/module-type-aliases": "3.6.3",
|
"@docusaurus/module-type-aliases": "3.6.3",
|
||||||
"@docusaurus/tsconfig": "3.6.3",
|
"@docusaurus/tsconfig": "3.6.3",
|
||||||
"@docusaurus/types": "3.6.3",
|
"@docusaurus/types": "3.6.3",
|
||||||
"@eslint/js": "^9.15.0",
|
"@eslint/js": "^9.16.0",
|
||||||
"@mdx-js/typescript-plugin": "^0.0.6",
|
"@mdx-js/typescript-plugin": "^0.0.6",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/eslint-config-prettier": "^6.11.3",
|
"@types/eslint-config-prettier": "^6.11.3",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-mdx": "^3.1.5",
|
"eslint-plugin-mdx": "^3.1.5",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-jsdoc": "^1.3.0",
|
"prettier-plugin-jsdoc": "^1.3.0",
|
||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
"prettier-plugin-organize-imports": "^4.1.0",
|
||||||
"remark-cli": "^12.0.1",
|
"remark-cli": "^12.0.1",
|
||||||
@ -53,7 +53,7 @@
|
|||||||
"remark-preset-lint-consistent": "^6.0.0",
|
"remark-preset-lint-consistent": "^6.0.0",
|
||||||
"remark-preset-lint-recommended": "^7.0.0",
|
"remark-preset-lint-recommended": "^7.0.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"typescript-eslint": "^8.16.0"
|
"typescript-eslint": "^8.17.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"path-to-regexp@npm:2.2.1": "^3",
|
"path-to-regexp@npm:2.2.1": "^3",
|
||||||
|
@ -30,13 +30,6 @@ export default defineConfig({
|
|||||||
"process.env.WS_NO_BUFFER_UTIL": "true",
|
"process.env.WS_NO_BUFFER_UTIL": "true",
|
||||||
"process.env.WS_NO_UTF_8_VALIDATE": "true",
|
"process.env.WS_NO_UTF_8_VALIDATE": "true",
|
||||||
},
|
},
|
||||||
css: {
|
|
||||||
preprocessorOptions: {
|
|
||||||
scss: {
|
|
||||||
api: "modern-compiler", // or "modern"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
root: ".",
|
root: ".",
|
||||||
|
@ -294,8 +294,8 @@ export class WaveBrowserWindow extends BaseWindow {
|
|||||||
await this.queueTabSwitch(tabView, tabInitialized);
|
await this.queueTabSwitch(tabView, tabInitialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTab() {
|
async createTab(pinned = false) {
|
||||||
const tabId = await WorkspaceService.CreateTab(this.workspaceId, null, true);
|
const tabId = await WorkspaceService.CreateTab(this.workspaceId, null, true, pinned);
|
||||||
await this.setActiveTab(tabId, false);
|
await this.setActiveTab(tabId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +75,10 @@ a.plain-link {
|
|||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-color {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* OverlayScrollbars styling */
|
/* OverlayScrollbars styling */
|
||||||
.os-scrollbar {
|
.os-scrollbar {
|
||||||
--os-handle-bg: var(--scrollbar-thumb-color);
|
--os-handle-bg: var(--scrollbar-thumb-color);
|
||||||
|
@ -124,6 +124,10 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&.flex-nogrow {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&.preview-filename {
|
&.preview-filename {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
blockViewToName,
|
blockViewToName,
|
||||||
computeConnColorNum,
|
computeConnColorNum,
|
||||||
ConnectionButton,
|
ConnectionButton,
|
||||||
ControllerStatusIcon,
|
|
||||||
getBlockHeaderIcon,
|
getBlockHeaderIcon,
|
||||||
Input,
|
Input,
|
||||||
} from "@/app/block/blockutil";
|
} from "@/app/block/blockutil";
|
||||||
@ -227,7 +226,6 @@ const BlockFrame_Header = ({
|
|||||||
} else if (Array.isArray(headerTextUnion)) {
|
} else if (Array.isArray(headerTextUnion)) {
|
||||||
headerTextElems.push(...renderHeaderElements(headerTextUnion, preview));
|
headerTextElems.push(...renderHeaderElements(headerTextUnion, preview));
|
||||||
}
|
}
|
||||||
headerTextElems.unshift(<ControllerStatusIcon key="connstatus" blockId={nodeModel.blockId} />);
|
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
const copyHeaderErr = () => {
|
const copyHeaderErr = () => {
|
||||||
navigator.clipboard.writeText(error.message + "\n" + error.stack);
|
navigator.clipboard.writeText(error.message + "\n" + error.stack);
|
||||||
@ -271,7 +269,7 @@ const HeaderTextElem = React.memo(({ elem, preview }: { elem: HeaderElem; previe
|
|||||||
return <Input decl={elem} className={clsx("block-frame-input", elem.className)} preview={preview} />;
|
return <Input decl={elem} className={clsx("block-frame-input", elem.className)} preview={preview} />;
|
||||||
} else if (elem.elemtype == "text") {
|
} else if (elem.elemtype == "text") {
|
||||||
return (
|
return (
|
||||||
<div className={clsx("block-frame-text", elem.className)}>
|
<div className={clsx("block-frame-text", elem.className, { "flex-nogrow": elem.noGrow })}>
|
||||||
<span ref={preview ? null : elem.ref} onClick={(e) => elem?.onClick(e)}>
|
<span ref={preview ? null : elem.ref} onClick={(e) => elem?.onClick(e)}>
|
||||||
‎{elem.text}
|
‎{elem.text}
|
||||||
</span>
|
</span>
|
||||||
|
@ -2,10 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { NumActiveConnColors } from "@/app/block/blockframe";
|
import { NumActiveConnColors } from "@/app/block/blockframe";
|
||||||
import { getConnStatusAtom, WOS } from "@/app/store/global";
|
import { getConnStatusAtom } from "@/app/store/global";
|
||||||
import * as services from "@/app/store/services";
|
|
||||||
import { makeORef } from "@/app/store/wos";
|
|
||||||
import { waveEventSubscribe } from "@/store/wps";
|
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
@ -150,52 +147,6 @@ interface ConnectionButtonProps {
|
|||||||
changeConnModalAtom: jotai.PrimitiveAtom<boolean>;
|
changeConnModalAtom: jotai.PrimitiveAtom<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ControllerStatusIcon = React.memo(({ blockId }: { blockId: string }) => {
|
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
|
||||||
const hasController = !util.isBlank(blockData?.meta?.controller);
|
|
||||||
const [controllerStatus, setControllerStatus] = React.useState<BlockControllerRuntimeStatus>(null);
|
|
||||||
const [gotInitialStatus, setGotInitialStatus] = React.useState(false);
|
|
||||||
const connection = blockData?.meta?.connection ?? "local";
|
|
||||||
const connStatusAtom = getConnStatusAtom(connection);
|
|
||||||
const connStatus = jotai.useAtomValue(connStatusAtom);
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!hasController) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const initialRTStatus = services.BlockService.GetControllerStatus(blockId);
|
|
||||||
initialRTStatus.then((rts) => {
|
|
||||||
setGotInitialStatus(true);
|
|
||||||
setControllerStatus(rts);
|
|
||||||
});
|
|
||||||
const unsubFn = waveEventSubscribe({
|
|
||||||
eventType: "controllerstatus",
|
|
||||||
scope: makeORef("block", blockId),
|
|
||||||
handler: (event) => {
|
|
||||||
const cstatus: BlockControllerRuntimeStatus = event.data;
|
|
||||||
setControllerStatus(cstatus);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
unsubFn();
|
|
||||||
};
|
|
||||||
}, [hasController]);
|
|
||||||
if (!hasController || !gotInitialStatus) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (controllerStatus?.shellprocstatus == "running") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (connStatus?.status != "connected") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const controllerStatusElem = (
|
|
||||||
<div className="iconbutton disabled" key="controller-status">
|
|
||||||
<i className="fa-sharp fa-solid fa-triangle-exclamation" title="Shell Process Is Not Running" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return controllerStatusElem;
|
|
||||||
});
|
|
||||||
|
|
||||||
export function computeConnColorNum(connStatus: ConnStatus): number {
|
export function computeConnColorNum(connStatus: ConnStatus): number {
|
||||||
// activeconnnum is 1-indexed, so we need to adjust for when mod is 0
|
// activeconnnum is 1-indexed, so we need to adjust for when mod is 0
|
||||||
const connColorNum = (connStatus?.activeconnnum ?? 1) % NumActiveConnColors;
|
const connColorNum = (connStatus?.activeconnnum ?? 1) % NumActiveConnColors;
|
||||||
|
@ -18,6 +18,10 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.no-action {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.45 !important;
|
opacity: 0.45 !important;
|
||||||
|
@ -9,15 +9,19 @@ import "./iconbutton.scss";
|
|||||||
|
|
||||||
export const IconButton = memo(({ decl, className }: { decl: IconButtonDecl; className?: string }) => {
|
export const IconButton = memo(({ decl, className }: { decl: IconButtonDecl; className?: string }) => {
|
||||||
const buttonRef = useRef<HTMLDivElement>(null);
|
const buttonRef = useRef<HTMLDivElement>(null);
|
||||||
|
const spin = decl.iconSpin ?? false;
|
||||||
useLongClick(buttonRef, decl.click, decl.longClick, decl.disabled);
|
useLongClick(buttonRef, decl.click, decl.longClick, decl.disabled);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className={clsx("iconbutton", className, decl.className, { disabled: decl.disabled })}
|
className={clsx("iconbutton", className, decl.className, {
|
||||||
|
disabled: decl.disabled,
|
||||||
|
"no-action": decl.noAction,
|
||||||
|
})}
|
||||||
title={decl.title}
|
title={decl.title}
|
||||||
style={{ color: decl.iconColor ?? "inherit" }}
|
style={{ color: decl.iconColor ?? "inherit" }}
|
||||||
>
|
>
|
||||||
{typeof decl.icon === "string" ? <i className={makeIconClass(decl.icon, true)} /> : decl.icon}
|
{typeof decl.icon === "string" ? <i className={makeIconClass(decl.icon, true, { spin })} /> : decl.icon}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
.link-button {
|
.link-button {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
.popover-content {
|
.popover-content {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1000; // TODO: put this in theme.scss
|
z-index: 1000; // TODO: put this in theme.scss
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
background: #212121;
|
background: #212121;
|
||||||
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
|
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
@mixin ellipsis(){
|
@mixin ellipsis() {
|
||||||
display: block;
|
display: block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin border-radius-mixin(){
|
@mixin border-radius-mixin() {
|
||||||
&.border-radius-2 {
|
&.border-radius-2 {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin vertical-padding-mixin(){
|
@mixin vertical-padding-mixin() {
|
||||||
&.vertical-padding-0 {
|
&.vertical-padding-0 {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
@ -85,7 +85,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin horizontal-padding-mixin(){
|
@mixin horizontal-padding-mixin() {
|
||||||
&.horizontal-padding-0 {
|
&.horizontal-padding-0 {
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
@ -132,7 +132,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin font-size-mixin(){
|
@mixin font-size-mixin() {
|
||||||
&.font-size-10 {
|
&.font-size-10 {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
@ -186,7 +186,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin font-weight-mixin(){
|
@mixin font-weight-mixin() {
|
||||||
&.font-weight-100 {
|
&.font-weight-100 {
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
@ -210,7 +210,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin avatar-dims-mixin(){
|
@mixin avatar-dims-mixin() {
|
||||||
&.size-xs {
|
&.size-xs {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
@ -2,118 +2,118 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
.notification-title {
|
.notification-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
|
|
||||||
&.green {
|
&.green {
|
||||||
color: var(--success-color);
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.yellow {
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
.notification-message {
|
||||||
color: var(--error-color);
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.yellow {
|
.notification-actions {
|
||||||
color: var(--warning-color);
|
display: flex;
|
||||||
}
|
gap: 10px;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
.notification-message {
|
i {
|
||||||
font-size: 13px;
|
margin-left: 3px;
|
||||||
font-style: normal;
|
font-size: 11px;
|
||||||
font-weight: 400;
|
}
|
||||||
line-height: 18px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
i {
|
|
||||||
margin-left: 3px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lock-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
right: 5px;
|
|
||||||
padding: 10px 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: rgb(from var(--main-text-color) r g b / 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification {
|
|
||||||
width: 100%;
|
|
||||||
color: var(--main-text-color);
|
|
||||||
padding: 12px 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-bubble {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
width: 380px;
|
|
||||||
padding: 16px 24px 16px 16px;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 0.5px solid rgba(255, 255, 255, 0.12);
|
|
||||||
background: #232323;
|
|
||||||
box-shadow: 0px 8px 32px 0px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-inner {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
column-gap: 6px;
|
|
||||||
|
|
||||||
.notification-icon {
|
|
||||||
margin-right: 5px;
|
|
||||||
margin-top: 1px;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
i.green {
|
|
||||||
color: var(--success-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
i.red {
|
|
||||||
color: var(--error-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
i.yellow {
|
|
||||||
color: var(--warning-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-timestamp {
|
.close-btn {
|
||||||
font-size: 12px;
|
position: absolute;
|
||||||
font-weight: 400;
|
top: 5px;
|
||||||
line-height: 18px;
|
right: 5px;
|
||||||
opacity: 0.5;
|
}
|
||||||
margin-bottom: 7px;
|
|
||||||
|
.lock-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
padding: 10px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgb(from var(--main-text-color) r g b / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
width: 100%;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
padding: 12px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bubble {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 380px;
|
||||||
|
padding: 16px 24px 16px 16px;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 0.5px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background: #232323;
|
||||||
|
box-shadow: 0px 8px 32px 0px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
column-gap: 6px;
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-top: 1px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.green {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.red {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.yellow {
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-timestamp {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.hovered {
|
||||||
|
background: #292929;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
&.hovered {
|
|
||||||
background: #292929;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -627,6 +627,7 @@ function createTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setActiveTab(tabId: string) {
|
function setActiveTab(tabId: string) {
|
||||||
|
// We use this hack to prevent a flicker in the tab bar when switching to a new tab. This class is unset in reinitWave in wave.ts. See tab.scss for where this class is used.
|
||||||
document.body.classList.add("nohover");
|
document.body.classList.add("nohover");
|
||||||
getApi().setActiveTab(tabId);
|
getApi().setActiveTab(tabId);
|
||||||
}
|
}
|
||||||
|
@ -99,15 +99,19 @@ function switchBlockInDirection(tabId: string, direction: NavigateDirection) {
|
|||||||
layoutModel.switchNodeFocusInDirection(direction);
|
layoutModel.switchNodeFocusInDirection(direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllTabs(ws: Workspace): string[] {
|
||||||
|
return [...(ws.pinnedtabids ?? []), ...(ws.tabids ?? [])];
|
||||||
|
}
|
||||||
|
|
||||||
function switchTabAbs(index: number) {
|
function switchTabAbs(index: number) {
|
||||||
console.log("switchTabAbs", index);
|
console.log("switchTabAbs", index);
|
||||||
const ws = globalStore.get(atoms.workspace);
|
const ws = globalStore.get(atoms.workspace);
|
||||||
const waveWindow = globalStore.get(atoms.waveWindow);
|
|
||||||
const newTabIdx = index - 1;
|
const newTabIdx = index - 1;
|
||||||
if (newTabIdx < 0 || newTabIdx >= ws.tabids.length) {
|
const tabids = getAllTabs(ws);
|
||||||
|
if (newTabIdx < 0 || newTabIdx >= tabids.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newActiveTabId = ws.tabids[newTabIdx];
|
const newActiveTabId = tabids[newTabIdx];
|
||||||
getApi().setActiveTab(newActiveTabId);
|
getApi().setActiveTab(newActiveTabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,8 +120,9 @@ function switchTab(offset: number) {
|
|||||||
const ws = globalStore.get(atoms.workspace);
|
const ws = globalStore.get(atoms.workspace);
|
||||||
const curTabId = globalStore.get(atoms.staticTabId);
|
const curTabId = globalStore.get(atoms.staticTabId);
|
||||||
let tabIdx = -1;
|
let tabIdx = -1;
|
||||||
for (let i = 0; i < ws.tabids.length; i++) {
|
const tabids = getAllTabs(ws);
|
||||||
if (ws.tabids[i] == curTabId) {
|
for (let i = 0; i < tabids.length; i++) {
|
||||||
|
if (tabids[i] == curTabId) {
|
||||||
tabIdx = i;
|
tabIdx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -125,8 +130,8 @@ function switchTab(offset: number) {
|
|||||||
if (tabIdx == -1) {
|
if (tabIdx == -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newTabIdx = (tabIdx + offset + ws.tabids.length) % ws.tabids.length;
|
const newTabIdx = (tabIdx + offset + tabids.length) % tabids.length;
|
||||||
const newActiveTabId = ws.tabids[newTabIdx];
|
const newActiveTabId = tabids[newTabIdx];
|
||||||
getApi().setActiveTab(newActiveTabId);
|
getApi().setActiveTab(newActiveTabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +246,10 @@ function registerGlobalKeys() {
|
|||||||
});
|
});
|
||||||
globalKeyMap.set("Cmd:w", () => {
|
globalKeyMap.set("Cmd:w", () => {
|
||||||
const tabId = globalStore.get(atoms.staticTabId);
|
const tabId = globalStore.get(atoms.staticTabId);
|
||||||
genericClose(tabId);
|
const ws = globalStore.get(atoms.workspace);
|
||||||
|
if (!ws.pinnedtabids?.includes(tabId)) {
|
||||||
|
genericClose(tabId);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
globalKeyMap.set("Cmd:m", () => {
|
globalKeyMap.set("Cmd:m", () => {
|
||||||
|
@ -168,13 +168,18 @@ export const WindowService = new WindowServiceType();
|
|||||||
|
|
||||||
// workspaceservice.WorkspaceService (workspace)
|
// workspaceservice.WorkspaceService (workspace)
|
||||||
class WorkspaceServiceType {
|
class WorkspaceServiceType {
|
||||||
|
// @returns object updates
|
||||||
|
ChangeTabPinning(workspaceId: string, tabId: string, pinned: boolean): Promise<void> {
|
||||||
|
return WOS.callBackendService("workspace", "ChangeTabPinning", Array.from(arguments))
|
||||||
|
}
|
||||||
|
|
||||||
// @returns object updates
|
// @returns object updates
|
||||||
CloseTab(workspaceId: string, tabId: string, fromElectron: boolean): Promise<CloseTabRtnType> {
|
CloseTab(workspaceId: string, tabId: string, fromElectron: boolean): Promise<CloseTabRtnType> {
|
||||||
return WOS.callBackendService("workspace", "CloseTab", Array.from(arguments))
|
return WOS.callBackendService("workspace", "CloseTab", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
|
||||||
// @returns tabId (and object updates)
|
// @returns tabId (and object updates)
|
||||||
CreateTab(workspaceId: string, tabName: string, activateTab: boolean): Promise<string> {
|
CreateTab(workspaceId: string, tabName: string, activateTab: boolean, pinned: boolean): Promise<string> {
|
||||||
return WOS.callBackendService("workspace", "CreateTab", Array.from(arguments))
|
return WOS.callBackendService("workspace", "CreateTab", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +200,7 @@ class WorkspaceServiceType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @returns object updates
|
// @returns object updates
|
||||||
UpdateTabIds(workspaceId: string, tabIds: string[]): Promise<void> {
|
UpdateTabIds(workspaceId: string, tabIds: string[], pinnedTabIds: string[]): Promise<void> {
|
||||||
return WOS.callBackendService("workspace", "UpdateTabIds", Array.from(arguments))
|
return WOS.callBackendService("workspace", "UpdateTabIds", Array.from(arguments))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,45 +79,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.button {
|
||||||
visibility: hidden;
|
position: absolute;
|
||||||
position: absolute;
|
top: 50%;
|
||||||
top: 50%;
|
right: 4px;
|
||||||
right: 4px;
|
transform: translate3d(0, -50%, 0);
|
||||||
transform: translate3d(0, -50%, 0);
|
width: 20px;
|
||||||
width: 20px;
|
height: 20px;
|
||||||
height: 20px;
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
justify-content: center;
|
cursor: pointer;
|
||||||
cursor: pointer;
|
z-index: var(--zindex-tab-name);
|
||||||
z-index: var(--zindex-tab-name);
|
padding: 1px 2px;
|
||||||
padding: 1px 2px;
|
transition: none !important;
|
||||||
transition: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .close {
|
.close {
|
||||||
visibility: visible;
|
visibility: hidden;
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--main-text-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body:not(.nohover) .tab:hover {
|
body:not(.nohover) .tab:hover {
|
||||||
.tab-inner {
|
& + .tab::after,
|
||||||
border-color: transparent;
|
&::after {
|
||||||
background: rgb(from var(--main-text-color) r g b / 0.07);
|
content: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.tab-inner {
|
||||||
visibility: visible;
|
border-color: transparent;
|
||||||
&:hover {
|
background: rgb(from var(--main-text-color) r g b / 0.07);
|
||||||
color: var(--main-text-color);
|
}
|
||||||
}
|
.close {
|
||||||
}
|
visibility: visible;
|
||||||
|
&:hover {
|
||||||
|
color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes expandWidthAndFadeIn {
|
@keyframes expandWidthAndFadeIn {
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
import { Button } from "@/element/button";
|
import { Button } from "@/element/button";
|
||||||
import { ContextMenuModel } from "@/store/contextmenu";
|
import { ContextMenuModel } from "@/store/contextmenu";
|
||||||
import * as services from "@/store/services";
|
|
||||||
import * as WOS from "@/store/wos";
|
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
|
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||||
@ -12,6 +10,8 @@ import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef,
|
|||||||
import { atoms, globalStore, refocusNode } from "@/app/store/global";
|
import { atoms, globalStore, refocusNode } from "@/app/store/global";
|
||||||
import { RpcApi } from "@/app/store/wshclientapi";
|
import { RpcApi } from "@/app/store/wshclientapi";
|
||||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||||
|
import { ObjectService } from "../store/services";
|
||||||
|
import { makeORef, useWaveObjectValue } from "../store/wos";
|
||||||
import "./tab.scss";
|
import "./tab.scss";
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
@ -22,11 +22,13 @@ interface TabProps {
|
|||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
tabWidth: number;
|
tabWidth: number;
|
||||||
isNew: boolean;
|
isNew: boolean;
|
||||||
|
isPinned: boolean;
|
||||||
tabIds: string[];
|
tabIds: string[];
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null) => void;
|
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null) => void;
|
||||||
onMouseDown: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onMouseDown: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
onLoaded: () => void;
|
onLoaded: () => void;
|
||||||
|
onPinChange: () => void;
|
||||||
onMouseEnter: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onMouseEnter: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
onMouseLeave: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onMouseLeave: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
@ -38,6 +40,7 @@ const Tab = memo(
|
|||||||
id,
|
id,
|
||||||
isActive,
|
isActive,
|
||||||
isFirst,
|
isFirst,
|
||||||
|
isPinned,
|
||||||
isBeforeActive,
|
isBeforeActive,
|
||||||
isDragging,
|
isDragging,
|
||||||
tabWidth,
|
tabWidth,
|
||||||
@ -49,10 +52,11 @@ const Tab = memo(
|
|||||||
onMouseDown,
|
onMouseDown,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
|
onPinChange,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
|
const [tabData, _] = useWaveObjectValue<Tab>(makeORef("tab", id));
|
||||||
const [originalName, setOriginalName] = useState("");
|
const [originalName, setOriginalName] = useState("");
|
||||||
const [isEditable, setIsEditable] = useState(false);
|
const [isEditable, setIsEditable] = useState(false);
|
||||||
|
|
||||||
@ -95,7 +99,7 @@ const Tab = memo(
|
|||||||
newText = newText || originalName;
|
newText = newText || originalName;
|
||||||
editableRef.current.innerText = newText;
|
editableRef.current.innerText = newText;
|
||||||
setIsEditable(false);
|
setIsEditable(false);
|
||||||
services.ObjectService.UpdateTabName(id, newText);
|
ObjectService.UpdateTabName(id, newText);
|
||||||
setTimeout(() => refocusNode(null), 10);
|
setTimeout(() => refocusNode(null), 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -153,7 +157,12 @@ const Tab = memo(
|
|||||||
|
|
||||||
function handleContextMenu(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
function handleContextMenu(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let menu: ContextMenuItem[] = [];
|
let menu: ContextMenuItem[] = [
|
||||||
|
{ label: isPinned ? "Unpin Tab" : "Pin Tab", click: onPinChange },
|
||||||
|
{ label: "Rename Tab", click: () => handleRenameTab(null) },
|
||||||
|
{ label: "Copy TabId", click: () => navigator.clipboard.writeText(id) },
|
||||||
|
{ type: "separator" },
|
||||||
|
];
|
||||||
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
||||||
const bgPresets: string[] = [];
|
const bgPresets: string[] = [];
|
||||||
for (const key in fullConfig?.presets ?? {}) {
|
for (const key in fullConfig?.presets ?? {}) {
|
||||||
@ -166,12 +175,9 @@ const Tab = memo(
|
|||||||
const bOrder = fullConfig.presets[b]["display:order"] ?? 0;
|
const bOrder = fullConfig.presets[b]["display:order"] ?? 0;
|
||||||
return aOrder - bOrder;
|
return aOrder - bOrder;
|
||||||
});
|
});
|
||||||
menu.push({ label: "Rename Tab", click: () => handleRenameTab(null) });
|
|
||||||
menu.push({ label: "Copy TabId", click: () => navigator.clipboard.writeText(id) });
|
|
||||||
menu.push({ type: "separator" });
|
|
||||||
if (bgPresets.length > 0) {
|
if (bgPresets.length > 0) {
|
||||||
const submenu: ContextMenuItem[] = [];
|
const submenu: ContextMenuItem[] = [];
|
||||||
const oref = WOS.makeORef("tab", id);
|
const oref = makeORef("tab", id);
|
||||||
for (const presetName of bgPresets) {
|
for (const presetName of bgPresets) {
|
||||||
const preset = fullConfig.presets[presetName];
|
const preset = fullConfig.presets[presetName];
|
||||||
if (preset == null) {
|
if (preset == null) {
|
||||||
@ -180,13 +186,12 @@ const Tab = memo(
|
|||||||
submenu.push({
|
submenu.push({
|
||||||
label: preset["display:name"] ?? presetName,
|
label: preset["display:name"] ?? presetName,
|
||||||
click: () => {
|
click: () => {
|
||||||
services.ObjectService.UpdateObjectMeta(oref, preset);
|
ObjectService.UpdateObjectMeta(oref, preset);
|
||||||
RpcApi.ActivityCommand(TabRpcClient, { settabtheme: 1 });
|
RpcApi.ActivityCommand(TabRpcClient, { settabtheme: 1 });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
menu.push({ label: "Backgrounds", type: "submenu", submenu });
|
menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" });
|
||||||
menu.push({ type: "separator" });
|
|
||||||
}
|
}
|
||||||
menu.push({ label: "Close Tab", click: () => onClose(null) });
|
menu.push({ label: "Close Tab", click: () => onClose(null) });
|
||||||
ContextMenuModel.showContextMenu(menu, e);
|
ContextMenuModel.showContextMenu(menu, e);
|
||||||
@ -233,9 +238,21 @@ const Tab = memo(
|
|||||||
{tabData?.name}
|
{tabData?.name}
|
||||||
{/* {id.substring(id.length - 3)} */}
|
{/* {id.substring(id.length - 3)} */}
|
||||||
</div>
|
</div>
|
||||||
<Button className="ghost grey close" onClick={onClose} onMouseDown={handleMouseDownOnClose}>
|
{isPinned ? (
|
||||||
<i className="fa fa-solid fa-xmark" />
|
<Button
|
||||||
</Button>
|
className="ghost grey pin"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPinChange();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="fa fa-solid fa-thumbtack" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button className="ghost grey close" onClick={onClose} onMouseDown={handleMouseDownOnClose}>
|
||||||
|
<i className="fa fa-solid fa-xmark" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -36,9 +36,18 @@
|
|||||||
|
|
||||||
.tab-bar {
|
.tab-bar {
|
||||||
position: relative; // Needed for absolute positioning of child tabs
|
position: relative; // Needed for absolute positioning of child tabs
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
height: 33px;
|
height: 33px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pinned-tab-spacer {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
margin: 2px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.dev-label,
|
.dev-label,
|
||||||
.app-menu-button {
|
.app-menu-button {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
|
@ -101,6 +101,7 @@ const ConfigErrorIcon = ({ buttonRef }: { buttonRef: React.RefObject<HTMLElement
|
|||||||
|
|
||||||
const TabBar = memo(({ workspace }: TabBarProps) => {
|
const TabBar = memo(({ workspace }: TabBarProps) => {
|
||||||
const [tabIds, setTabIds] = useState([]);
|
const [tabIds, setTabIds] = useState([]);
|
||||||
|
const [pinnedTabIds, setPinnedTabIds] = useState<Set<string>>(new Set());
|
||||||
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
||||||
const [draggingTab, setDraggingTab] = useState<string>();
|
const [draggingTab, setDraggingTab] = useState<string>();
|
||||||
const [tabsLoaded, setTabsLoaded] = useState({});
|
const [tabsLoaded, setTabsLoaded] = useState({});
|
||||||
@ -116,6 +117,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
tabId: "",
|
tabId: "",
|
||||||
ref: { current: null },
|
ref: { current: null },
|
||||||
tabStartX: 0,
|
tabStartX: 0,
|
||||||
|
tabStartIndex: 0,
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
initialOffsetX: null,
|
initialOffsetX: null,
|
||||||
totalScrollOffset: null,
|
totalScrollOffset: null,
|
||||||
@ -148,17 +150,25 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
// Compare current tabIds with new workspace.tabids
|
// Compare current tabIds with new workspace.tabids
|
||||||
const currentTabIds = new Set(tabIds);
|
console.log("tabbar workspace", workspace);
|
||||||
const newTabIds = new Set(workspace.tabids);
|
const newTabIds = new Set([...(workspace.pinnedtabids ?? []), ...(workspace.tabids ?? [])]);
|
||||||
|
const newPinnedTabIds = workspace.pinnedtabids ?? [];
|
||||||
|
|
||||||
const areEqual =
|
const areEqual =
|
||||||
currentTabIds.size === newTabIds.size && [...currentTabIds].every((id) => newTabIds.has(id));
|
tabIds.length === newTabIds.size &&
|
||||||
|
tabIds.every((id) => newTabIds.has(id)) &&
|
||||||
|
newPinnedTabIds.length === pinnedTabIds.size;
|
||||||
|
|
||||||
if (!areEqual) {
|
if (!areEqual) {
|
||||||
setTabIds(workspace.tabids);
|
const newPinnedTabIdSet = new Set(newPinnedTabIds);
|
||||||
|
console.log("newPinnedTabIds", newPinnedTabIds);
|
||||||
|
const newTabIdList = newPinnedTabIds.concat([...newTabIds].filter((id) => !newPinnedTabIdSet.has(id))); // Corrects for any duplicates between the two lists
|
||||||
|
console.log("newTabIdList", newTabIdList);
|
||||||
|
setTabIds(newTabIdList);
|
||||||
|
setPinnedTabIds(newPinnedTabIdSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [workspace, tabIds]);
|
}, [workspace, tabIds, pinnedTabIds]);
|
||||||
|
|
||||||
const saveTabsPosition = useCallback(() => {
|
const saveTabsPosition = useCallback(() => {
|
||||||
const tabs = tabRefs.current;
|
const tabs = tabRefs.current;
|
||||||
@ -246,9 +256,14 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveTabsPositionDebounced = useCallback(
|
||||||
|
debounce(100, () => saveTabsPosition()),
|
||||||
|
[saveTabsPosition]
|
||||||
|
);
|
||||||
|
|
||||||
const handleResizeTabs = useCallback(() => {
|
const handleResizeTabs = useCallback(() => {
|
||||||
setSizeAndPosition();
|
setSizeAndPosition();
|
||||||
debounce(100, () => saveTabsPosition())();
|
saveTabsPositionDebounced();
|
||||||
}, [tabIds, newTabId, isFullScreen]);
|
}, [tabIds, newTabId, isFullScreen]);
|
||||||
|
|
||||||
const reinitVersion = useAtomValue(atoms.reinitVersion);
|
const reinitVersion = useAtomValue(atoms.reinitVersion);
|
||||||
@ -278,7 +293,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
}, [tabIds, tabsLoaded, newTabId, saveTabsPosition]);
|
}, [tabIds, tabsLoaded, newTabId, saveTabsPosition]);
|
||||||
|
|
||||||
const getDragDirection = (currentX: number) => {
|
const getDragDirection = (currentX: number) => {
|
||||||
let dragDirection;
|
let dragDirection: string;
|
||||||
if (currentX - prevDelta > 0) {
|
if (currentX - prevDelta > 0) {
|
||||||
dragDirection = "+";
|
dragDirection = "+";
|
||||||
} else if (currentX - prevDelta === 0) {
|
} else if (currentX - prevDelta === 0) {
|
||||||
@ -433,6 +448,50 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// } else if ((tabIndex > pinnedTabCount || (tabIndex === 1 && pinnedTabCount === 1)) && isPinned) {
|
||||||
|
|
||||||
|
const setUpdatedTabsDebounced = useCallback(
|
||||||
|
debounce(300, (tabIndex: number, tabIds: string[], pinnedTabIds: Set<string>) => {
|
||||||
|
console.log(
|
||||||
|
"setting updated tabs",
|
||||||
|
tabIds,
|
||||||
|
pinnedTabIds,
|
||||||
|
tabIndex,
|
||||||
|
draggingTabDataRef.current.tabStartIndex
|
||||||
|
);
|
||||||
|
// Reset styles
|
||||||
|
tabRefs.current.forEach((ref) => {
|
||||||
|
ref.current.style.zIndex = "0";
|
||||||
|
ref.current.classList.remove("animate");
|
||||||
|
});
|
||||||
|
let pinnedTabCount = pinnedTabIds.size;
|
||||||
|
const draggedTabId = draggingTabDataRef.current.tabId;
|
||||||
|
const isPinned = pinnedTabIds.has(draggedTabId);
|
||||||
|
if (pinnedTabIds.has(tabIds[tabIndex + 1]) && !isPinned) {
|
||||||
|
pinnedTabIds.add(draggedTabId);
|
||||||
|
} else if (!pinnedTabIds.has(tabIds[tabIndex - 1]) && isPinned) {
|
||||||
|
pinnedTabIds.delete(draggedTabId);
|
||||||
|
}
|
||||||
|
if (pinnedTabCount != pinnedTabIds.size) {
|
||||||
|
console.log("updated pinnedTabIds", pinnedTabIds, tabIds);
|
||||||
|
setPinnedTabIds(pinnedTabIds);
|
||||||
|
pinnedTabCount = pinnedTabIds.size;
|
||||||
|
}
|
||||||
|
// Reset dragging state
|
||||||
|
setDraggingTab(null);
|
||||||
|
// Update workspace tab ids
|
||||||
|
fireAndForget(
|
||||||
|
async () =>
|
||||||
|
await WorkspaceService.UpdateTabIds(
|
||||||
|
workspace.oid,
|
||||||
|
tabIds.slice(pinnedTabCount),
|
||||||
|
tabIds.slice(0, pinnedTabCount)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const handleMouseUp = (event: MouseEvent) => {
|
const handleMouseUp = (event: MouseEvent) => {
|
||||||
const { tabIndex, dragged } = draggingTabDataRef.current;
|
const { tabIndex, dragged } = draggingTabDataRef.current;
|
||||||
|
|
||||||
@ -447,17 +506,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dragged) {
|
if (dragged) {
|
||||||
debounce(300, () => {
|
setUpdatedTabsDebounced(tabIndex, tabIds, pinnedTabIds);
|
||||||
// Reset styles
|
|
||||||
tabRefs.current.forEach((ref) => {
|
|
||||||
ref.current.style.zIndex = "0";
|
|
||||||
ref.current.classList.remove("animate");
|
|
||||||
});
|
|
||||||
// Reset dragging state
|
|
||||||
setDraggingTab(null);
|
|
||||||
// Update workspace tab ids
|
|
||||||
fireAndForget(async () => await WorkspaceService.UpdateTabIds(workspace.oid, tabIds));
|
|
||||||
})();
|
|
||||||
} else {
|
} else {
|
||||||
// Reset styles
|
// Reset styles
|
||||||
tabRefs.current.forEach((ref) => {
|
tabRefs.current.forEach((ref) => {
|
||||||
@ -480,12 +529,14 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
const tabIndex = tabIds.indexOf(tabId);
|
const tabIndex = tabIds.indexOf(tabId);
|
||||||
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
|
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
|
||||||
|
|
||||||
|
console.log("handleDragStart", tabId, tabIndex, tabStartX);
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
draggingTabDataRef.current = {
|
draggingTabDataRef.current = {
|
||||||
tabId,
|
tabId,
|
||||||
ref,
|
ref,
|
||||||
tabStartX,
|
tabStartX,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
|
tabStartIndex: tabIndex,
|
||||||
initialOffsetX: null,
|
initialOffsetX: null,
|
||||||
totalScrollOffset: 0,
|
totalScrollOffset: 0,
|
||||||
dragged: false,
|
dragged: false,
|
||||||
@ -504,19 +555,31 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddTab = () => {
|
const updateScrollDebounced = useCallback(
|
||||||
createTab();
|
|
||||||
tabsWrapperRef.current.style.transition;
|
|
||||||
tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.1s ease");
|
|
||||||
|
|
||||||
debounce(30, () => {
|
debounce(30, () => {
|
||||||
if (scrollableRef.current) {
|
if (scrollableRef.current) {
|
||||||
const { viewport } = osInstanceRef.current.elements();
|
const { viewport } = osInstanceRef.current.elements();
|
||||||
viewport.scrollLeft = tabIds.length * tabWidthRef.current;
|
viewport.scrollLeft = tabIds.length * tabWidthRef.current;
|
||||||
}
|
}
|
||||||
})();
|
}),
|
||||||
|
[tabIds]
|
||||||
|
);
|
||||||
|
|
||||||
debounce(100, () => setNewTabId(null))();
|
const setNewTabIdDebounced = useCallback(
|
||||||
|
debounce(100, (tabId: string) => {
|
||||||
|
setNewTabId(tabId);
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddTab = () => {
|
||||||
|
createTab();
|
||||||
|
tabsWrapperRef.current.style.transition;
|
||||||
|
tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.1s ease");
|
||||||
|
|
||||||
|
updateScrollDebounced();
|
||||||
|
|
||||||
|
setNewTabIdDebounced(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, tabId: string) => {
|
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, tabId: string) => {
|
||||||
@ -526,7 +589,14 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
deleteLayoutModelForTab(tabId);
|
deleteLayoutModelForTab(tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabLoaded = useCallback((tabId) => {
|
const handlePinChange = (tabId: string, pinned: boolean) => {
|
||||||
|
console.log("handlePinChange", tabId, pinned);
|
||||||
|
fireAndForget(async () => {
|
||||||
|
await WorkspaceService.ChangeTabPinning(workspace.oid, tabId, pinned);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabLoaded = useCallback((tabId: string) => {
|
||||||
setTabsLoaded((prev) => {
|
setTabsLoaded((prev) => {
|
||||||
if (!prev[tabId]) {
|
if (!prev[tabId]) {
|
||||||
// Only update if the tab isn't already marked as loaded
|
// Only update if the tab isn't already marked as loaded
|
||||||
@ -574,17 +644,20 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
|
|||||||
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
|
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
|
||||||
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}>
|
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}>
|
||||||
{tabIds.map((tabId, index) => {
|
{tabIds.map((tabId, index) => {
|
||||||
|
const isPinned = pinnedTabIds.has(tabId);
|
||||||
return (
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
key={tabId}
|
key={tabId}
|
||||||
ref={tabRefs.current[index]}
|
ref={tabRefs.current[index]}
|
||||||
id={tabId}
|
id={tabId}
|
||||||
isFirst={index === 0}
|
isFirst={index === 0}
|
||||||
|
isPinned={isPinned}
|
||||||
onClick={() => handleSelectTab(tabId)}
|
onClick={() => handleSelectTab(tabId)}
|
||||||
isActive={activeTabId === tabId}
|
isActive={activeTabId === tabId}
|
||||||
onMouseDown={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
|
onMouseDown={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
|
||||||
onClose={(event) => handleCloseTab(event, tabId)}
|
onClose={(event) => handleCloseTab(event, tabId)}
|
||||||
onLoaded={() => handleTabLoaded(tabId)}
|
onLoaded={() => handleTabLoaded(tabId)}
|
||||||
|
onPinChange={() => handlePinChange(tabId, !isPinned)}
|
||||||
isBeforeActive={isBeforeActive(tabId)}
|
isBeforeActive={isBeforeActive(tabId)}
|
||||||
isDragging={draggingTab === tabId}
|
isDragging={draggingTab === tabId}
|
||||||
tabWidth={tabWidthRef.current}
|
tabWidth={tabWidthRef.current}
|
||||||
|
@ -2,224 +2,230 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
.workspace-switcher-button {
|
.workspace-switcher-button {
|
||||||
display: flex;
|
|
||||||
height: 26px;
|
|
||||||
padding: 0px 12px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--modal-bg-color);
|
|
||||||
margin-top: 6px;
|
|
||||||
margin-right: 13px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
.workspace-icon {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0px 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 12px;
|
||||||
}
|
border-radius: 6px;
|
||||||
|
background: var(--modal-bg-color);
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-right: 13px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.workspace-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-left,
|
.icon-left,
|
||||||
.icon-right {
|
.icon-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-switcher-content {
|
.workspace-switcher-content {
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 256px;
|
width: 256px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0px 8px 24px 0px var(--modal-shadow-color);
|
box-shadow: 0px 8px 24px 0px var(--modal-shadow-color);
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 19px;
|
line-height: 19px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 8px 0px;
|
padding: 6px 8px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expandable-menu {
|
.expandable-menu {
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
|
||||||
|
|
||||||
.expandable-menu-item {
|
|
||||||
margin: 3px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expandable-menu-item-group {
|
|
||||||
margin: 0 8px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-bottom-left-radius: 8px;
|
|
||||||
border-bottom-right-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.expandable-menu-item {
|
.expandable-menu-item {
|
||||||
margin: 0;
|
margin: 3px 8px;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.expandable-menu-item-group {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
--workspace-color: var(--main-bg-color);
|
|
||||||
|
|
||||||
.menu-group-title-wrapper {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
.icons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconbutton.edit {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconbutton.window {
|
|
||||||
cursor: default;
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .iconbutton.edit {
|
.expandable-menu-item-group {
|
||||||
visibility: visible;
|
margin: 0 8px;
|
||||||
}
|
|
||||||
|
|
||||||
&.open {
|
&:last-child {
|
||||||
background-color: var(--modal-bg-color);
|
margin-bottom: 4px;
|
||||||
border: 1px solid var(--modal-border-color);
|
border-bottom-left-radius: 8px;
|
||||||
}
|
border-bottom-right-radius: 8px;
|
||||||
|
|
||||||
&.is-current .menu-group-title-wrapper {
|
|
||||||
background-color: rgb(from var(--workspace-color) r g b / 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.expandable-menu-item,
|
|
||||||
.expandable-menu-item-group-title {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 19px;
|
|
||||||
padding: 5px 8px;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.expandable-menu-item-group-title {
|
|
||||||
height: 29px;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
.left-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-icon-selector {
|
|
||||||
width: 100%;
|
|
||||||
.input {
|
|
||||||
margin: 5px 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-selector {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(15px, 15px)); // Ensures each color circle has a fixed 14px size
|
|
||||||
grid-gap: 18.5px; // Space between items
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.color-circle {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
// Border offset outward
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -3px;
|
|
||||||
left: -3px;
|
|
||||||
right: -3px;
|
|
||||||
bottom: -3px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected:before {
|
.expandable-menu-item {
|
||||||
border-color: var(--main-text-color); // Highlight for the selected circle
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-selector {
|
.expandable-menu-item-group {
|
||||||
display: grid;
|
border: 1px solid transparent;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(16px, 16px)); // Ensures each color circle has a fixed 14px size
|
border-radius: 4px;
|
||||||
grid-column-gap: 17.5px; // Space between items
|
|
||||||
grid-row-gap: 13px; // Space between items
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 15px;
|
|
||||||
|
|
||||||
.icon-item {
|
--workspace-color: var(--main-bg-color);
|
||||||
font-size: 15px;
|
|
||||||
color: oklch(from var(--modal-bg-color) calc(l * 1.5) c h);
|
.menu-group-title-wrapper {
|
||||||
cursor: pointer;
|
display: flex;
|
||||||
transition: color 0.3s ease;
|
width: 100%;
|
||||||
|
padding: 5px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
.icons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconbutton.edit {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconbutton.window {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .iconbutton.edit {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
background-color: var(--modal-bg-color);
|
||||||
|
border: 1px solid var(--modal-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-current .menu-group-title-wrapper {
|
||||||
|
background-color: rgb(from var(--workspace-color) r g b / 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-menu-item,
|
||||||
|
.expandable-menu-item-group-title {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 19px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&.selected,
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--main-text-color);
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-ws-btn-wrapper {
|
.expandable-menu-item-group-title {
|
||||||
display: flex;
|
height: 29px;
|
||||||
align-items: center;
|
padding: 0;
|
||||||
justify-content: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.left-icon {
|
||||||
width: 100%;
|
font-size: 14px;
|
||||||
padding: 3px 0;
|
}
|
||||||
border-top: 1px solid var(--modal-border-color);
|
}
|
||||||
}
|
|
||||||
|
.color-icon-selector {
|
||||||
|
width: 100%;
|
||||||
|
.input {
|
||||||
|
margin: 5px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-selector {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(
|
||||||
|
auto-fit,
|
||||||
|
minmax(15px, 15px)
|
||||||
|
); // Ensures each color circle has a fixed 14px size
|
||||||
|
grid-gap: 18.5px; // Space between items
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
.color-circle {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
// Border offset outward
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
left: -3px;
|
||||||
|
right: -3px;
|
||||||
|
bottom: -3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected:before {
|
||||||
|
border-color: var(--main-text-color); // Highlight for the selected circle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-selector {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(
|
||||||
|
auto-fit,
|
||||||
|
minmax(16px, 16px)
|
||||||
|
); // Ensures each color circle has a fixed 14px size
|
||||||
|
grid-column-gap: 17.5px; // Space between items
|
||||||
|
grid-row-gap: 13px; // Space between items
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.icon-item {
|
||||||
|
font-size: 15px;
|
||||||
|
color: oklch(from var(--modal-bg-color) calc(l * 1.5) c h);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&:hover {
|
||||||
|
color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-ws-btn-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
padding: 3px 0;
|
||||||
|
border-top: 1px solid var(--modal-border-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,7 +281,7 @@ const WorkspaceSwitcherItem = ({
|
|||||||
const windowIconDecl: IconButtonDecl = {
|
const windowIconDecl: IconButtonDecl = {
|
||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
className: "window",
|
className: "window",
|
||||||
disabled: true,
|
noAction: true,
|
||||||
icon: isCurrentWorkspace ? "check" : "window",
|
icon: isCurrentWorkspace ? "check" : "window",
|
||||||
title: isCurrentWorkspace ? "This is your current workspace" : "This workspace is open",
|
title: isCurrentWorkspace ? "This is your current workspace" : "This workspace is open",
|
||||||
};
|
};
|
||||||
|
@ -5,157 +5,157 @@
|
|||||||
@import url("../../node_modules/highlight.js/styles/github-dark-dimmed.min.css");
|
@import url("../../node_modules/highlight.js/styles/github-dark-dimmed.min.css");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--main-text-color: #f7f7f7;
|
--main-text-color: #f7f7f7;
|
||||||
--title-font-size: 18px;
|
--title-font-size: 18px;
|
||||||
--window-opacity: 1;
|
--window-opacity: 1;
|
||||||
--secondary-text-color: rgb(195, 200, 194);
|
--secondary-text-color: rgb(195, 200, 194);
|
||||||
--grey-text-color: #666;
|
--grey-text-color: #666;
|
||||||
--main-bg-color: rgb(34, 34, 34);
|
--main-bg-color: rgb(34, 34, 34);
|
||||||
--border-color: rgba(255, 255, 255, 0.16);
|
--border-color: rgba(255, 255, 255, 0.16);
|
||||||
--base-font: normal 14px / normal "Inter", sans-serif;
|
--base-font: normal 14px / normal "Inter", sans-serif;
|
||||||
--fixed-font: normal 12px / normal "Hack", monospace;
|
--fixed-font: normal 12px / normal "Hack", monospace;
|
||||||
--accent-color: rgb(88, 193, 66);
|
--accent-color: rgb(88, 193, 66);
|
||||||
--panel-bg-color: rgba(31, 33, 31, 0.5);
|
--panel-bg-color: rgba(31, 33, 31, 0.5);
|
||||||
--highlight-bg-color: rgba(255, 255, 255, 0.2);
|
--highlight-bg-color: rgba(255, 255, 255, 0.2);
|
||||||
--markdown-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif,
|
--markdown-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif,
|
||||||
"Apple Color Emoji", "Segoe UI Emoji";
|
"Apple Color Emoji", "Segoe UI Emoji";
|
||||||
--error-color: rgb(229, 77, 46);
|
--error-color: rgb(229, 77, 46);
|
||||||
--warning-color: rgb(224, 185, 86);
|
--warning-color: rgb(224, 185, 86);
|
||||||
--success-color: rgb(78, 154, 6);
|
--success-color: rgb(78, 154, 6);
|
||||||
--hover-bg-color: rgba(255, 255, 255, 0.1);
|
--hover-bg-color: rgba(255, 255, 255, 0.1);
|
||||||
--block-bg-color: rgba(0, 0, 0, 0.5);
|
--block-bg-color: rgba(0, 0, 0, 0.5);
|
||||||
--block-bg-solid-color: rgb(0, 0, 0);
|
--block-bg-solid-color: rgb(0, 0, 0);
|
||||||
--block-border-radius: 8px;
|
--block-border-radius: 8px;
|
||||||
|
|
||||||
--keybinding-color: #e0e0e0;
|
--keybinding-color: #e0e0e0;
|
||||||
--keybinding-bg-color: #333;
|
--keybinding-bg-color: #333;
|
||||||
--keybinding-border-color: #444;
|
--keybinding-border-color: #444;
|
||||||
|
|
||||||
/* scrollbar colors */
|
/* scrollbar colors */
|
||||||
--scrollbar-background-color: transparent;
|
--scrollbar-background-color: transparent;
|
||||||
--scrollbar-thumb-color: rgba(255, 255, 255, 0.15);
|
--scrollbar-thumb-color: rgba(255, 255, 255, 0.15);
|
||||||
--scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.5);
|
--scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.5);
|
||||||
--scrollbar-thumb-active-color: rgba(255, 255, 255, 0.6);
|
--scrollbar-thumb-active-color: rgba(255, 255, 255, 0.6);
|
||||||
|
|
||||||
--header-font: 700 11px / normal "Inter", sans-serif;
|
--header-font: 700 11px / normal "Inter", sans-serif;
|
||||||
--header-icon-size: 14px;
|
--header-icon-size: 14px;
|
||||||
--header-icon-width: 16px;
|
--header-icon-width: 16px;
|
||||||
--header-height: 30px;
|
--header-height: 30px;
|
||||||
|
|
||||||
--tab-green: rgb(88, 193, 66);
|
--tab-green: rgb(88, 193, 66);
|
||||||
|
|
||||||
/* z-index values */
|
/* z-index values */
|
||||||
--zindex-header-hover: 100;
|
--zindex-header-hover: 100;
|
||||||
--zindex-termstickers: 20;
|
--zindex-termstickers: 20;
|
||||||
--zindex-modal: 2;
|
--zindex-modal: 2;
|
||||||
--zindex-modal-wrapper: 500;
|
--zindex-modal-wrapper: 500;
|
||||||
--zindex-modal-backdrop: 1;
|
--zindex-modal-backdrop: 1;
|
||||||
--zindex-typeahead-modal: 100;
|
--zindex-typeahead-modal: 100;
|
||||||
--zindex-typeahead-modal-backdrop: 90;
|
--zindex-typeahead-modal-backdrop: 90;
|
||||||
--zindex-elem-modal: 100;
|
--zindex-elem-modal: 100;
|
||||||
--zindex-window-drag: 100;
|
--zindex-window-drag: 100;
|
||||||
--zindex-tab-name: 3;
|
--zindex-tab-name: 3;
|
||||||
--zindex-layout-display-container: 0;
|
--zindex-layout-display-container: 0;
|
||||||
--zindex-layout-last-magnified-node: 1;
|
--zindex-layout-last-magnified-node: 1;
|
||||||
--zindex-layout-last-ephemeral-node: 2;
|
--zindex-layout-last-ephemeral-node: 2;
|
||||||
--zindex-layout-resize-handle: 3;
|
--zindex-layout-resize-handle: 3;
|
||||||
--zindex-layout-placeholder-container: 4;
|
--zindex-layout-placeholder-container: 4;
|
||||||
--zindex-layout-overlay-container: 5;
|
--zindex-layout-overlay-container: 5;
|
||||||
--zindex-layout-magnified-node-backdrop: 6;
|
--zindex-layout-magnified-node-backdrop: 6;
|
||||||
--zindex-layout-magnified-node: 7;
|
--zindex-layout-magnified-node: 7;
|
||||||
--zindex-layout-ephemeral-node-backdrop: 8;
|
--zindex-layout-ephemeral-node-backdrop: 8;
|
||||||
--zindex-layout-ephemeral-node: 9;
|
--zindex-layout-ephemeral-node: 9;
|
||||||
--zindex-block-mask-inner: 10;
|
--zindex-block-mask-inner: 10;
|
||||||
--zindex-flash-error-container: 550;
|
--zindex-flash-error-container: 550;
|
||||||
--zindex-app-background: -1;
|
--zindex-app-background: -1;
|
||||||
|
|
||||||
// z-indexes in xterm.css
|
// z-indexes in xterm.css
|
||||||
// xterm-helpers: 5
|
// xterm-helpers: 5
|
||||||
// xterm-helper-textarea: -5
|
// xterm-helper-textarea: -5
|
||||||
// composition-view: 1
|
// composition-view: 1
|
||||||
// xterm-message: 10
|
// xterm-message: 10
|
||||||
// xterm-decoration: 6
|
// xterm-decoration: 6
|
||||||
// xterm-decoration-top-layer: 7
|
// xterm-decoration-top-layer: 7
|
||||||
// xterm-decoration-overview-ruler: 8
|
// xterm-decoration-overview-ruler: 8
|
||||||
// xterm-decoration-top: 2
|
// xterm-decoration-top: 2
|
||||||
--zindex-xterm-viewport-overlay: 5; // Viewport contains the scrollbar
|
--zindex-xterm-viewport-overlay: 5; // Viewport contains the scrollbar
|
||||||
|
|
||||||
// modal colors
|
// modal colors
|
||||||
--modal-bg-color: #232323;
|
--modal-bg-color: #232323;
|
||||||
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
|
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
|
||||||
--modal-border-color: rgba(255, 255, 255, 0.12); /* toggle colors */
|
--modal-border-color: rgba(255, 255, 255, 0.12); /* toggle colors */
|
||||||
--toggle-bg-color: var(--border-color);
|
--toggle-bg-color: var(--border-color);
|
||||||
--modal-shadow-color: rgba(0, 0, 0, 0.8);
|
--modal-shadow-color: rgba(0, 0, 0, 0.8);
|
||||||
|
|
||||||
--toggle-thumb-color: var(--main-text-color);
|
--toggle-thumb-color: var(--main-text-color);
|
||||||
--toggle-checked-bg-color: var(--accent-color);
|
--toggle-checked-bg-color: var(--accent-color);
|
||||||
|
|
||||||
// link color
|
// link color
|
||||||
--link-color: #58c142;
|
--link-color: #58c142;
|
||||||
|
|
||||||
// form colors
|
// form colors
|
||||||
--form-element-border-color: rgba(241, 246, 243, 0.15);
|
--form-element-border-color: rgba(241, 246, 243, 0.15);
|
||||||
--form-element-bg-color: var(--main-bg-color);
|
--form-element-bg-color: var(--main-bg-color);
|
||||||
--form-element-text-color: var(--main-text-color);
|
--form-element-text-color: var(--main-text-color);
|
||||||
--form-element-primary-text-color: var(--main-text-color);
|
--form-element-primary-text-color: var(--main-text-color);
|
||||||
--form-element-primary-color: var(--accent-color);
|
--form-element-primary-color: var(--accent-color);
|
||||||
--form-element-secondary-color: rgba(255, 255, 255, 0.2);
|
--form-element-secondary-color: rgba(255, 255, 255, 0.2);
|
||||||
--form-element-error-color: var(--error-color);
|
--form-element-error-color: var(--error-color);
|
||||||
|
|
||||||
--conn-icon-color: #53b4ea;
|
--conn-icon-color: #53b4ea;
|
||||||
--conn-icon-color-1: #53b4ea;
|
--conn-icon-color-1: #53b4ea;
|
||||||
--conn-icon-color-2: #aa67ff;
|
--conn-icon-color-2: #aa67ff;
|
||||||
--conn-icon-color-3: #fda7fd;
|
--conn-icon-color-3: #fda7fd;
|
||||||
--conn-icon-color-4: #ef476f;
|
--conn-icon-color-4: #ef476f;
|
||||||
--conn-icon-color-5: #497bf8;
|
--conn-icon-color-5: #497bf8;
|
||||||
--conn-icon-color-6: #ffa24e;
|
--conn-icon-color-6: #ffa24e;
|
||||||
--conn-icon-color-7: #dbde52;
|
--conn-icon-color-7: #dbde52;
|
||||||
--conn-icon-color-8: #58c142;
|
--conn-icon-color-8: #58c142;
|
||||||
--conn-status-overlay-bg-color: rgba(230, 186, 30, 0.2);
|
--conn-status-overlay-bg-color: rgba(230, 186, 30, 0.2);
|
||||||
|
|
||||||
--sysinfo-cpu-color: #58c142;
|
--sysinfo-cpu-color: #58c142;
|
||||||
--sysinfo-mem-color: #53b4ea;
|
--sysinfo-mem-color: #53b4ea;
|
||||||
|
|
||||||
--bulb-color: rgb(255, 221, 51);
|
--bulb-color: rgb(255, 221, 51);
|
||||||
|
|
||||||
// term colors (16 + 6) form the base terminal theme
|
// term colors (16 + 6) form the base terminal theme
|
||||||
// for consistency these colors should be used by plugins/applications
|
// for consistency these colors should be used by plugins/applications
|
||||||
--term-black: #000000;
|
--term-black: #000000;
|
||||||
--term-red: #cc0000;
|
--term-red: #cc0000;
|
||||||
--term-green: #4e9a06;
|
--term-green: #4e9a06;
|
||||||
--term-yellow: #c4a000;
|
--term-yellow: #c4a000;
|
||||||
--term-blue: #3465a4;
|
--term-blue: #3465a4;
|
||||||
--term-magenta: #bc3fbc;
|
--term-magenta: #bc3fbc;
|
||||||
--term-cyan: #06989a;
|
--term-cyan: #06989a;
|
||||||
--term-white: #d0d0d0;
|
--term-white: #d0d0d0;
|
||||||
--term-bright-black: #555753;
|
--term-bright-black: #555753;
|
||||||
--term-bright-red: #ef2929;
|
--term-bright-red: #ef2929;
|
||||||
--term-bright-green: #58c142;
|
--term-bright-green: #58c142;
|
||||||
--term-bright-yellow: #fce94f;
|
--term-bright-yellow: #fce94f;
|
||||||
--term-bright-blue: #32afff;
|
--term-bright-blue: #32afff;
|
||||||
--term-bright-magenta: #ad7fa8;
|
--term-bright-magenta: #ad7fa8;
|
||||||
--term-bright-cyan: #34e2e2;
|
--term-bright-cyan: #34e2e2;
|
||||||
--term-bright-white: #e7e7e7;
|
--term-bright-white: #e7e7e7;
|
||||||
|
|
||||||
--term-gray: #8b918a; // not an official terminal color
|
--term-gray: #8b918a; // not an official terminal color
|
||||||
--term-cmdtext: #ffffff;
|
--term-cmdtext: #ffffff;
|
||||||
--term-foreground: #d3d7cf;
|
--term-foreground: #d3d7cf;
|
||||||
--term-background: #000000;
|
--term-background: #000000;
|
||||||
--term-selection-background: #ffffff60;
|
--term-selection-background: #ffffff60;
|
||||||
--term-cursor-accent: #000000;
|
--term-cursor-accent: #000000;
|
||||||
|
|
||||||
// button colors
|
// button colors
|
||||||
--button-text-color: #000000;
|
--button-text-color: #000000;
|
||||||
--button-green-bg: var(--term-green);
|
--button-green-bg: var(--term-green);
|
||||||
--button-green-border-color: #29f200;
|
--button-green-border-color: #29f200;
|
||||||
--button-grey-bg: rgba(255, 255, 255, 0.04);
|
--button-grey-bg: rgba(255, 255, 255, 0.04);
|
||||||
--button-grey-hover-bg: rgba(255, 255, 255, 0.09);
|
--button-grey-hover-bg: rgba(255, 255, 255, 0.09);
|
||||||
--button-grey-border-color: rgba(255, 255, 255, 0.1);
|
--button-grey-border-color: rgba(255, 255, 255, 0.1);
|
||||||
--button-grey-outlined-color: rgba(255, 255, 255, 0.6);
|
--button-grey-outlined-color: rgba(255, 255, 255, 0.6);
|
||||||
--button-red-bg: #cc0000;
|
--button-red-bg: #cc0000;
|
||||||
--button-red-hover-bg: #f93939;
|
--button-red-hover-bg: #f93939;
|
||||||
--button-red-border-color: #fc3131;
|
--button-red-border-color: #fc3131;
|
||||||
--button-red-outlined-color: #ff3c3c;
|
--button-red-outlined-color: #ff3c3c;
|
||||||
--button-yellow-bg: #c4a000;
|
--button-yellow-bg: #c4a000;
|
||||||
--button-yellow-hover-bg: #fce94f;
|
--button-yellow-hover-bg: #fce94f;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { atoms } from "@/app/store/global";
|
import { useOverrideConfigAtom } from "@/app/store/global";
|
||||||
import loader from "@monaco-editor/loader";
|
import loader from "@monaco-editor/loader";
|
||||||
import { Editor, Monaco } from "@monaco-editor/react";
|
import { Editor, Monaco } from "@monaco-editor/react";
|
||||||
import { atom, useAtomValue } from "jotai";
|
|
||||||
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
import { configureMonacoYaml } from "monaco-yaml";
|
import { configureMonacoYaml } from "monaco-yaml";
|
||||||
import React, { useMemo, useRef } from "react";
|
import React, { useMemo, useRef } from "react";
|
||||||
@ -108,6 +107,7 @@ function defaultEditorOptions(): MonacoTypes.editor.IEditorOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CodeEditorProps {
|
interface CodeEditorProps {
|
||||||
|
blockId: string;
|
||||||
text: string;
|
text: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
@ -116,21 +116,12 @@ interface CodeEditorProps {
|
|||||||
onMount?: (monacoPtr: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => () => void;
|
onMount?: (monacoPtr: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const minimapEnabledAtom = atom((get) => {
|
export function CodeEditor({ blockId, text, language, filename, meta, onChange, onMount }: CodeEditorProps) {
|
||||||
const settings = get(atoms.settingsAtom);
|
|
||||||
return settings["editor:minimapenabled"] ?? false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const stickyScrollEnabledAtom = atom((get) => {
|
|
||||||
const settings = get(atoms.settingsAtom);
|
|
||||||
return settings["editor:stickyscrollenabled"] ?? false;
|
|
||||||
});
|
|
||||||
|
|
||||||
export function CodeEditor({ text, language, filename, meta, onChange, onMount }: CodeEditorProps) {
|
|
||||||
const divRef = useRef<HTMLDivElement>(null);
|
const divRef = useRef<HTMLDivElement>(null);
|
||||||
const unmountRef = useRef<() => void>(null);
|
const unmountRef = useRef<() => void>(null);
|
||||||
const minimapEnabled = useAtomValue(minimapEnabledAtom);
|
const minimapEnabled = useOverrideConfigAtom(blockId, "editor:minimapenabled") ?? false;
|
||||||
const stickyScrollEnabled = useAtomValue(stickyScrollEnabledAtom);
|
const stickyScrollEnabled = useOverrideConfigAtom(blockId, "editor:stickyscrollenabled") ?? false;
|
||||||
|
const wordWrap = useOverrideConfigAtom(blockId, "editor:wordwrap") ?? false;
|
||||||
const theme = "wave-theme-dark";
|
const theme = "wave-theme-dark";
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -158,9 +149,9 @@ export function CodeEditor({ text, language, filename, meta, onChange, onMount }
|
|||||||
const opts = defaultEditorOptions();
|
const opts = defaultEditorOptions();
|
||||||
opts.minimap.enabled = minimapEnabled;
|
opts.minimap.enabled = minimapEnabled;
|
||||||
opts.stickyScroll.enabled = stickyScrollEnabled;
|
opts.stickyScroll.enabled = stickyScrollEnabled;
|
||||||
opts.wordWrap = meta?.["editor:wordwrap"] ? "on" : "off";
|
opts.wordWrap = wordWrap ? "on" : "off";
|
||||||
return opts;
|
return opts;
|
||||||
}, [minimapEnabled, stickyScrollEnabled, meta?.["editor:wordwrap"]]);
|
}, [minimapEnabled, stickyScrollEnabled, wordWrap]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="code-editor-wrapper">
|
<div className="code-editor-wrapper">
|
||||||
|
@ -4,243 +4,249 @@
|
|||||||
@use "../../mixins.scss";
|
@use "../../mixins.scss";
|
||||||
|
|
||||||
.dir-table-container {
|
.dir-table-container {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
--min-row-width: 35rem;
|
|
||||||
.dir-table {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
--col-size-size: 0.2rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
&:not([data-scroll-height="0"]) .dir-table-head::after {
|
--min-row-width: 35rem;
|
||||||
background: oklch(from var(--block-bg-color) calc(l + 0.5) c h);
|
.dir-table {
|
||||||
backdrop-filter: blur(2px);
|
height: 100%;
|
||||||
content: "";
|
width: 100%;
|
||||||
z-index: -1;
|
--col-size-size: 0.2rem;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dir-table-head {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
|
|
||||||
.dir-table-head-row {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: var(--min-row-width);
|
flex-direction: column;
|
||||||
padding: 4px 6px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
|
|
||||||
.dir-table-head-cell {
|
&:not([data-scroll-height="0"]) .dir-table-head::after {
|
||||||
flex: 0 0 auto;
|
background: oklch(from var(--block-bg-color) calc(l + 0.5) c h);
|
||||||
user-select: none;
|
backdrop-filter: blur(2px);
|
||||||
|
content: "";
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
.dir-table-head-cell:not(:first-child) {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.dir-table-head-cell-content {
|
.dir-table-head {
|
||||||
padding: 2px 4px;
|
position: sticky;
|
||||||
display: flex;
|
top: 0;
|
||||||
gap: 0.3rem;
|
z-index: 10;
|
||||||
flex: 1 1 auto;
|
width: 100%;
|
||||||
overflow-x: hidden;
|
border-bottom: 1px solid var(--border-color);
|
||||||
letter-spacing: -0.12px;
|
|
||||||
|
|
||||||
.dir-table-head-direction {
|
.dir-table-head-row {
|
||||||
margin-right: 0.2rem;
|
display: flex;
|
||||||
margin-top: 0.2rem;
|
min-width: var(--min-row-width);
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
.dir-table-head-cell {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.dir-table-head-cell:not(:first-child) {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.dir-table-head-cell-content {
|
||||||
|
padding: 2px 4px;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.3rem;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
letter-spacing: -0.12px;
|
||||||
|
|
||||||
|
.dir-table-head-direction {
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-table-head-size {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-table-head-resize-box {
|
||||||
|
width: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
.dir-table-head-resize {
|
||||||
|
cursor: col-resize;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-table-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.dir-table-body-search-display {
|
||||||
|
display: flex;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background-color: var(--warning-color);
|
||||||
|
|
||||||
|
.search-display-close-button {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-table-body-scroll-box {
|
||||||
|
position: relative;
|
||||||
|
.dummy {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.dir-table-body-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0 6px;
|
||||||
|
min-width: var(--min-row-width);
|
||||||
|
|
||||||
|
&.focused {
|
||||||
|
background-color: rgb(from var(--accent-color) r g b / 0.5);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
|
||||||
|
.dir-table-body-cell {
|
||||||
|
.dir-table-lastmod,
|
||||||
|
.dir-table-modestr,
|
||||||
|
.dir-table-size,
|
||||||
|
.dir-table-type {
|
||||||
|
color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: rgb(from var(--accent-color) r g b / 0.5);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
|
||||||
|
.dir-table-body-cell {
|
||||||
|
.dir-table-lastmod,
|
||||||
|
.dir-table-modestr,
|
||||||
|
.dir-table-size,
|
||||||
|
.dir-table-type {
|
||||||
|
color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:focus):not(.focused) {
|
||||||
|
background-color: var(--highlight-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-table-body-cell {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0.25rem;
|
||||||
|
cursor: default;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
&.col-size {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-table-lastmod,
|
||||||
|
.dir-table-modestr,
|
||||||
|
.dir-table-size,
|
||||||
|
.dir-table-type {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-table-type {
|
||||||
|
@include mixins.ellipsis();
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-table-modestr {
|
||||||
|
font-family: Hack;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.dir-table-name) {
|
||||||
|
@include mixins.ellipsis();
|
||||||
|
}
|
||||||
|
.dir-table-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dir-table-head-size {
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dir-table-head-resize-box {
|
|
||||||
width: 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
.dir-table-head-resize {
|
|
||||||
cursor: col-resize;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
touch-action: none;
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dir-table-body {
|
.dir-table-search-line {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.dir-table-body-search-display {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: 3px;
|
justify-content: flex-end;
|
||||||
padding: 0.25rem 0.5rem;
|
gap: 0.7rem;
|
||||||
background-color: var(--warning-color);
|
|
||||||
|
|
||||||
.search-display-close-button {
|
.dir-table-search-box {
|
||||||
margin-left: auto;
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.dir-table-body-scroll-box {
|
|
||||||
position: relative;
|
|
||||||
.dummy {
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
.dir-table-body-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 0 6px;
|
|
||||||
min-width: var(--min-row-width);
|
|
||||||
|
|
||||||
&.focused {
|
|
||||||
background-color: rgb(from var(--accent-color) r g b / 0.5);
|
|
||||||
color: var(--main-text-color);
|
|
||||||
|
|
||||||
.dir-table-body-cell {
|
|
||||||
.dir-table-lastmod,
|
|
||||||
.dir-table-modestr,
|
|
||||||
.dir-table-size,
|
|
||||||
.dir-table-type {
|
|
||||||
color: var(--main-text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
background-color: rgb(from var(--accent-color) r g b / 0.5);
|
|
||||||
color: var(--main-text-color);
|
|
||||||
|
|
||||||
.dir-table-body-cell {
|
|
||||||
.dir-table-lastmod,
|
|
||||||
.dir-table-modestr,
|
|
||||||
.dir-table-size,
|
|
||||||
.dir-table-type {
|
|
||||||
color: var(--main-text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(:focus):not(.focused) {
|
|
||||||
background-color: var(--highlight-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dir-table-body-cell {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0.25rem;
|
|
||||||
cursor: default;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
&.col-size {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dir-table-lastmod,
|
|
||||||
.dir-table-modestr,
|
|
||||||
.dir-table-size,
|
|
||||||
.dir-table-type {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dir-table-type {
|
|
||||||
@include mixins.ellipsis();
|
|
||||||
}
|
|
||||||
|
|
||||||
.dir-table-modestr {
|
|
||||||
font-family: Hack;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(.dir-table-name) {
|
|
||||||
@include mixins.ellipsis();
|
|
||||||
}
|
|
||||||
.dir-table-name {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.dir-table-search-line {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.7rem;
|
|
||||||
|
|
||||||
.dir-table-search-box {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dir-table-button {
|
.dir-table-button {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--highlight-bg-color);
|
background-color: var(--highlight-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: var(--highlight-bg-color);
|
background-color: var(--highlight-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
background-color: var(--highlight-bg-color);
|
background-color: var(--highlight-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry-manager-overlay {
|
.entry-manager-overlay {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
max-height: fit-content;
|
max-height: fit-content;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
background: #212121;
|
background: #212121;
|
||||||
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
|
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
.entry-manager-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { Button } from "@/app/element/button";
|
||||||
import { Input } from "@/app/element/input";
|
import { Input } from "@/app/element/input";
|
||||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||||
import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/global";
|
import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/global";
|
||||||
@ -146,12 +147,21 @@ type EntryManagerOverlayProps = {
|
|||||||
entryManagerType: EntryManagerType;
|
entryManagerType: EntryManagerType;
|
||||||
startingValue?: string;
|
startingValue?: string;
|
||||||
onSave: (newValue: string) => void;
|
onSave: (newValue: string) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
getReferenceProps?: () => any;
|
getReferenceProps?: () => any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EntryManagerOverlay = memo(
|
const EntryManagerOverlay = memo(
|
||||||
({ entryManagerType, startingValue, onSave, forwardRef, style, getReferenceProps }: EntryManagerOverlayProps) => {
|
({
|
||||||
|
entryManagerType,
|
||||||
|
startingValue,
|
||||||
|
onSave,
|
||||||
|
onCancel,
|
||||||
|
forwardRef,
|
||||||
|
style,
|
||||||
|
getReferenceProps,
|
||||||
|
}: EntryManagerOverlayProps) => {
|
||||||
const [value, setValue] = useState(startingValue);
|
const [value, setValue] = useState(startingValue);
|
||||||
return (
|
return (
|
||||||
<div className="entry-manager-overlay" ref={forwardRef} style={style} {...getReferenceProps()}>
|
<div className="entry-manager-overlay" ref={forwardRef} style={style} {...getReferenceProps()}>
|
||||||
@ -168,7 +178,15 @@ const EntryManagerOverlay = memo(
|
|||||||
onSave(value);
|
onSave(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
></Input>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="entry-manager-buttons">
|
||||||
|
<Button className="vertical-padding-4" onClick={() => onSave(value)}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button className="vertical-padding-4 red outlined" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -870,6 +888,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
|||||||
}}
|
}}
|
||||||
{...getReferenceProps()}
|
{...getReferenceProps()}
|
||||||
onContextMenu={(e) => handleFileContextMenu(e)}
|
onContextMenu={(e) => handleFileContextMenu(e)}
|
||||||
|
onClick={() => setEntryManagerProps(undefined)}
|
||||||
>
|
>
|
||||||
<DirectoryTable
|
<DirectoryTable
|
||||||
model={model}
|
model={model}
|
||||||
@ -891,6 +910,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
|||||||
forwardRef={refs.setFloating}
|
forwardRef={refs.setFloating}
|
||||||
style={floatingStyles}
|
style={floatingStyles}
|
||||||
getReferenceProps={getFloatingProps}
|
getReferenceProps={getFloatingProps}
|
||||||
|
onCancel={() => setEntryManagerProps(undefined)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -10,7 +10,15 @@ import { RpcApi } from "@/app/store/wshclientapi";
|
|||||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||||
import { CodeEditor } from "@/app/view/codeeditor/codeeditor";
|
import { CodeEditor } from "@/app/view/codeeditor/codeeditor";
|
||||||
import { Markdown } from "@/element/markdown";
|
import { Markdown } from "@/element/markdown";
|
||||||
import { atoms, createBlock, getConnStatusAtom, getSettingsKeyAtom, globalStore, refocusNode } from "@/store/global";
|
import {
|
||||||
|
atoms,
|
||||||
|
createBlock,
|
||||||
|
getConnStatusAtom,
|
||||||
|
getOverrideConfigAtom,
|
||||||
|
getSettingsKeyAtom,
|
||||||
|
globalStore,
|
||||||
|
refocusNode,
|
||||||
|
} from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||||
@ -659,6 +667,8 @@ export class PreviewModel implements ViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const loadableSV = globalStore.get(this.loadableSpecializedView);
|
const loadableSV = globalStore.get(this.loadableSpecializedView);
|
||||||
|
const wordWrapAtom = getOverrideConfigAtom(this.blockId, "editor:wordwrap");
|
||||||
|
const wordWrap = globalStore.get(wordWrapAtom) ?? false;
|
||||||
if (loadableSV.state == "hasData") {
|
if (loadableSV.state == "hasData") {
|
||||||
if (loadableSV.data.specializedView == "codeedit") {
|
if (loadableSV.data.specializedView == "codeedit") {
|
||||||
if (globalStore.get(this.newFileContent) != null) {
|
if (globalStore.get(this.newFileContent) != null) {
|
||||||
@ -676,11 +686,11 @@ export class PreviewModel implements ViewModel {
|
|||||||
menuItems.push({
|
menuItems.push({
|
||||||
label: "Word Wrap",
|
label: "Word Wrap",
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: blockData?.meta?.["editor:wordwrap"] ?? false,
|
checked: wordWrap,
|
||||||
click: () => {
|
click: () => {
|
||||||
const blockOref = WOS.makeORef("block", this.blockId);
|
const blockOref = WOS.makeORef("block", this.blockId);
|
||||||
services.ObjectService.UpdateObjectMeta(blockOref, {
|
services.ObjectService.UpdateObjectMeta(blockOref, {
|
||||||
"editor:wordwrap": !blockData?.meta?.["editor:wordwrap"],
|
"editor:wordwrap": !wordWrap,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -865,6 +875,7 @@ function CodeEditPreview({ model }: SpecializedViewProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
blockId={model.blockId}
|
||||||
text={fileContent}
|
text={fileContent}
|
||||||
filename={fileName}
|
filename={fileName}
|
||||||
meta={blockMeta}
|
meta={blockMeta}
|
||||||
|
@ -36,6 +36,24 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.term-cmd-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 24px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.term-cmd-toolbar-text {
|
||||||
|
font: var(--fixed-font);
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.term-connectelem {
|
.term-connectelem {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
@ -56,7 +56,6 @@ class TermViewModel {
|
|||||||
manageConnection: jotai.Atom<boolean>;
|
manageConnection: jotai.Atom<boolean>;
|
||||||
connStatus: jotai.Atom<ConnStatus>;
|
connStatus: jotai.Atom<ConnStatus>;
|
||||||
termWshClient: TermWshClient;
|
termWshClient: TermWshClient;
|
||||||
shellProcStatusRef: React.MutableRefObject<string>;
|
|
||||||
vdomBlockId: jotai.Atom<string>;
|
vdomBlockId: jotai.Atom<string>;
|
||||||
vdomToolbarBlockId: jotai.Atom<string>;
|
vdomToolbarBlockId: jotai.Atom<string>;
|
||||||
vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>;
|
vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>;
|
||||||
@ -64,6 +63,11 @@ class TermViewModel {
|
|||||||
termThemeNameAtom: jotai.Atom<string>;
|
termThemeNameAtom: jotai.Atom<string>;
|
||||||
noPadding: jotai.PrimitiveAtom<boolean>;
|
noPadding: jotai.PrimitiveAtom<boolean>;
|
||||||
endIconButtons: jotai.Atom<IconButtonDecl[]>;
|
endIconButtons: jotai.Atom<IconButtonDecl[]>;
|
||||||
|
shellProcFullStatus: jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
|
||||||
|
shellProcStatus: jotai.Atom<string>;
|
||||||
|
shellProcStatusUnsubFn: () => void;
|
||||||
|
isCmdController: jotai.Atom<boolean>;
|
||||||
|
isRestarting: jotai.PrimitiveAtom<boolean>;
|
||||||
|
|
||||||
constructor(blockId: string, nodeModel: BlockNodeModel) {
|
constructor(blockId: string, nodeModel: BlockNodeModel) {
|
||||||
this.viewType = "term";
|
this.viewType = "term";
|
||||||
@ -85,11 +89,15 @@ class TermViewModel {
|
|||||||
const blockData = get(this.blockAtom);
|
const blockData = get(this.blockAtom);
|
||||||
return blockData?.meta?.["term:mode"] ?? "term";
|
return blockData?.meta?.["term:mode"] ?? "term";
|
||||||
});
|
});
|
||||||
|
this.isRestarting = jotai.atom(false);
|
||||||
this.viewIcon = jotai.atom((get) => {
|
this.viewIcon = jotai.atom((get) => {
|
||||||
const termMode = get(this.termMode);
|
const termMode = get(this.termMode);
|
||||||
if (termMode == "vdom") {
|
if (termMode == "vdom") {
|
||||||
return "bolt";
|
return "bolt";
|
||||||
}
|
}
|
||||||
|
const isCmd = get(this.isCmdController);
|
||||||
|
if (isCmd) {
|
||||||
|
}
|
||||||
return "terminal";
|
return "terminal";
|
||||||
});
|
});
|
||||||
this.viewName = jotai.atom((get) => {
|
this.viewName = jotai.atom((get) => {
|
||||||
@ -99,7 +107,7 @@ class TermViewModel {
|
|||||||
return "Wave App";
|
return "Wave App";
|
||||||
}
|
}
|
||||||
if (blockData?.meta?.controller == "cmd") {
|
if (blockData?.meta?.controller == "cmd") {
|
||||||
return "Command";
|
return "";
|
||||||
}
|
}
|
||||||
return "Terminal";
|
return "Terminal";
|
||||||
});
|
});
|
||||||
@ -116,28 +124,76 @@ class TermViewModel {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} else {
|
}
|
||||||
const vdomBlockId = get(this.vdomBlockId);
|
const vdomBlockId = get(this.vdomBlockId);
|
||||||
if (vdomBlockId) {
|
const rtn = [];
|
||||||
return [
|
if (vdomBlockId) {
|
||||||
{
|
rtn.push({
|
||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "bolt",
|
icon: "bolt",
|
||||||
title: "Switch to Wave App",
|
title: "Switch to Wave App",
|
||||||
click: () => {
|
click: () => {
|
||||||
this.setTermMode("vdom");
|
this.setTermMode("vdom");
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
const isCmd = get(this.isCmdController);
|
||||||
|
if (isCmd) {
|
||||||
|
const blockMeta = get(this.blockAtom)?.meta;
|
||||||
|
let cmdText = blockMeta?.["cmd"];
|
||||||
|
let cmdArgs = blockMeta?.["cmd:args"];
|
||||||
|
if (cmdArgs != null && Array.isArray(cmdArgs) && cmdArgs.length > 0) {
|
||||||
|
cmdText += " " + cmdArgs.join(" ");
|
||||||
|
}
|
||||||
|
rtn.push({
|
||||||
|
elemtype: "text",
|
||||||
|
text: cmdText,
|
||||||
|
noGrow: true,
|
||||||
|
});
|
||||||
|
const isRestarting = get(this.isRestarting);
|
||||||
|
if (isRestarting) {
|
||||||
|
rtn.push({
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
icon: "refresh",
|
||||||
|
iconColor: "var(--success-color)",
|
||||||
|
iconSpin: true,
|
||||||
|
title: "Restarting Command",
|
||||||
|
noAction: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const fullShellProcStatus = get(this.shellProcFullStatus);
|
||||||
|
if (fullShellProcStatus?.shellprocstatus == "done") {
|
||||||
|
if (fullShellProcStatus?.shellprocexitcode == 0) {
|
||||||
|
rtn.push({
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
icon: "check",
|
||||||
|
iconColor: "var(--success-color)",
|
||||||
|
title: "Command Exited Successfully",
|
||||||
|
noAction: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
rtn.push({
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
icon: "xmark-large",
|
||||||
|
iconColor: "var(--error-color)",
|
||||||
|
title: "Exit Code: " + fullShellProcStatus?.shellprocexitcode,
|
||||||
|
noAction: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return rtn;
|
||||||
});
|
});
|
||||||
this.manageConnection = jotai.atom((get) => {
|
this.manageConnection = jotai.atom((get) => {
|
||||||
const termMode = get(this.termMode);
|
const termMode = get(this.termMode);
|
||||||
if (termMode == "vdom") {
|
if (termMode == "vdom") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const isCmd = get(this.isCmdController);
|
||||||
|
if (isCmd) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
this.termThemeNameAtom = useBlockAtom(blockId, "termthemeatom", () => {
|
this.termThemeNameAtom = useBlockAtom(blockId, "termthemeatom", () => {
|
||||||
@ -175,17 +231,60 @@ class TermViewModel {
|
|||||||
this.noPadding = jotai.atom(true);
|
this.noPadding = jotai.atom(true);
|
||||||
this.endIconButtons = jotai.atom((get) => {
|
this.endIconButtons = jotai.atom((get) => {
|
||||||
const blockData = get(this.blockAtom);
|
const blockData = get(this.blockAtom);
|
||||||
if (blockData?.meta?.["controller"] != "cmd") {
|
const shellProcStatus = get(this.shellProcStatus);
|
||||||
|
const connStatus = get(this.connStatus);
|
||||||
|
const isCmd = get(this.isCmdController);
|
||||||
|
if (blockData?.meta?.["controller"] != "cmd" && shellProcStatus != "done") {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [
|
if (connStatus?.status != "connected") {
|
||||||
{
|
return [];
|
||||||
elemtype: "iconbutton",
|
}
|
||||||
icon: "refresh",
|
let iconName: string = null;
|
||||||
click: this.forceRestartController.bind(this),
|
let title: string = null;
|
||||||
title: "Force Restart Controller",
|
const noun = isCmd ? "Command" : "Shell";
|
||||||
},
|
if (shellProcStatus == "init") {
|
||||||
];
|
iconName = "play";
|
||||||
|
title = "Click to Start " + noun;
|
||||||
|
} else if (shellProcStatus == "running") {
|
||||||
|
iconName = "refresh";
|
||||||
|
title = noun + " Running. Click to Restart";
|
||||||
|
} else if (shellProcStatus == "done") {
|
||||||
|
iconName = "refresh";
|
||||||
|
title = noun + " Exited. Click to Restart";
|
||||||
|
}
|
||||||
|
if (iconName == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const buttonDecl: IconButtonDecl = {
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
icon: iconName,
|
||||||
|
click: this.forceRestartController.bind(this),
|
||||||
|
title: title,
|
||||||
|
};
|
||||||
|
const rtn = [buttonDecl];
|
||||||
|
return rtn;
|
||||||
|
});
|
||||||
|
this.isCmdController = jotai.atom((get) => {
|
||||||
|
const controllerMetaAtom = getBlockMetaKeyAtom(this.blockId, "controller");
|
||||||
|
return get(controllerMetaAtom) == "cmd";
|
||||||
|
});
|
||||||
|
this.shellProcFullStatus = jotai.atom(null) as jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
|
||||||
|
const initialShellProcStatus = services.BlockService.GetControllerStatus(blockId);
|
||||||
|
initialShellProcStatus.then((rts) => {
|
||||||
|
this.updateShellProcStatus(rts);
|
||||||
|
});
|
||||||
|
this.shellProcStatusUnsubFn = waveEventSubscribe({
|
||||||
|
eventType: "controllerstatus",
|
||||||
|
scope: WOS.makeORef("block", blockId),
|
||||||
|
handler: (event) => {
|
||||||
|
let bcRTS: BlockControllerRuntimeStatus = event.data;
|
||||||
|
this.updateShellProcStatus(bcRTS);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.shellProcStatus = jotai.atom((get) => {
|
||||||
|
const fullStatus = get(this.shellProcFullStatus);
|
||||||
|
return fullStatus?.shellprocstatus ?? "init";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,6 +298,23 @@ class TermViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
triggerRestartAtom() {
|
||||||
|
globalStore.set(this.isRestarting, true);
|
||||||
|
setTimeout(() => {
|
||||||
|
globalStore.set(this.isRestarting, false);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateShellProcStatus(fullStatus: BlockControllerRuntimeStatus) {
|
||||||
|
globalStore.set(this.shellProcFullStatus, fullStatus);
|
||||||
|
const status = fullStatus?.shellprocstatus ?? "init";
|
||||||
|
if (status == "running") {
|
||||||
|
this.termRef.current?.setIsRunning?.(true);
|
||||||
|
} else {
|
||||||
|
this.termRef.current?.setIsRunning?.(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getVDomModel(): VDomModel {
|
getVDomModel(): VDomModel {
|
||||||
const vdomBlockId = globalStore.get(this.vdomBlockId);
|
const vdomBlockId = globalStore.get(this.vdomBlockId);
|
||||||
if (!vdomBlockId) {
|
if (!vdomBlockId) {
|
||||||
@ -225,6 +341,9 @@ class TermViewModel {
|
|||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
|
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
|
||||||
|
if (this.shellProcStatusUnsubFn) {
|
||||||
|
this.shellProcStatusUnsubFn();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
giveFocus(): boolean {
|
giveFocus(): boolean {
|
||||||
@ -284,11 +403,9 @@ class TermViewModel {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
const shellProcStatus = globalStore.get(this.shellProcStatus);
|
||||||
// restart
|
if ((shellProcStatus == "done" || shellProcStatus == "init") && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
||||||
const tabId = globalStore.get(atoms.staticTabId);
|
this.forceRestartController();
|
||||||
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, { tabid: tabId, blockid: this.blockId });
|
|
||||||
prtn.catch((e) => console.log("error controller resync (enter)", this.blockId, e));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const globalKeys = getAllGlobalKeyBindings();
|
const globalKeys = getAllGlobalKeyBindings();
|
||||||
@ -308,6 +425,10 @@ class TermViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
forceRestartController() {
|
forceRestartController() {
|
||||||
|
if (globalStore.get(this.isRestarting)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.triggerRestartAtom();
|
||||||
const termsize = {
|
const termsize = {
|
||||||
rows: this.termRef.current?.terminal?.rows,
|
rows: this.termRef.current?.terminal?.rows,
|
||||||
cols: this.termRef.current?.terminal?.cols,
|
cols: this.termRef.current?.terminal?.cols,
|
||||||
@ -387,6 +508,62 @@ class TermViewModel {
|
|||||||
label: "Force Restart Controller",
|
label: "Force Restart Controller",
|
||||||
click: this.forceRestartController.bind(this),
|
click: this.forceRestartController.bind(this),
|
||||||
});
|
});
|
||||||
|
const isClearOnStart = blockData?.meta?.["cmd:clearonstart"];
|
||||||
|
fullMenu.push({
|
||||||
|
label: "Clear Output On Restart",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "On",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: isClearOnStart,
|
||||||
|
click: () => {
|
||||||
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||||
|
oref: WOS.makeORef("block", this.blockId),
|
||||||
|
meta: { "cmd:clearonstart": true },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Off",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: !isClearOnStart,
|
||||||
|
click: () => {
|
||||||
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||||
|
oref: WOS.makeORef("block", this.blockId),
|
||||||
|
meta: { "cmd:clearonstart": false },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const runOnStart = blockData?.meta?.["cmd:runonstart"];
|
||||||
|
fullMenu.push({
|
||||||
|
label: "Run On Startup",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "On",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: runOnStart,
|
||||||
|
click: () => {
|
||||||
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||||
|
oref: WOS.makeORef("block", this.blockId),
|
||||||
|
meta: { "cmd:runonstart": true },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Off",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: !runOnStart,
|
||||||
|
click: () => {
|
||||||
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||||
|
oref: WOS.makeORef("block", this.blockId),
|
||||||
|
meta: { "cmd:runonstart": false },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
if (blockData?.meta?.["term:vdomtoolbarblockid"]) {
|
if (blockData?.meta?.["term:vdomtoolbarblockid"]) {
|
||||||
fullMenu.push({ type: "separator" });
|
fullMenu.push({ type: "separator" });
|
||||||
fullMenu.push({
|
fullMenu.push({
|
||||||
@ -538,8 +715,6 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||||
const termRef = React.useRef<TermWrap>(null);
|
const termRef = React.useRef<TermWrap>(null);
|
||||||
model.termRef = termRef;
|
model.termRef = termRef;
|
||||||
const spstatusRef = React.useRef<string>(null);
|
|
||||||
model.shellProcStatusRef = spstatusRef;
|
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
const termSettingsAtom = useSettingsPrefixAtom("term");
|
const termSettingsAtom = useSettingsPrefixAtom("term");
|
||||||
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
||||||
@ -587,6 +762,12 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
useWebGl: !termSettings?.["term:disablewebgl"],
|
useWebGl: !termSettings?.["term:disablewebgl"],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const shellProcStatus = globalStore.get(model.shellProcStatus);
|
||||||
|
if (shellProcStatus == "running") {
|
||||||
|
termWrap.setIsRunning(true);
|
||||||
|
} else if (shellProcStatus == "done") {
|
||||||
|
termWrap.setIsRunning(false);
|
||||||
|
}
|
||||||
(window as any).term = termWrap;
|
(window as any).term = termWrap;
|
||||||
termRef.current = termWrap;
|
termRef.current = termWrap;
|
||||||
const rszObs = new ResizeObserver(() => {
|
const rszObs = new ResizeObserver(() => {
|
||||||
@ -613,34 +794,6 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
termModeRef.current = termMode;
|
termModeRef.current = termMode;
|
||||||
}, [termMode]);
|
}, [termMode]);
|
||||||
|
|
||||||
// set intitial controller status, and then subscribe for updates
|
|
||||||
React.useEffect(() => {
|
|
||||||
function updateShellProcStatus(status: string) {
|
|
||||||
if (status == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
model.shellProcStatusRef.current = status;
|
|
||||||
if (status == "running") {
|
|
||||||
termRef.current?.setIsRunning(true);
|
|
||||||
} else {
|
|
||||||
termRef.current?.setIsRunning(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const initialRTStatus = services.BlockService.GetControllerStatus(blockId);
|
|
||||||
initialRTStatus.then((rts) => {
|
|
||||||
updateShellProcStatus(rts?.shellprocstatus);
|
|
||||||
});
|
|
||||||
return waveEventSubscribe({
|
|
||||||
eventType: "controllerstatus",
|
|
||||||
scope: WOS.makeORef("block", blockId),
|
|
||||||
handler: (event) => {
|
|
||||||
console.log("term waveEvent handler", event);
|
|
||||||
let bcRTS: BlockControllerRuntimeStatus = event.data;
|
|
||||||
updateShellProcStatus(bcRTS?.shellprocstatus);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
let stickerConfig = {
|
let stickerConfig = {
|
||||||
charWidth: 8,
|
charWidth: 8,
|
||||||
charHeight: 16,
|
charHeight: 16,
|
||||||
|
@ -186,14 +186,14 @@ export class WaveAiModel implements ViewModel {
|
|||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "globe",
|
icon: "globe",
|
||||||
title: "Using Remote Antropic API (" + modelName + ")",
|
title: "Using Remote Antropic API (" + modelName + ")",
|
||||||
disabled: true,
|
noAction: true,
|
||||||
});
|
});
|
||||||
} else if (isCloud) {
|
} else if (isCloud) {
|
||||||
viewTextChildren.push({
|
viewTextChildren.push({
|
||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "cloud",
|
icon: "cloud",
|
||||||
title: "Using Wave's AI Proxy (gpt-4o-mini)",
|
title: "Using Wave's AI Proxy (gpt-4o-mini)",
|
||||||
disabled: true,
|
noAction: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const baseUrl = aiOpts.baseurl ?? "OpenAI Default Endpoint";
|
const baseUrl = aiOpts.baseurl ?? "OpenAI Default Endpoint";
|
||||||
@ -203,14 +203,14 @@ export class WaveAiModel implements ViewModel {
|
|||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "location-dot",
|
icon: "location-dot",
|
||||||
title: "Using Local Model @ " + baseUrl + " (" + modelName + ")",
|
title: "Using Local Model @ " + baseUrl + " (" + modelName + ")",
|
||||||
disabled: true,
|
noAction: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
viewTextChildren.push({
|
viewTextChildren.push({
|
||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "globe",
|
icon: "globe",
|
||||||
title: "Using Remote Model @ " + baseUrl + " (" + modelName + ")",
|
title: "Using Remote Model @ " + baseUrl + " (" + modelName + ")",
|
||||||
disabled: true,
|
noAction: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
frontend/types/custom.d.ts
vendored
3
frontend/types/custom.d.ts
vendored
@ -154,11 +154,13 @@ declare global {
|
|||||||
elemtype: "iconbutton";
|
elemtype: "iconbutton";
|
||||||
icon: string | React.ReactNode;
|
icon: string | React.ReactNode;
|
||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
|
iconSpin?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
click?: (e: React.MouseEvent<any>) => void;
|
click?: (e: React.MouseEvent<any>) => void;
|
||||||
longClick?: (e: React.MouseEvent<any>) => void;
|
longClick?: (e: React.MouseEvent<any>) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
noAction?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HeaderTextButton = {
|
type HeaderTextButton = {
|
||||||
@ -173,6 +175,7 @@ declare global {
|
|||||||
text: string;
|
text: string;
|
||||||
ref?: React.MutableRefObject<HTMLDivElement>;
|
ref?: React.MutableRefObject<HTMLDivElement>;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
noGrow?: boolean;
|
||||||
onClick?: (e: React.MouseEvent<any>) => void;
|
onClick?: (e: React.MouseEvent<any>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
17
frontend/types/gotypes.d.ts
vendored
17
frontend/types/gotypes.d.ts
vendored
@ -45,7 +45,6 @@ declare global {
|
|||||||
// waveobj.Block
|
// waveobj.Block
|
||||||
type Block = WaveObj & {
|
type Block = WaveObj & {
|
||||||
parentoref?: string;
|
parentoref?: string;
|
||||||
blockdef: BlockDef;
|
|
||||||
runtimeopts?: RuntimeOpts;
|
runtimeopts?: RuntimeOpts;
|
||||||
stickers?: StickerType[];
|
stickers?: StickerType[];
|
||||||
subblockids?: string[];
|
subblockids?: string[];
|
||||||
@ -56,6 +55,7 @@ declare global {
|
|||||||
blockid: string;
|
blockid: string;
|
||||||
shellprocstatus?: string;
|
shellprocstatus?: string;
|
||||||
shellprocconnname?: string;
|
shellprocconnname?: string;
|
||||||
|
shellprocexitcode: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// waveobj.BlockDef
|
// waveobj.BlockDef
|
||||||
@ -70,6 +70,7 @@ declare global {
|
|||||||
tabid: string;
|
tabid: string;
|
||||||
workspaceid: string;
|
workspaceid: string;
|
||||||
block: Block;
|
block: Block;
|
||||||
|
files: WaveFile[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// webcmd.BlockInputWSCommand
|
// webcmd.BlockInputWSCommand
|
||||||
@ -329,9 +330,6 @@ declare global {
|
|||||||
|
|
||||||
// waveobj.FileDef
|
// waveobj.FileDef
|
||||||
type FileDef = {
|
type FileDef = {
|
||||||
filetype?: string;
|
|
||||||
path?: string;
|
|
||||||
url?: string;
|
|
||||||
content?: string;
|
content?: string;
|
||||||
meta?: {[key: string]: any};
|
meta?: {[key: string]: any};
|
||||||
};
|
};
|
||||||
@ -430,10 +428,15 @@ declare global {
|
|||||||
"cmd:login"?: boolean;
|
"cmd:login"?: boolean;
|
||||||
"cmd:runonstart"?: boolean;
|
"cmd:runonstart"?: boolean;
|
||||||
"cmd:clearonstart"?: boolean;
|
"cmd:clearonstart"?: boolean;
|
||||||
"cmd:clearonrestart"?: boolean;
|
"cmd:runonce"?: boolean;
|
||||||
|
"cmd:closeonexit"?: boolean;
|
||||||
|
"cmd:closeonexitforce"?: boolean;
|
||||||
|
"cmd:closeonexitdelay"?: number;
|
||||||
"cmd:env"?: {[key: string]: string};
|
"cmd:env"?: {[key: string]: string};
|
||||||
"cmd:cwd"?: string;
|
"cmd:cwd"?: string;
|
||||||
"cmd:nowsh"?: boolean;
|
"cmd:nowsh"?: boolean;
|
||||||
|
"cmd:args"?: string[];
|
||||||
|
"cmd:shell"?: boolean;
|
||||||
"ai:*"?: boolean;
|
"ai:*"?: boolean;
|
||||||
"ai:preset"?: string;
|
"ai:preset"?: string;
|
||||||
"ai:apitype"?: string;
|
"ai:apitype"?: string;
|
||||||
@ -446,6 +449,8 @@ declare global {
|
|||||||
"ai:maxtokens"?: number;
|
"ai:maxtokens"?: number;
|
||||||
"ai:timeoutms"?: number;
|
"ai:timeoutms"?: number;
|
||||||
"editor:*"?: boolean;
|
"editor:*"?: boolean;
|
||||||
|
"editor:minimapenabled"?: boolean;
|
||||||
|
"editor:stickyscrollenabled"?: boolean;
|
||||||
"editor:wordwrap"?: boolean;
|
"editor:wordwrap"?: boolean;
|
||||||
"graph:*"?: boolean;
|
"graph:*"?: boolean;
|
||||||
"graph:numpoints"?: number;
|
"graph:numpoints"?: number;
|
||||||
@ -603,6 +608,7 @@ declare global {
|
|||||||
"term:copyonselect"?: boolean;
|
"term:copyonselect"?: boolean;
|
||||||
"editor:minimapenabled"?: boolean;
|
"editor:minimapenabled"?: boolean;
|
||||||
"editor:stickyscrollenabled"?: boolean;
|
"editor:stickyscrollenabled"?: boolean;
|
||||||
|
"editor:wordwrap"?: boolean;
|
||||||
"web:*"?: boolean;
|
"web:*"?: boolean;
|
||||||
"web:openlinksinternally"?: boolean;
|
"web:openlinksinternally"?: boolean;
|
||||||
"web:defaulturl"?: string;
|
"web:defaulturl"?: string;
|
||||||
@ -1119,6 +1125,7 @@ declare global {
|
|||||||
icon: string;
|
icon: string;
|
||||||
color: string;
|
color: string;
|
||||||
tabids: string[];
|
tabids: string[];
|
||||||
|
pinnedtabids: string[];
|
||||||
activetabid: string;
|
activetabid: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -87,11 +87,14 @@ async function initWaveWrap(initOpts: WaveInitOpts) {
|
|||||||
async function reinitWave() {
|
async function reinitWave() {
|
||||||
console.log("Reinit Wave");
|
console.log("Reinit Wave");
|
||||||
getApi().sendLog("Reinit Wave");
|
getApi().sendLog("Reinit Wave");
|
||||||
|
|
||||||
|
// We use this hack to prevent a flicker in the tab bar when switching to a new tab. This class is set in setActiveTab in global.ts. See tab.scss for where this class is used.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.classList.remove("nohover");
|
document.body.classList.remove("nohover");
|
||||||
}, 50);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = await WOS.reloadWaveObject<Client>(WOS.makeORef("client", savedInitOpts.clientId));
|
const client = await WOS.reloadWaveObject<Client>(WOS.makeORef("client", savedInitOpts.clientId));
|
||||||
const waveWindow = await WOS.reloadWaveObject<WaveWindow>(WOS.makeORef("window", savedInitOpts.windowId));
|
const waveWindow = await WOS.reloadWaveObject<WaveWindow>(WOS.makeORef("window", savedInitOpts.windowId));
|
||||||
const ws = await WOS.reloadWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
|
const ws = await WOS.reloadWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
|
||||||
|
22
package.json
22
package.json
@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "^3.2.2",
|
"@chromatic-com/storybook": "^3.2.2",
|
||||||
"@eslint/js": "^9.15.0",
|
"@eslint/js": "^9.16.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||||
"@storybook/addon-essentials": "^8.4.6",
|
"@storybook/addon-essentials": "^8.4.6",
|
||||||
"@storybook/addon-interactions": "^8.4.6",
|
"@storybook/addon-interactions": "^8.4.6",
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"@types/papaparse": "^5",
|
"@types/papaparse": "^5",
|
||||||
"@types/pngjs": "^6.0.5",
|
"@types/pngjs": "^6.0.5",
|
||||||
"@types/prop-types": "^15",
|
"@types/prop-types": "^15",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.13",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/semver": "^7",
|
"@types/semver": "^7",
|
||||||
"@types/shell-quote": "^1",
|
"@types/shell-quote": "^1",
|
||||||
@ -56,17 +56,17 @@
|
|||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/ws": "^8",
|
"@types/ws": "^8",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||||
"@vitest/coverage-istanbul": "^2.1.6",
|
"@vitest/coverage-istanbul": "^2.1.8",
|
||||||
"electron": "^33.2.0",
|
"electron": "^33.2.0",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"electron-vite": "^2.3.0",
|
"electron-vite": "^2.3.0",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-jsdoc": "^1.3.0",
|
"prettier-plugin-jsdoc": "^1.3.0",
|
||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
"prettier-plugin-organize-imports": "^4.1.0",
|
||||||
"rollup-plugin-flow": "^1.1.1",
|
"rollup-plugin-flow": "^1.1.1",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.82.0",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"storybook": "^8.4.6",
|
"storybook": "^8.4.6",
|
||||||
"storybook-dark-mode": "^4.0.2",
|
"storybook-dark-mode": "^4.0.2",
|
||||||
@ -74,13 +74,13 @@
|
|||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"typescript-eslint": "^8.16.0",
|
"typescript-eslint": "^8.17.0",
|
||||||
"vite": "^6.0.1",
|
"vite": "^6.0.2",
|
||||||
"vite-plugin-image-optimizer": "^1.1.8",
|
"vite-plugin-image-optimizer": "^1.1.8",
|
||||||
"vite-plugin-static-copy": "^2.2.0",
|
"vite-plugin-static-copy": "^2.2.0",
|
||||||
"vite-plugin-svgr": "^4.3.0",
|
"vite-plugin-svgr": "^4.3.0",
|
||||||
"vite-tsconfig-paths": "^5.1.3",
|
"vite-tsconfig-paths": "^5.1.3",
|
||||||
"vitest": "^2.1.6"
|
"vitest": "^2.1.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react": "^0.26.28",
|
"@floating-ui/react": "^0.26.28",
|
||||||
@ -112,7 +112,7 @@
|
|||||||
"jotai": "2.9.3",
|
"jotai": "2.9.3",
|
||||||
"monaco-editor": "^0.52.0",
|
"monaco-editor": "^0.52.0",
|
||||||
"monaco-yaml": "^5.2.3",
|
"monaco-yaml": "^5.2.3",
|
||||||
"overlayscrollbars": "^2.10.0",
|
"overlayscrollbars": "^2.10.1",
|
||||||
"overlayscrollbars-react": "^0.5.6",
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
@ -133,7 +133,7 @@
|
|||||||
"remark-github-blockquote-alert": "^1.3.0",
|
"remark-github-blockquote-alert": "^1.3.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"shell-quote": "^1.8.1",
|
"shell-quote": "^1.8.2",
|
||||||
"sprintf-js": "^1.1.3",
|
"sprintf-js": "^1.1.3",
|
||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"tinycolor2": "^1.6.0",
|
"tinycolor2": "^1.6.0",
|
||||||
|
@ -20,11 +20,14 @@ import (
|
|||||||
"github.com/wavetermdev/waveterm/pkg/remote"
|
"github.com/wavetermdev/waveterm/pkg/remote"
|
||||||
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
|
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
|
||||||
"github.com/wavetermdev/waveterm/pkg/shellexec"
|
"github.com/wavetermdev/waveterm/pkg/shellexec"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/util/envutil"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
||||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wsl"
|
"github.com/wavetermdev/waveterm/pkg/wsl"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wstore"
|
"github.com/wavetermdev/waveterm/pkg/wstore"
|
||||||
@ -36,13 +39,15 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BlockFile_Term = "term" // used for main pty output
|
BlockFile_Term = "term" // used for main pty output
|
||||||
BlockFile_VDom = "vdom" // used for alt html layout
|
BlockFile_Cache = "cache:term:full" // for cached block
|
||||||
|
BlockFile_VDom = "vdom" // used for alt html layout
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Status_Running = "running"
|
Status_Running = "running"
|
||||||
Status_Done = "done"
|
Status_Done = "done"
|
||||||
|
Status_Init = "init"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -62,21 +67,23 @@ type BlockInputUnion struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BlockController struct {
|
type BlockController struct {
|
||||||
Lock *sync.Mutex
|
Lock *sync.Mutex
|
||||||
ControllerType string
|
ControllerType string
|
||||||
TabId string
|
TabId string
|
||||||
BlockId string
|
BlockId string
|
||||||
BlockDef *waveobj.BlockDef
|
BlockDef *waveobj.BlockDef
|
||||||
CreatedHtmlFile bool
|
CreatedHtmlFile bool
|
||||||
ShellProc *shellexec.ShellProc
|
ShellProc *shellexec.ShellProc
|
||||||
ShellInputCh chan *BlockInputUnion
|
ShellInputCh chan *BlockInputUnion
|
||||||
ShellProcStatus string
|
ShellProcStatus string
|
||||||
|
ShellProcExitCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlockControllerRuntimeStatus struct {
|
type BlockControllerRuntimeStatus struct {
|
||||||
BlockId string `json:"blockid"`
|
BlockId string `json:"blockid"`
|
||||||
ShellProcStatus string `json:"shellprocstatus,omitempty"`
|
ShellProcStatus string `json:"shellprocstatus,omitempty"`
|
||||||
ShellProcConnName string `json:"shellprocconnname,omitempty"`
|
ShellProcConnName string `json:"shellprocconnname,omitempty"`
|
||||||
|
ShellProcExitCode int `json:"shellprocexitcode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *BlockController) WithLock(f func()) {
|
func (bc *BlockController) WithLock(f func()) {
|
||||||
@ -93,6 +100,7 @@ func (bc *BlockController) GetRuntimeStatus() *BlockControllerRuntimeStatus {
|
|||||||
if bc.ShellProc != nil {
|
if bc.ShellProc != nil {
|
||||||
rtn.ShellProcConnName = bc.ShellProc.ConnName
|
rtn.ShellProcConnName = bc.ShellProc.ConnName
|
||||||
}
|
}
|
||||||
|
rtn.ShellProcExitCode = bc.ShellProcExitCode
|
||||||
})
|
})
|
||||||
return &rtn
|
return &rtn
|
||||||
}
|
}
|
||||||
@ -126,22 +134,29 @@ func (bc *BlockController) UpdateControllerAndSendUpdate(updateFn func() bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleTruncateBlockFile(blockId string, blockFile string) error {
|
func HandleTruncateBlockFile(blockId string) error {
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
err := filestore.WFS.WriteFile(ctx, blockId, blockFile, nil)
|
err := filestore.WFS.WriteFile(ctx, blockId, BlockFile_Term, nil)
|
||||||
if err == fs.ErrNotExist {
|
if err == fs.ErrNotExist {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error truncating blockfile: %w", err)
|
return fmt.Errorf("error truncating blockfile: %w", err)
|
||||||
}
|
}
|
||||||
|
err = filestore.WFS.DeleteFile(ctx, blockId, BlockFile_Cache)
|
||||||
|
if err == fs.ErrNotExist {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error deleting cache file (continuing): %v\n", err)
|
||||||
|
}
|
||||||
wps.Broker.Publish(wps.WaveEvent{
|
wps.Broker.Publish(wps.WaveEvent{
|
||||||
Event: wps.Event_BlockFile,
|
Event: wps.Event_BlockFile,
|
||||||
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, blockId).String()},
|
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, blockId).String()},
|
||||||
Data: &wps.WSFileEventData{
|
Data: &wps.WSFileEventData{
|
||||||
ZoneId: blockId,
|
ZoneId: blockId,
|
||||||
FileName: blockFile,
|
FileName: BlockFile_Term,
|
||||||
FileOp: wps.FileOp_Truncate,
|
FileOp: wps.FileOp_Truncate,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -174,16 +189,8 @@ func HandleAppendBlockFile(blockId string, blockFile string, data []byte) error
|
|||||||
func (bc *BlockController) resetTerminalState() {
|
func (bc *BlockController) resetTerminalState() {
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
var shouldTruncate bool
|
wfile, statErr := filestore.WFS.Stat(ctx, bc.BlockId, BlockFile_Term)
|
||||||
blockData, getBlockDataErr := wstore.DBMustGet[*waveobj.Block](ctx, bc.BlockId)
|
if statErr == fs.ErrNotExist || wfile.Size == 0 {
|
||||||
if getBlockDataErr == nil {
|
|
||||||
shouldTruncate = blockData.Meta.GetBool(waveobj.MetaKey_CmdClearOnRestart, false)
|
|
||||||
}
|
|
||||||
if shouldTruncate {
|
|
||||||
err := HandleTruncateBlockFile(bc.BlockId, BlockFile_Term)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error truncating term blockfile: %v\n", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// controller type = "shell"
|
// controller type = "shell"
|
||||||
@ -199,6 +206,66 @@ func (bc *BlockController) resetTerminalState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for "cmd" type blocks
|
||||||
|
func createCmdStrAndOpts(blockId string, blockMeta waveobj.MetaMapType) (string, *shellexec.CommandOptsType, error) {
|
||||||
|
var cmdStr string
|
||||||
|
var cmdOpts shellexec.CommandOptsType
|
||||||
|
cmdOpts.Env = make(map[string]string)
|
||||||
|
cmdStr = blockMeta.GetString(waveobj.MetaKey_Cmd, "")
|
||||||
|
if cmdStr == "" {
|
||||||
|
return "", nil, fmt.Errorf("missing cmd in block meta")
|
||||||
|
}
|
||||||
|
cmdOpts.Cwd = blockMeta.GetString(waveobj.MetaKey_CmdCwd, "")
|
||||||
|
if cmdOpts.Cwd != "" {
|
||||||
|
cwdPath, err := wavebase.ExpandHomeDir(cmdOpts.Cwd)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
cmdOpts.Cwd = cwdPath
|
||||||
|
}
|
||||||
|
useShell := blockMeta.GetBool(waveobj.MetaKey_CmdShell, true)
|
||||||
|
if !useShell {
|
||||||
|
if strings.Contains(cmdStr, " ") {
|
||||||
|
return "", nil, fmt.Errorf("cmd should not have spaces if cmd:shell is false (use cmd:args)")
|
||||||
|
}
|
||||||
|
cmdArgs := blockMeta.GetStringList(waveobj.MetaKey_CmdArgs)
|
||||||
|
// shell escape the args
|
||||||
|
for _, arg := range cmdArgs {
|
||||||
|
cmdStr = cmdStr + " " + utilfn.ShellQuote(arg, false, -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the "env" file
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancelFn()
|
||||||
|
_, envFileData, err := filestore.WFS.ReadFile(ctx, blockId, "env")
|
||||||
|
if err == fs.ErrNotExist {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("error reading command env file: %w", err)
|
||||||
|
}
|
||||||
|
if len(envFileData) > 0 {
|
||||||
|
envMap := envutil.EnvToMap(string(envFileData))
|
||||||
|
for k, v := range envMap {
|
||||||
|
cmdOpts.Env[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmdEnv := blockMeta.GetMap(waveobj.MetaKey_CmdEnv)
|
||||||
|
for k, v := range cmdEnv {
|
||||||
|
if v == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := v.(string); ok {
|
||||||
|
cmdOpts.Env[k] = v.(string)
|
||||||
|
}
|
||||||
|
if _, ok := v.(float64); ok {
|
||||||
|
cmdOpts.Env[k] = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cmdStr, &cmdOpts, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj.MetaMapType) error {
|
func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj.MetaMapType) error {
|
||||||
// create a circular blockfile for the output
|
// create a circular blockfile for the output
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
@ -220,10 +287,9 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
|||||||
// TODO better sync here (don't let two starts happen at the same times)
|
// TODO better sync here (don't let two starts happen at the same times)
|
||||||
remoteName := blockMeta.GetString(waveobj.MetaKey_Connection, "")
|
remoteName := blockMeta.GetString(waveobj.MetaKey_Connection, "")
|
||||||
var cmdStr string
|
var cmdStr string
|
||||||
cmdOpts := shellexec.CommandOptsType{
|
var cmdOpts shellexec.CommandOptsType
|
||||||
Env: make(map[string]string),
|
|
||||||
}
|
|
||||||
if bc.ControllerType == BlockController_Shell {
|
if bc.ControllerType == BlockController_Shell {
|
||||||
|
cmdOpts.Env = make(map[string]string)
|
||||||
cmdOpts.Interactive = true
|
cmdOpts.Interactive = true
|
||||||
cmdOpts.Login = true
|
cmdOpts.Login = true
|
||||||
cmdOpts.Cwd = blockMeta.GetString(waveobj.MetaKey_CmdCwd, "")
|
cmdOpts.Cwd = blockMeta.GetString(waveobj.MetaKey_CmdCwd, "")
|
||||||
@ -235,32 +301,12 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
|||||||
cmdOpts.Cwd = cwdPath
|
cmdOpts.Cwd = cwdPath
|
||||||
}
|
}
|
||||||
} else if bc.ControllerType == BlockController_Cmd {
|
} else if bc.ControllerType == BlockController_Cmd {
|
||||||
cmdStr = blockMeta.GetString(waveobj.MetaKey_Cmd, "")
|
var cmdOptsPtr *shellexec.CommandOptsType
|
||||||
if cmdStr == "" {
|
cmdStr, cmdOptsPtr, err = createCmdStrAndOpts(bc.BlockId, blockMeta)
|
||||||
return fmt.Errorf("missing cmd in block meta")
|
if err != nil {
|
||||||
}
|
return err
|
||||||
cmdOpts.Cwd = blockMeta.GetString(waveobj.MetaKey_CmdCwd, "")
|
|
||||||
if cmdOpts.Cwd != "" {
|
|
||||||
cwdPath, err := wavebase.ExpandHomeDir(cmdOpts.Cwd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cmdOpts.Cwd = cwdPath
|
|
||||||
}
|
|
||||||
cmdOpts.Interactive = blockMeta.GetBool(waveobj.MetaKey_CmdInteractive, false)
|
|
||||||
cmdOpts.Login = blockMeta.GetBool(waveobj.MetaKey_CmdLogin, false)
|
|
||||||
cmdEnv := blockMeta.GetMap(waveobj.MetaKey_CmdEnv)
|
|
||||||
for k, v := range cmdEnv {
|
|
||||||
if v == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := v.(string); ok {
|
|
||||||
cmdOpts.Env[k] = v.(string)
|
|
||||||
}
|
|
||||||
if _, ok := v.(float64); ok {
|
|
||||||
cmdOpts.Env[k] = fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
cmdOpts = *cmdOptsPtr
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("unknown controller type %q", bc.ControllerType)
|
return fmt.Errorf("unknown controller type %q", bc.ControllerType)
|
||||||
}
|
}
|
||||||
@ -352,17 +398,21 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
|||||||
wshProxy := wshutil.MakeRpcProxy()
|
wshProxy := wshutil.MakeRpcProxy()
|
||||||
wshProxy.SetRpcContext(&wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId})
|
wshProxy.SetRpcContext(&wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId})
|
||||||
wshutil.DefaultRouter.RegisterRoute(wshutil.MakeControllerRouteId(bc.BlockId), wshProxy, true)
|
wshutil.DefaultRouter.RegisterRoute(wshutil.MakeControllerRouteId(bc.BlockId), wshProxy, true)
|
||||||
ptyBuffer := wshutil.MakePtyBuffer(wshutil.WaveOSCPrefix, bc.ShellProc.Cmd, wshProxy.FromRemoteCh)
|
ptyBuffer := wshutil.MakePtyBuffer(wshutil.WaveOSCPrefix, shellProc.Cmd, wshProxy.FromRemoteCh)
|
||||||
go func() {
|
go func() {
|
||||||
// handles regular output from the pty (goes to the blockfile and xterm)
|
// handles regular output from the pty (goes to the blockfile and xterm)
|
||||||
defer panichandler.PanicHandler("blockcontroller:shellproc-pty-read-loop")
|
defer panichandler.PanicHandler("blockcontroller:shellproc-pty-read-loop")
|
||||||
defer func() {
|
defer func() {
|
||||||
log.Printf("[shellproc] pty-read loop done\n")
|
log.Printf("[shellproc] pty-read loop done\n")
|
||||||
bc.ShellProc.Close()
|
shellProc.Close()
|
||||||
bc.WithLock(func() {
|
bc.WithLock(func() {
|
||||||
// so no other events are sent
|
// so no other events are sent
|
||||||
bc.ShellInputCh = nil
|
bc.ShellInputCh = nil
|
||||||
})
|
})
|
||||||
|
shellProc.Cmd.Wait()
|
||||||
|
exitCode := shellProc.Cmd.ExitCode()
|
||||||
|
termMsg := fmt.Sprintf("\r\nprocess finished with exit code = %d\r\n\r\n", exitCode)
|
||||||
|
HandleAppendBlockFile(bc.BlockId, BlockFile_Term, []byte(termMsg))
|
||||||
// to stop the inputCh loop
|
// to stop the inputCh loop
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
close(shellInputCh) // don't use bc.ShellInputCh (it's nil)
|
close(shellInputCh) // don't use bc.ShellInputCh (it's nil)
|
||||||
@ -391,14 +441,14 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
|||||||
defer panichandler.PanicHandler("blockcontroller:shellproc-input-loop")
|
defer panichandler.PanicHandler("blockcontroller:shellproc-input-loop")
|
||||||
for ic := range shellInputCh {
|
for ic := range shellInputCh {
|
||||||
if len(ic.InputData) > 0 {
|
if len(ic.InputData) > 0 {
|
||||||
bc.ShellProc.Cmd.Write(ic.InputData)
|
shellProc.Cmd.Write(ic.InputData)
|
||||||
}
|
}
|
||||||
if ic.TermSize != nil {
|
if ic.TermSize != nil {
|
||||||
err = setTermSize(ctx, bc.BlockId, *ic.TermSize)
|
err = setTermSize(ctx, bc.BlockId, *ic.TermSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error setting pty size: %v\n", err)
|
log.Printf("error setting pty size: %v\n", err)
|
||||||
}
|
}
|
||||||
err = bc.ShellProc.Cmd.SetSize(ic.TermSize.Rows, ic.TermSize.Cols)
|
err = shellProc.Cmd.SetSize(ic.TermSize.Rows, ic.TermSize.Cols)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error setting pty size: %v\n", err)
|
log.Printf("error setting pty size: %v\n", err)
|
||||||
}
|
}
|
||||||
@ -419,24 +469,49 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
|
|||||||
go func() {
|
go func() {
|
||||||
defer panichandler.PanicHandler("blockcontroller:shellproc-wait-loop")
|
defer panichandler.PanicHandler("blockcontroller:shellproc-wait-loop")
|
||||||
// wait for the shell to finish
|
// wait for the shell to finish
|
||||||
|
var exitCode int
|
||||||
defer func() {
|
defer func() {
|
||||||
wshutil.DefaultRouter.UnregisterRoute(wshutil.MakeControllerRouteId(bc.BlockId))
|
wshutil.DefaultRouter.UnregisterRoute(wshutil.MakeControllerRouteId(bc.BlockId))
|
||||||
bc.UpdateControllerAndSendUpdate(func() bool {
|
bc.UpdateControllerAndSendUpdate(func() bool {
|
||||||
bc.ShellProcStatus = Status_Done
|
bc.ShellProcStatus = Status_Done
|
||||||
|
bc.ShellProcExitCode = exitCode
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
log.Printf("[shellproc] shell process wait loop done\n")
|
log.Printf("[shellproc] shell process wait loop done\n")
|
||||||
}()
|
}()
|
||||||
waitErr := shellProc.Cmd.Wait()
|
waitErr := shellProc.Cmd.Wait()
|
||||||
exitCode := shellexec.ExitCodeFromWaitErr(waitErr)
|
exitCode = shellProc.Cmd.ExitCode()
|
||||||
termMsg := fmt.Sprintf("\r\nprocess finished with exit code = %d\r\n\r\n", exitCode)
|
|
||||||
//HandleAppendBlockFile(bc.BlockId, BlockFile_Term, []byte("\r\n"))
|
|
||||||
HandleAppendBlockFile(bc.BlockId, BlockFile_Term, []byte(termMsg))
|
|
||||||
shellProc.SetWaitErrorAndSignalDone(waitErr)
|
shellProc.SetWaitErrorAndSignalDone(waitErr)
|
||||||
|
go checkCloseOnExit(bc.BlockId, exitCode)
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkCloseOnExit(blockId string, exitCode int) {
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||||
|
defer cancelFn()
|
||||||
|
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting block data: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
closeOnExit := blockData.Meta.GetBool(waveobj.MetaKey_CmdCloseOnExit, false)
|
||||||
|
closeOnExitForce := blockData.Meta.GetBool(waveobj.MetaKey_CmdCloseOnExitForce, false)
|
||||||
|
if !closeOnExitForce && !(closeOnExit && exitCode == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delayMs := blockData.Meta.GetFloat(waveobj.MetaKey_CmdCloseOnExitDelay, 2000)
|
||||||
|
if delayMs < 0 {
|
||||||
|
delayMs = 0
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(delayMs) * time.Millisecond)
|
||||||
|
rpcClient := wshclient.GetBareRpcClient()
|
||||||
|
err = wshclient.DeleteBlockCommand(rpcClient, wshrpc.CommandDeleteBlockData{BlockId: blockId}, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error deleting block data (close on exit): %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getBoolFromMeta(meta map[string]any, key string, def bool) bool {
|
func getBoolFromMeta(meta map[string]any, key string, def bool) bool {
|
||||||
ival, found := meta[key]
|
ival, found := meta[key]
|
||||||
if !found || ival == nil {
|
if !found || ival == nil {
|
||||||
@ -474,20 +549,35 @@ func setTermSize(ctx context.Context, blockId string, termSize waveobj.TermSize)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *BlockController) run(bdata *waveobj.Block, blockMeta map[string]any, rtOpts *waveobj.RuntimeOpts) {
|
func (bc *BlockController) run(bdata *waveobj.Block, blockMeta map[string]any, rtOpts *waveobj.RuntimeOpts, force bool) {
|
||||||
|
curStatus := bc.GetRuntimeStatus()
|
||||||
controllerName := bdata.Meta.GetString(waveobj.MetaKey_Controller, "")
|
controllerName := bdata.Meta.GetString(waveobj.MetaKey_Controller, "")
|
||||||
if controllerName != BlockController_Shell && controllerName != BlockController_Cmd {
|
if controllerName != BlockController_Shell && controllerName != BlockController_Cmd {
|
||||||
log.Printf("unknown controller %q\n", controllerName)
|
log.Printf("unknown controller %q\n", controllerName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if getBoolFromMeta(blockMeta, waveobj.MetaKey_CmdClearOnStart, false) {
|
runOnce := getBoolFromMeta(blockMeta, waveobj.MetaKey_CmdRunOnce, false)
|
||||||
err := HandleTruncateBlockFile(bc.BlockId, BlockFile_Term)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error truncating term blockfile: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runOnStart := getBoolFromMeta(blockMeta, waveobj.MetaKey_CmdRunOnStart, true)
|
runOnStart := getBoolFromMeta(blockMeta, waveobj.MetaKey_CmdRunOnStart, true)
|
||||||
if runOnStart {
|
if ((runOnStart || runOnce) && curStatus.ShellProcStatus == Status_Init) || force {
|
||||||
|
if getBoolFromMeta(blockMeta, waveobj.MetaKey_CmdClearOnStart, false) {
|
||||||
|
err := HandleTruncateBlockFile(bc.BlockId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error truncating term blockfile: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if runOnce {
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancelFn()
|
||||||
|
metaUpdate := map[string]any{
|
||||||
|
waveobj.MetaKey_CmdRunOnce: false,
|
||||||
|
waveobj.MetaKey_CmdRunOnStart: false,
|
||||||
|
}
|
||||||
|
err := wstore.UpdateObjectMeta(ctx, waveobj.MakeORef(waveobj.OType_Block, bc.BlockId), metaUpdate)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error updating block meta (in blockcontroller.run): %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer panichandler.PanicHandler("blockcontroller:run-shell-command")
|
defer panichandler.PanicHandler("blockcontroller:run-shell-command")
|
||||||
var termSize waveobj.TermSize
|
var termSize waveobj.TermSize
|
||||||
@ -579,7 +669,7 @@ func getOrCreateBlockController(tabId string, blockId string, controllerName str
|
|||||||
ControllerType: controllerName,
|
ControllerType: controllerName,
|
||||||
TabId: tabId,
|
TabId: tabId,
|
||||||
BlockId: blockId,
|
BlockId: blockId,
|
||||||
ShellProcStatus: Status_Done,
|
ShellProcStatus: Status_Init,
|
||||||
}
|
}
|
||||||
blockControllerMap[blockId] = bc
|
blockControllerMap[blockId] = bc
|
||||||
createdController = true
|
createdController = true
|
||||||
@ -587,7 +677,7 @@ func getOrCreateBlockController(tabId string, blockId string, controllerName str
|
|||||||
return bc
|
return bc
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts *waveobj.RuntimeOpts) error {
|
func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts *waveobj.RuntimeOpts, force bool) error {
|
||||||
if tabId == "" || blockId == "" {
|
if tabId == "" || blockId == "" {
|
||||||
return fmt.Errorf("invalid tabId or blockId passed to ResyncController")
|
return fmt.Errorf("invalid tabId or blockId passed to ResyncController")
|
||||||
}
|
}
|
||||||
@ -595,6 +685,9 @@ func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting block: %w", err)
|
return fmt.Errorf("error getting block: %w", err)
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
StopBlockController(blockId)
|
||||||
|
}
|
||||||
connName := blockData.Meta.GetString(waveobj.MetaKey_Connection, "")
|
connName := blockData.Meta.GetString(waveobj.MetaKey_Connection, "")
|
||||||
controllerName := blockData.Meta.GetString(waveobj.MetaKey_Controller, "")
|
controllerName := blockData.Meta.GetString(waveobj.MetaKey_Controller, "")
|
||||||
curBc := GetBlockController(blockId)
|
curBc := GetBlockController(blockId)
|
||||||
@ -619,16 +712,16 @@ func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if curBc == nil {
|
if curBc == nil {
|
||||||
return startBlockController(ctx, tabId, blockId, rtOpts)
|
return startBlockController(ctx, tabId, blockId, rtOpts, force)
|
||||||
}
|
}
|
||||||
bcStatus := curBc.GetRuntimeStatus()
|
bcStatus := curBc.GetRuntimeStatus()
|
||||||
if bcStatus.ShellProcStatus != Status_Running {
|
if bcStatus.ShellProcStatus == Status_Init || bcStatus.ShellProcStatus == Status_Done {
|
||||||
return startBlockController(ctx, tabId, blockId, rtOpts)
|
return startBlockController(ctx, tabId, blockId, rtOpts, force)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startBlockController(ctx context.Context, tabId string, blockId string, rtOpts *waveobj.RuntimeOpts) error {
|
func startBlockController(ctx context.Context, tabId string, blockId string, rtOpts *waveobj.RuntimeOpts, force bool) error {
|
||||||
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting block: %w", err)
|
return fmt.Errorf("error getting block: %w", err)
|
||||||
@ -649,8 +742,8 @@ func startBlockController(ctx context.Context, tabId string, blockId string, rtO
|
|||||||
}
|
}
|
||||||
bc := getOrCreateBlockController(tabId, blockId, controllerName)
|
bc := getOrCreateBlockController(tabId, blockId, controllerName)
|
||||||
bcStatus := bc.GetRuntimeStatus()
|
bcStatus := bc.GetRuntimeStatus()
|
||||||
if bcStatus.ShellProcStatus == Status_Done {
|
if bcStatus.ShellProcStatus == Status_Init || bcStatus.ShellProcStatus == Status_Done {
|
||||||
go bc.run(blockData, blockData.Meta, rtOpts)
|
go bc.run(blockData, blockData.Meta, rtOpts, force)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ func (svc *WindowService) CreateWindow(ctx context.Context, winSize *waveobj.Win
|
|||||||
return nil, fmt.Errorf("error getting workspace: %w", err)
|
return nil, fmt.Errorf("error getting workspace: %w", err)
|
||||||
}
|
}
|
||||||
if len(ws.TabIds) == 0 {
|
if len(ws.TabIds) == 0 {
|
||||||
_, err = wcore.CreateTab(ctx, ws.OID, "", true)
|
_, err = wcore.CreateTab(ctx, ws.OID, "", true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return window, fmt.Errorf("error creating tab: %w", err)
|
return window, fmt.Errorf("error creating tab: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package workspaceservice
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
||||||
@ -68,16 +69,16 @@ func (svg *WorkspaceService) ListWorkspaces() (waveobj.WorkspaceList, error) {
|
|||||||
|
|
||||||
func (svc *WorkspaceService) CreateTab_Meta() tsgenmeta.MethodMeta {
|
func (svc *WorkspaceService) CreateTab_Meta() tsgenmeta.MethodMeta {
|
||||||
return tsgenmeta.MethodMeta{
|
return tsgenmeta.MethodMeta{
|
||||||
ArgNames: []string{"workspaceId", "tabName", "activateTab"},
|
ArgNames: []string{"workspaceId", "tabName", "activateTab", "pinned"},
|
||||||
ReturnDesc: "tabId",
|
ReturnDesc: "tabId",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *WorkspaceService) CreateTab(workspaceId string, tabName string, activateTab bool) (string, waveobj.UpdatesRtnType, error) {
|
func (svc *WorkspaceService) CreateTab(workspaceId string, tabName string, activateTab bool, pinned bool) (string, waveobj.UpdatesRtnType, error) {
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
ctx = waveobj.ContextWithUpdates(ctx)
|
ctx = waveobj.ContextWithUpdates(ctx)
|
||||||
tabId, err := wcore.CreateTab(ctx, workspaceId, tabName, activateTab)
|
tabId, err := wcore.CreateTab(ctx, workspaceId, tabName, activateTab, pinned)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("error creating tab: %w", err)
|
return "", nil, fmt.Errorf("error creating tab: %w", err)
|
||||||
}
|
}
|
||||||
@ -93,17 +94,39 @@ func (svc *WorkspaceService) CreateTab(workspaceId string, tabName string, activ
|
|||||||
return tabId, updates, nil
|
return tabId, updates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *WorkspaceService) UpdateTabIds_Meta() tsgenmeta.MethodMeta {
|
func (svc *WorkspaceService) ChangeTabPinning_Meta() tsgenmeta.MethodMeta {
|
||||||
return tsgenmeta.MethodMeta{
|
return tsgenmeta.MethodMeta{
|
||||||
ArgNames: []string{"uiContext", "workspaceId", "tabIds"},
|
ArgNames: []string{"ctx", "workspaceId", "tabId", "pinned"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *WorkspaceService) UpdateTabIds(uiContext waveobj.UIContext, workspaceId string, tabIds []string) (waveobj.UpdatesRtnType, error) {
|
func (svc *WorkspaceService) ChangeTabPinning(ctx context.Context, workspaceId string, tabId string, pinned bool) (waveobj.UpdatesRtnType, error) {
|
||||||
|
log.Printf("ChangeTabPinning %s %s %v\n", workspaceId, tabId, pinned)
|
||||||
|
ctx = waveobj.ContextWithUpdates(ctx)
|
||||||
|
err := wcore.ChangeTabPinning(ctx, workspaceId, tabId, pinned)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error toggling tab pinning: %w", err)
|
||||||
|
}
|
||||||
|
updates := waveobj.ContextGetUpdatesRtn(ctx)
|
||||||
|
go func() {
|
||||||
|
defer panichandler.PanicHandler("WorkspaceService:ChangeTabPinning:SendUpdateEvents")
|
||||||
|
wps.Broker.SendUpdateEvents(updates)
|
||||||
|
}()
|
||||||
|
return updates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *WorkspaceService) UpdateTabIds_Meta() tsgenmeta.MethodMeta {
|
||||||
|
return tsgenmeta.MethodMeta{
|
||||||
|
ArgNames: []string{"uiContext", "workspaceId", "tabIds", "pinnedTabIds"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *WorkspaceService) UpdateTabIds(uiContext waveobj.UIContext, workspaceId string, tabIds []string, pinnedTabIds []string) (waveobj.UpdatesRtnType, error) {
|
||||||
|
log.Printf("UpdateTabIds %s %v %v\n", workspaceId, tabIds, pinnedTabIds)
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
ctx = waveobj.ContextWithUpdates(ctx)
|
ctx = waveobj.ContextWithUpdates(ctx)
|
||||||
err := wcore.UpdateWorkspaceTabIds(ctx, workspaceId, tabIds)
|
err := wcore.UpdateWorkspaceTabIds(ctx, workspaceId, tabIds, pinnedTabIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error updating workspace tab ids: %w", err)
|
return nil, fmt.Errorf("error updating workspace tab ids: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ type ConnInterface interface {
|
|||||||
KillGraceful(time.Duration)
|
KillGraceful(time.Duration)
|
||||||
Wait() error
|
Wait() error
|
||||||
Start() error
|
Start() error
|
||||||
|
ExitCode() int
|
||||||
StdinPipe() (io.WriteCloser, error)
|
StdinPipe() (io.WriteCloser, error)
|
||||||
StdoutPipe() (io.ReadCloser, error)
|
StdoutPipe() (io.ReadCloser, error)
|
||||||
StderrPipe() (io.ReadCloser, error)
|
StderrPipe() (io.ReadCloser, error)
|
||||||
@ -27,16 +29,38 @@ type ConnInterface interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CmdWrap struct {
|
type CmdWrap struct {
|
||||||
Cmd *exec.Cmd
|
Cmd *exec.Cmd
|
||||||
|
WaitOnce *sync.Once
|
||||||
|
WaitErr error
|
||||||
pty.Pty
|
pty.Pty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MakeCmdWrap(cmd *exec.Cmd, cmdPty pty.Pty) CmdWrap {
|
||||||
|
return CmdWrap{
|
||||||
|
Cmd: cmd,
|
||||||
|
WaitOnce: &sync.Once{},
|
||||||
|
Pty: cmdPty,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (cw CmdWrap) Kill() {
|
func (cw CmdWrap) Kill() {
|
||||||
cw.Cmd.Process.Kill()
|
cw.Cmd.Process.Kill()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cw CmdWrap) Wait() error {
|
func (cw CmdWrap) Wait() error {
|
||||||
return cw.Cmd.Wait()
|
cw.WaitOnce.Do(func() {
|
||||||
|
cw.WaitErr = cw.Cmd.Wait()
|
||||||
|
})
|
||||||
|
return cw.WaitErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// only valid once Wait() has returned (or you know Cmd is done)
|
||||||
|
func (cw CmdWrap) ExitCode() int {
|
||||||
|
state := cw.Cmd.ProcessState
|
||||||
|
if state == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return state.ExitCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cw CmdWrap) KillGraceful(timeout time.Duration) {
|
func (cw CmdWrap) KillGraceful(timeout time.Duration) {
|
||||||
@ -95,9 +119,21 @@ type SessionWrap struct {
|
|||||||
Session *ssh.Session
|
Session *ssh.Session
|
||||||
StartCmd string
|
StartCmd string
|
||||||
Tty pty.Tty
|
Tty pty.Tty
|
||||||
|
WaitOnce *sync.Once
|
||||||
|
WaitErr error
|
||||||
pty.Pty
|
pty.Pty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MakeSessionWrap(session *ssh.Session, startCmd string, sessionPty pty.Pty) SessionWrap {
|
||||||
|
return SessionWrap{
|
||||||
|
Session: session,
|
||||||
|
StartCmd: startCmd,
|
||||||
|
Tty: sessionPty,
|
||||||
|
WaitOnce: &sync.Once{},
|
||||||
|
Pty: sessionPty,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (sw SessionWrap) Kill() {
|
func (sw SessionWrap) Kill() {
|
||||||
sw.Tty.Close()
|
sw.Tty.Close()
|
||||||
sw.Session.Close()
|
sw.Session.Close()
|
||||||
@ -107,8 +143,19 @@ func (sw SessionWrap) KillGraceful(timeout time.Duration) {
|
|||||||
sw.Kill()
|
sw.Kill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sw SessionWrap) ExitCode() int {
|
||||||
|
waitErr := sw.WaitErr
|
||||||
|
if waitErr == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return ExitCodeFromWaitErr(waitErr)
|
||||||
|
}
|
||||||
|
|
||||||
func (sw SessionWrap) Wait() error {
|
func (sw SessionWrap) Wait() error {
|
||||||
return sw.Session.Wait()
|
sw.WaitOnce.Do(func() {
|
||||||
|
sw.WaitErr = sw.Session.Wait()
|
||||||
|
})
|
||||||
|
return sw.WaitErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sw SessionWrap) Start() error {
|
func (sw SessionWrap) Start() error {
|
||||||
|
@ -232,7 +232,8 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ShellProc{Cmd: CmdWrap{ecmd, cmdPty}, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
|
cmdWrap := MakeCmdWrap(ecmd, cmdPty)
|
||||||
|
return &ShellProc{Cmd: cmdWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) {
|
func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) {
|
||||||
@ -270,7 +271,7 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm
|
|||||||
session.Stderr = remoteStdoutWrite
|
session.Stderr = remoteStdoutWrite
|
||||||
|
|
||||||
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
|
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
|
||||||
sessionWrap := SessionWrap{session, "", pipePty, pipePty}
|
sessionWrap := MakeSessionWrap(session, "", pipePty)
|
||||||
err = session.Shell()
|
err = session.Shell()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pipePty.Close()
|
pipePty.Close()
|
||||||
@ -381,8 +382,7 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
|
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
|
||||||
|
sessionWrap := MakeSessionWrap(session, cmdCombined, pipePty)
|
||||||
sessionWrap := SessionWrap{session, cmdCombined, pipePty, pipePty}
|
|
||||||
err = sessionWrap.Start()
|
err = sessionWrap.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pipePty.Close()
|
pipePty.Close()
|
||||||
@ -469,7 +469,8 @@ func StartShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOpt
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ShellProc{Cmd: CmdWrap{ecmd, cmdPty}, CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
|
cmdWrap := MakeCmdWrap(ecmd, cmdPty)
|
||||||
|
return &ShellProc{Cmd: cmdWrap, CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunSimpleCmdInPty(ecmd *exec.Cmd, termSize waveobj.TermSize) ([]byte, error) {
|
func RunSimpleCmdInPty(ecmd *exec.Cmd, termSize waveobj.TermSize) ([]byte, error) {
|
||||||
|
@ -80,32 +80,24 @@ func GetBool(v interface{}, field string) bool {
|
|||||||
|
|
||||||
var needsQuoteRe = regexp.MustCompile(`[^\w@%:,./=+-]`)
|
var needsQuoteRe = regexp.MustCompile(`[^\w@%:,./=+-]`)
|
||||||
|
|
||||||
// minimum maxlen=6
|
// minimum maxlen=6, pass -1 for no max length
|
||||||
func ShellQuote(val string, forceQuote bool, maxLen int) string {
|
func ShellQuote(val string, forceQuote bool, maxLen int) string {
|
||||||
if maxLen < 6 {
|
if maxLen != -1 && maxLen < 6 {
|
||||||
maxLen = 6
|
maxLen = 6
|
||||||
}
|
}
|
||||||
rtn := val
|
rtn := val
|
||||||
if needsQuoteRe.MatchString(val) {
|
if needsQuoteRe.MatchString(val) {
|
||||||
rtn = "'" + strings.ReplaceAll(val, "'", `'"'"'`) + "'"
|
rtn = "'" + strings.ReplaceAll(val, "'", `'"'"'`) + "'"
|
||||||
|
} else if forceQuote {
|
||||||
|
rtn = "\"" + rtn + "\""
|
||||||
|
}
|
||||||
|
if maxLen == -1 || len(rtn) <= maxLen {
|
||||||
|
return rtn
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(rtn, "\"") || strings.HasPrefix(rtn, "'") {
|
if strings.HasPrefix(rtn, "\"") || strings.HasPrefix(rtn, "'") {
|
||||||
if len(rtn) > maxLen {
|
return rtn[0:maxLen-4] + "..." + rtn[len(rtn)-1:]
|
||||||
return rtn[0:maxLen-4] + "..." + rtn[0:1]
|
|
||||||
}
|
|
||||||
return rtn
|
|
||||||
}
|
|
||||||
if forceQuote {
|
|
||||||
if len(rtn) > maxLen-2 {
|
|
||||||
return "\"" + rtn[0:maxLen-5] + "...\""
|
|
||||||
}
|
|
||||||
return "\"" + rtn + "\""
|
|
||||||
} else {
|
|
||||||
if len(rtn) > maxLen {
|
|
||||||
return rtn[0:maxLen-3] + "..."
|
|
||||||
}
|
|
||||||
return rtn
|
|
||||||
}
|
}
|
||||||
|
return rtn[0:maxLen-3] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
func EllipsisStr(s string, maxLen int) string {
|
func EllipsisStr(s string, maxLen int) string {
|
||||||
|
@ -43,10 +43,15 @@ const (
|
|||||||
MetaKey_CmdLogin = "cmd:login"
|
MetaKey_CmdLogin = "cmd:login"
|
||||||
MetaKey_CmdRunOnStart = "cmd:runonstart"
|
MetaKey_CmdRunOnStart = "cmd:runonstart"
|
||||||
MetaKey_CmdClearOnStart = "cmd:clearonstart"
|
MetaKey_CmdClearOnStart = "cmd:clearonstart"
|
||||||
MetaKey_CmdClearOnRestart = "cmd:clearonrestart"
|
MetaKey_CmdRunOnce = "cmd:runonce"
|
||||||
|
MetaKey_CmdCloseOnExit = "cmd:closeonexit"
|
||||||
|
MetaKey_CmdCloseOnExitForce = "cmd:closeonexitforce"
|
||||||
|
MetaKey_CmdCloseOnExitDelay = "cmd:closeonexitdelay"
|
||||||
MetaKey_CmdEnv = "cmd:env"
|
MetaKey_CmdEnv = "cmd:env"
|
||||||
MetaKey_CmdCwd = "cmd:cwd"
|
MetaKey_CmdCwd = "cmd:cwd"
|
||||||
MetaKey_CmdNoWsh = "cmd:nowsh"
|
MetaKey_CmdNoWsh = "cmd:nowsh"
|
||||||
|
MetaKey_CmdArgs = "cmd:args"
|
||||||
|
MetaKey_CmdShell = "cmd:shell"
|
||||||
|
|
||||||
MetaKey_AiClear = "ai:*"
|
MetaKey_AiClear = "ai:*"
|
||||||
MetaKey_AiPresetKey = "ai:preset"
|
MetaKey_AiPresetKey = "ai:preset"
|
||||||
@ -61,6 +66,8 @@ const (
|
|||||||
MetaKey_AiTimeoutMs = "ai:timeoutms"
|
MetaKey_AiTimeoutMs = "ai:timeoutms"
|
||||||
|
|
||||||
MetaKey_EditorClear = "editor:*"
|
MetaKey_EditorClear = "editor:*"
|
||||||
|
MetaKey_EditorMinimapEnabled = "editor:minimapenabled"
|
||||||
|
MetaKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled"
|
||||||
MetaKey_EditorWordWrap = "editor:wordwrap"
|
MetaKey_EditorWordWrap = "editor:wordwrap"
|
||||||
|
|
||||||
MetaKey_GraphClear = "graph:*"
|
MetaKey_GraphClear = "graph:*"
|
||||||
|
@ -165,14 +165,15 @@ type ActiveTabUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Workspace struct {
|
type Workspace struct {
|
||||||
OID string `json:"oid"`
|
OID string `json:"oid"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
TabIds []string `json:"tabids"`
|
TabIds []string `json:"tabids"`
|
||||||
ActiveTabId string `json:"activetabid"`
|
PinnedTabIds []string `json:"pinnedtabids"`
|
||||||
Meta MetaMapType `json:"meta"`
|
ActiveTabId string `json:"activetabid"`
|
||||||
|
Meta MetaMapType `json:"meta"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Workspace) GetOType() string {
|
func (*Workspace) GetOType() string {
|
||||||
@ -230,11 +231,8 @@ func (*LayoutState) GetOType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FileDef struct {
|
type FileDef struct {
|
||||||
FileType string `json:"filetype,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
Path string `json:"path,omitempty"`
|
Meta map[string]any `json:"meta,omitempty"`
|
||||||
Url string `json:"url,omitempty"`
|
|
||||||
Content string `json:"content,omitempty"`
|
|
||||||
Meta map[string]any `json:"meta,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlockDef struct {
|
type BlockDef struct {
|
||||||
@ -279,7 +277,6 @@ type Block struct {
|
|||||||
OID string `json:"oid"`
|
OID string `json:"oid"`
|
||||||
ParentORef string `json:"parentoref,omitempty"`
|
ParentORef string `json:"parentoref,omitempty"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
BlockDef *BlockDef `json:"blockdef"`
|
|
||||||
RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"`
|
RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"`
|
||||||
Stickers []*StickerType `json:"stickers,omitempty"`
|
Stickers []*StickerType `json:"stickers,omitempty"`
|
||||||
Meta MetaMapType `json:"meta"`
|
Meta MetaMapType `json:"meta"`
|
||||||
|
@ -36,16 +36,21 @@ type MetaTSType struct {
|
|||||||
FrameIcon string `json:"frame:icon,omitempty"`
|
FrameIcon string `json:"frame:icon,omitempty"`
|
||||||
FrameText string `json:"frame:text,omitempty"`
|
FrameText string `json:"frame:text,omitempty"`
|
||||||
|
|
||||||
CmdClear bool `json:"cmd:*,omitempty"`
|
CmdClear bool `json:"cmd:*,omitempty"`
|
||||||
Cmd string `json:"cmd,omitempty"`
|
Cmd string `json:"cmd,omitempty"`
|
||||||
CmdInteractive bool `json:"cmd:interactive,omitempty"`
|
CmdInteractive bool `json:"cmd:interactive,omitempty"`
|
||||||
CmdLogin bool `json:"cmd:login,omitempty"`
|
CmdLogin bool `json:"cmd:login,omitempty"`
|
||||||
CmdRunOnStart bool `json:"cmd:runonstart,omitempty"`
|
CmdRunOnStart bool `json:"cmd:runonstart,omitempty"`
|
||||||
CmdClearOnStart bool `json:"cmd:clearonstart,omitempty"`
|
CmdClearOnStart bool `json:"cmd:clearonstart,omitempty"`
|
||||||
CmdClearOnRestart bool `json:"cmd:clearonrestart,omitempty"`
|
CmdRunOnce bool `json:"cmd:runonce,omitempty"`
|
||||||
CmdEnv map[string]string `json:"cmd:env,omitempty"`
|
CmdCloseOnExit bool `json:"cmd:closeonexit,omitempty"`
|
||||||
CmdCwd string `json:"cmd:cwd,omitempty"`
|
CmdCloseOnExitForce bool `json:"cmd:closeonexitforce,omitempty"`
|
||||||
CmdNoWsh bool `json:"cmd:nowsh,omitempty"`
|
CmdCloseOnExitDelay float64 `json:"cmd:closeonexitdelay,omitempty"`
|
||||||
|
CmdEnv map[string]string `json:"cmd:env,omitempty"`
|
||||||
|
CmdCwd string `json:"cmd:cwd,omitempty"`
|
||||||
|
CmdNoWsh bool `json:"cmd:nowsh,omitempty"`
|
||||||
|
CmdArgs []string `json:"cmd:args,omitempty"` // args for cmd (only if cmd:shell is false)
|
||||||
|
CmdShell bool `json:"cmd:shell,omitempty"` // shell expansion for cmd+args (defaults to true)
|
||||||
|
|
||||||
// AI options match settings
|
// AI options match settings
|
||||||
AiClear bool `json:"ai:*,omitempty"`
|
AiClear bool `json:"ai:*,omitempty"`
|
||||||
@ -60,8 +65,10 @@ type MetaTSType struct {
|
|||||||
AiMaxTokens float64 `json:"ai:maxtokens,omitempty"`
|
AiMaxTokens float64 `json:"ai:maxtokens,omitempty"`
|
||||||
AiTimeoutMs float64 `json:"ai:timeoutms,omitempty"`
|
AiTimeoutMs float64 `json:"ai:timeoutms,omitempty"`
|
||||||
|
|
||||||
EditorClear bool `json:"editor:*,omitempty"`
|
EditorClear bool `json:"editor:*,omitempty"`
|
||||||
EditorWordWrap bool `json:"editor:wordwrap,omitempty"`
|
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
|
||||||
|
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`
|
||||||
|
EditorWordWrap bool `json:"editor:wordwrap,omitempty"`
|
||||||
|
|
||||||
GraphClear bool `json:"graph:*,omitempty"`
|
GraphClear bool `json:"graph:*,omitempty"`
|
||||||
GraphNumPoints int `json:"graph:numpoints,omitempty"`
|
GraphNumPoints int `json:"graph:numpoints,omitempty"`
|
||||||
|
@ -30,6 +30,7 @@ const (
|
|||||||
|
|
||||||
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
|
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
|
||||||
ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled"
|
ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled"
|
||||||
|
ConfigKey_EditorWordWrap = "editor:wordwrap"
|
||||||
|
|
||||||
ConfigKey_WebClear = "web:*"
|
ConfigKey_WebClear = "web:*"
|
||||||
ConfigKey_WebOpenLinksInternally = "web:openlinksinternally"
|
ConfigKey_WebOpenLinksInternally = "web:openlinksinternally"
|
||||||
|
@ -57,6 +57,7 @@ type SettingsType struct {
|
|||||||
|
|
||||||
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
|
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
|
||||||
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`
|
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`
|
||||||
|
EditorWordWrap bool `json:"editor:wordwrap,omitempty"`
|
||||||
|
|
||||||
WebClear bool `json:"web:*,omitempty"`
|
WebClear bool `json:"web:*,omitempty"`
|
||||||
WebOpenLinksInternally bool `json:"web:openlinksinternally,omitempty"`
|
WebOpenLinksInternally bool `json:"web:openlinksinternally,omitempty"`
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/filestore"
|
||||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||||
@ -41,7 +42,6 @@ func createSubBlockObj(ctx context.Context, parentBlockId string, blockDef *wave
|
|||||||
blockData := &waveobj.Block{
|
blockData := &waveobj.Block{
|
||||||
OID: blockId,
|
OID: blockId,
|
||||||
ParentORef: waveobj.MakeORef(waveobj.OType_Block, parentBlockId).String(),
|
ParentORef: waveobj.MakeORef(waveobj.OType_Block, parentBlockId).String(),
|
||||||
BlockDef: blockDef,
|
|
||||||
RuntimeOpts: nil,
|
RuntimeOpts: nil,
|
||||||
Meta: blockDef.Meta,
|
Meta: blockDef.Meta,
|
||||||
}
|
}
|
||||||
@ -52,7 +52,19 @@ func createSubBlockObj(ctx context.Context, parentBlockId string, blockDef *wave
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
|
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (rtnBlock *waveobj.Block, rtnErr error) {
|
||||||
|
var blockCreated bool
|
||||||
|
var newBlockOID string
|
||||||
|
defer func() {
|
||||||
|
if rtnErr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if there was an error, and we created the block, clean it up since the function failed
|
||||||
|
if blockCreated && newBlockOID != "" {
|
||||||
|
deleteBlockObj(ctx, newBlockOID)
|
||||||
|
filestore.WFS.DeleteZone(ctx, newBlockOID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
if blockDef == nil {
|
if blockDef == nil {
|
||||||
return nil, fmt.Errorf("blockDef is nil")
|
return nil, fmt.Errorf("blockDef is nil")
|
||||||
}
|
}
|
||||||
@ -63,6 +75,21 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating block: %w", err)
|
return nil, fmt.Errorf("error creating block: %w", err)
|
||||||
}
|
}
|
||||||
|
blockCreated = true
|
||||||
|
newBlockOID = blockData.OID
|
||||||
|
// upload the files if present
|
||||||
|
if len(blockDef.Files) > 0 {
|
||||||
|
for fileName, fileDef := range blockDef.Files {
|
||||||
|
err := filestore.WFS.MakeFile(ctx, newBlockOID, fileName, fileDef.Meta, filestore.FileOptsType{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error making blockfile %q: %w", fileName, err)
|
||||||
|
}
|
||||||
|
err = filestore.WFS.WriteFile(ctx, newBlockOID, fileName, []byte(fileDef.Content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error writing blockfile %q: %w", fileName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer panichandler.PanicHandler("CreateBlock:telemetry")
|
defer panichandler.PanicHandler("CreateBlock:telemetry")
|
||||||
blockView := blockDef.Meta.GetString(waveobj.MetaKey_View, "")
|
blockView := blockDef.Meta.GetString(waveobj.MetaKey_View, "")
|
||||||
@ -88,7 +115,6 @@ func createBlockObj(ctx context.Context, tabId string, blockDef *waveobj.BlockDe
|
|||||||
blockData := &waveobj.Block{
|
blockData := &waveobj.Block{
|
||||||
OID: blockId,
|
OID: blockId,
|
||||||
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
|
ParentORef: waveobj.MakeORef(waveobj.OType_Tab, tabId).String(),
|
||||||
BlockDef: blockDef,
|
|
||||||
RuntimeOpts: rtOpts,
|
RuntimeOpts: rtOpts,
|
||||||
Meta: blockDef.Meta,
|
Meta: blockDef.Meta,
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wstore"
|
"github.com/wavetermdev/waveterm/pkg/wstore"
|
||||||
)
|
)
|
||||||
@ -62,7 +63,7 @@ func EnsureInitialData() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating default workspace: %w", err)
|
return fmt.Errorf("error creating default workspace: %w", err)
|
||||||
}
|
}
|
||||||
_, err = CreateTab(ctx, defaultWs.OID, "", true)
|
_, err = CreateTab(ctx, defaultWs.OID, "", true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating tab: %w", err)
|
return fmt.Errorf("error creating tab: %w", err)
|
||||||
}
|
}
|
||||||
@ -90,6 +91,5 @@ func GetClientData(ctx context.Context) (*waveobj.Client, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting client data: %w", err)
|
return nil, fmt.Errorf("error getting client data: %w", err)
|
||||||
}
|
}
|
||||||
log.Printf("clientData: %v\n", clientData)
|
|
||||||
return clientData, nil
|
return clientData, nil
|
||||||
}
|
}
|
||||||
|
@ -176,7 +176,7 @@ func CheckAndFixWindow(ctx context.Context, windowId string) *waveobj.Window {
|
|||||||
}
|
}
|
||||||
if len(ws.TabIds) == 0 {
|
if len(ws.TabIds) == 0 {
|
||||||
log.Printf("fixing workspace with no tabs %q (in checkAndFixWindow)\n", ws.OID)
|
log.Printf("fixing workspace with no tabs %q (in checkAndFixWindow)\n", ws.OID)
|
||||||
_, err = CreateTab(ctx, ws.OID, "", true)
|
_, err = CreateTab(ctx, ws.OID, "", true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error creating tab (in checkAndFixWindow): %v\n", err)
|
log.Printf("error creating tab (in checkAndFixWindow): %v\n", err)
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,12 @@ import (
|
|||||||
func CreateWorkspace(ctx context.Context, name string, icon string, color string) (*waveobj.Workspace, error) {
|
func CreateWorkspace(ctx context.Context, name string, icon string, color string) (*waveobj.Workspace, error) {
|
||||||
log.Println("CreateWorkspace")
|
log.Println("CreateWorkspace")
|
||||||
ws := &waveobj.Workspace{
|
ws := &waveobj.Workspace{
|
||||||
OID: uuid.NewString(),
|
OID: uuid.NewString(),
|
||||||
TabIds: []string{},
|
TabIds: []string{},
|
||||||
Name: name,
|
PinnedTabIds: []string{},
|
||||||
Icon: icon,
|
Name: name,
|
||||||
Color: color,
|
Icon: icon,
|
||||||
|
Color: color,
|
||||||
}
|
}
|
||||||
wstore.DBInsert(ctx, ws)
|
wstore.DBInsert(ctx, ws)
|
||||||
return ws, nil
|
return ws, nil
|
||||||
@ -37,11 +38,13 @@ func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("error getting workspace: %w", err)
|
return false, fmt.Errorf("error getting workspace: %w", err)
|
||||||
}
|
}
|
||||||
if workspace.Name != "" && workspace.Icon != "" && !force && len(workspace.TabIds) > 0 {
|
if workspace.Name != "" && workspace.Icon != "" && !force && len(workspace.TabIds) > 0 && len(workspace.PinnedTabIds) > 0 {
|
||||||
log.Printf("Ignoring DeleteWorkspace for workspace %s as it is named\n", workspaceId)
|
log.Printf("Ignoring DeleteWorkspace for workspace %s as it is named\n", workspaceId)
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
for _, tabId := range workspace.TabIds {
|
|
||||||
|
// delete all pinned and unpinned tabs
|
||||||
|
for _, tabId := range append(workspace.TabIds, workspace.PinnedTabIds...) {
|
||||||
log.Printf("deleting tab %s\n", tabId)
|
log.Printf("deleting tab %s\n", tabId)
|
||||||
_, err := DeleteTab(ctx, workspaceId, tabId, false)
|
_, err := DeleteTab(ctx, workspaceId, tabId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -60,7 +63,30 @@ func GetWorkspace(ctx context.Context, wsID string) (*waveobj.Workspace, error)
|
|||||||
return wstore.DBMustGet[*waveobj.Workspace](ctx, wsID)
|
return wstore.DBMustGet[*waveobj.Workspace](ctx, wsID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTabObj(ctx context.Context, workspaceId string, name string) (*waveobj.Tab, error) {
|
// returns tabid
|
||||||
|
func CreateTab(ctx context.Context, workspaceId string, tabName string, activateTab bool, pinned bool) (string, error) {
|
||||||
|
if tabName == "" {
|
||||||
|
ws, err := GetWorkspace(ctx, workspaceId)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
||||||
|
}
|
||||||
|
tabName = "T" + fmt.Sprint(len(ws.TabIds)+len(ws.PinnedTabIds)+1)
|
||||||
|
}
|
||||||
|
tab, err := createTabObj(ctx, workspaceId, tabName, pinned)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error creating tab: %w", err)
|
||||||
|
}
|
||||||
|
if activateTab {
|
||||||
|
err = SetActiveTab(ctx, workspaceId, tab.OID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error setting active tab: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{NewTab: 1}, "createtab")
|
||||||
|
return tab.OID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTabObj(ctx context.Context, workspaceId string, name string, pinned bool) (*waveobj.Tab, error) {
|
||||||
ws, err := GetWorkspace(ctx, workspaceId)
|
ws, err := GetWorkspace(ctx, workspaceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
||||||
@ -75,36 +101,17 @@ func createTabObj(ctx context.Context, workspaceId string, name string) (*waveob
|
|||||||
layoutState := &waveobj.LayoutState{
|
layoutState := &waveobj.LayoutState{
|
||||||
OID: layoutStateId,
|
OID: layoutStateId,
|
||||||
}
|
}
|
||||||
ws.TabIds = append(ws.TabIds, tab.OID)
|
if pinned {
|
||||||
|
ws.PinnedTabIds = append(ws.PinnedTabIds, tab.OID)
|
||||||
|
} else {
|
||||||
|
ws.TabIds = append(ws.TabIds, tab.OID)
|
||||||
|
}
|
||||||
wstore.DBInsert(ctx, tab)
|
wstore.DBInsert(ctx, tab)
|
||||||
wstore.DBInsert(ctx, layoutState)
|
wstore.DBInsert(ctx, layoutState)
|
||||||
wstore.DBUpdate(ctx, ws)
|
wstore.DBUpdate(ctx, ws)
|
||||||
return tab, nil
|
return tab, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns tabid
|
|
||||||
func CreateTab(ctx context.Context, workspaceId string, tabName string, activateTab bool) (string, error) {
|
|
||||||
if tabName == "" {
|
|
||||||
ws, err := GetWorkspace(ctx, workspaceId)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
|
||||||
}
|
|
||||||
tabName = "T" + fmt.Sprint(len(ws.TabIds)+1)
|
|
||||||
}
|
|
||||||
tab, err := createTabObj(ctx, workspaceId, tabName)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error creating tab: %w", err)
|
|
||||||
}
|
|
||||||
if activateTab {
|
|
||||||
err = SetActiveTab(ctx, workspaceId, tab.OID)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error setting active tab: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{NewTab: 1}, "createtab")
|
|
||||||
return tab.OID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must delete all blocks individually first.
|
// Must delete all blocks individually first.
|
||||||
// Also deletes LayoutState.
|
// Also deletes LayoutState.
|
||||||
// recursive: if true, will recursively close parent window, workspace, if they are empty.
|
// recursive: if true, will recursively close parent window, workspace, if they are empty.
|
||||||
@ -114,30 +121,40 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive
|
|||||||
if ws == nil {
|
if ws == nil {
|
||||||
return "", fmt.Errorf("workspace not found: %q", workspaceId)
|
return "", fmt.Errorf("workspace not found: %q", workspaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure tab is in workspace
|
||||||
|
tabIdx := utilfn.FindStringInSlice(ws.TabIds, tabId)
|
||||||
|
tabIdxPinned := utilfn.FindStringInSlice(ws.PinnedTabIds, tabId)
|
||||||
|
if tabIdx != -1 {
|
||||||
|
ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...)
|
||||||
|
} else if tabIdxPinned != -1 {
|
||||||
|
ws.PinnedTabIds = append(ws.PinnedTabIds[:tabIdxPinned], ws.PinnedTabIds[tabIdxPinned+1:]...)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("tab %s not found in workspace %s", tabId, workspaceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// close blocks (sends events + stops block controllers)
|
||||||
tab, _ := wstore.DBGet[*waveobj.Tab](ctx, tabId)
|
tab, _ := wstore.DBGet[*waveobj.Tab](ctx, tabId)
|
||||||
if tab == nil {
|
if tab == nil {
|
||||||
return "", fmt.Errorf("tab not found: %q", tabId)
|
return "", fmt.Errorf("tab not found: %q", tabId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// close blocks (sends events + stops block controllers)
|
|
||||||
for _, blockId := range tab.BlockIds {
|
for _, blockId := range tab.BlockIds {
|
||||||
err := DeleteBlock(ctx, blockId, false)
|
err := DeleteBlock(ctx, blockId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error deleting block %s: %w", blockId, err)
|
return "", fmt.Errorf("error deleting block %s: %w", blockId, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tabIdx := utilfn.FindStringInSlice(ws.TabIds, tabId)
|
|
||||||
if tabIdx == -1 {
|
// if the tab is active, determine new active tab
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...)
|
|
||||||
newActiveTabId := ws.ActiveTabId
|
newActiveTabId := ws.ActiveTabId
|
||||||
if len(ws.TabIds) > 0 {
|
if ws.ActiveTabId == tabId {
|
||||||
if ws.ActiveTabId == tabId {
|
if len(ws.TabIds) > 0 && tabIdx != -1 {
|
||||||
newActiveTabId = ws.TabIds[max(0, min(tabIdx-1, len(ws.TabIds)-1))]
|
newActiveTabId = ws.TabIds[max(0, min(tabIdx-1, len(ws.TabIds)-1))]
|
||||||
|
} else if len(ws.PinnedTabIds) > 0 {
|
||||||
|
newActiveTabId = ws.PinnedTabIds[0]
|
||||||
|
} else {
|
||||||
|
newActiveTabId = ""
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
newActiveTabId = ""
|
|
||||||
}
|
}
|
||||||
ws.ActiveTabId = newActiveTabId
|
ws.ActiveTabId = newActiveTabId
|
||||||
|
|
||||||
@ -145,7 +162,9 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive
|
|||||||
wstore.DBDelete(ctx, waveobj.OType_Tab, tabId)
|
wstore.DBDelete(ctx, waveobj.OType_Tab, tabId)
|
||||||
wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState)
|
wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState)
|
||||||
|
|
||||||
|
// if no tabs remaining, close window
|
||||||
if newActiveTabId == "" && recursive {
|
if newActiveTabId == "" && recursive {
|
||||||
|
log.Printf("no tabs remaining in workspace %s, closing window\n", workspaceId)
|
||||||
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
|
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newActiveTabId, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
|
return newActiveTabId, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
|
||||||
@ -159,7 +178,7 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
|
func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
|
||||||
if tabId != "" {
|
if tabId != "" && workspaceId != "" {
|
||||||
workspace, err := GetWorkspace(ctx, workspaceId)
|
workspace, err := GetWorkspace(ctx, workspaceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
return fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
||||||
@ -174,6 +193,30 @@ func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ChangeTabPinning(ctx context.Context, workspaceId string, tabId string, pinned bool) error {
|
||||||
|
if tabId != "" && workspaceId != "" {
|
||||||
|
workspace, err := GetWorkspace(ctx, workspaceId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
||||||
|
}
|
||||||
|
if pinned && utilfn.FindStringInSlice(workspace.PinnedTabIds, tabId) == -1 {
|
||||||
|
if utilfn.FindStringInSlice(workspace.TabIds, tabId) == -1 {
|
||||||
|
return fmt.Errorf("tab %s not found in workspace %s", tabId, workspaceId)
|
||||||
|
}
|
||||||
|
workspace.TabIds = utilfn.RemoveElemFromSlice(workspace.TabIds, tabId)
|
||||||
|
workspace.PinnedTabIds = append(workspace.PinnedTabIds, tabId)
|
||||||
|
} else if !pinned && utilfn.FindStringInSlice(workspace.PinnedTabIds, tabId) != -1 {
|
||||||
|
if utilfn.FindStringInSlice(workspace.PinnedTabIds, tabId) == -1 {
|
||||||
|
return fmt.Errorf("tab %s not found in workspace %s", tabId, workspaceId)
|
||||||
|
}
|
||||||
|
workspace.PinnedTabIds = utilfn.RemoveElemFromSlice(workspace.PinnedTabIds, tabId)
|
||||||
|
workspace.TabIds = append([]string{tabId}, workspace.TabIds...)
|
||||||
|
}
|
||||||
|
wstore.DBUpdate(ctx, workspace)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func SendActiveTabUpdate(ctx context.Context, workspaceId string, newActiveTabId string) {
|
func SendActiveTabUpdate(ctx context.Context, workspaceId string, newActiveTabId string) {
|
||||||
eventbus.SendEventToElectron(eventbus.WSEventType{
|
eventbus.SendEventToElectron(eventbus.WSEventType{
|
||||||
EventType: eventbus.WSEvent_ElectronUpdateActiveTab,
|
EventType: eventbus.WSEvent_ElectronUpdateActiveTab,
|
||||||
@ -181,12 +224,13 @@ func SendActiveTabUpdate(ctx context.Context, workspaceId string, newActiveTabId
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error {
|
func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string, pinnedTabIds []string) error {
|
||||||
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
||||||
if ws == nil {
|
if ws == nil {
|
||||||
return fmt.Errorf("workspace not found: %q", workspaceId)
|
return fmt.Errorf("workspace not found: %q", workspaceId)
|
||||||
}
|
}
|
||||||
ws.TabIds = tabIds
|
ws.TabIds = tabIds
|
||||||
|
ws.PinnedTabIds = pinnedTabIds
|
||||||
wstore.DBUpdate(ctx, ws)
|
wstore.DBUpdate(ctx, ws)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -528,10 +528,11 @@ type CommandWebSelectorData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BlockInfoData struct {
|
type BlockInfoData struct {
|
||||||
BlockId string `json:"blockid"`
|
BlockId string `json:"blockid"`
|
||||||
TabId string `json:"tabid"`
|
TabId string `json:"tabid"`
|
||||||
WorkspaceId string `json:"workspaceid"`
|
WorkspaceId string `json:"workspaceid"`
|
||||||
Block *waveobj.Block `json:"block"`
|
Block *waveobj.Block `json:"block"`
|
||||||
|
Files []*filestore.WaveFile `json:"files"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WaveNotificationOptions struct {
|
type WaveNotificationOptions struct {
|
||||||
|
@ -231,10 +231,7 @@ func (ws *WshServer) ControllerStopCommand(ctx context.Context, blockId string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WshServer) ControllerResyncCommand(ctx context.Context, data wshrpc.CommandControllerResyncData) error {
|
func (ws *WshServer) ControllerResyncCommand(ctx context.Context, data wshrpc.CommandControllerResyncData) error {
|
||||||
if data.ForceRestart {
|
return blockcontroller.ResyncController(ctx, data.TabId, data.BlockId, data.RtOpts, data.ForceRestart)
|
||||||
blockcontroller.StopBlockController(data.BlockId)
|
|
||||||
}
|
|
||||||
return blockcontroller.ResyncController(ctx, data.TabId, data.BlockId, data.RtOpts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WshServer) ControllerInputCommand(ctx context.Context, data wshrpc.CommandBlockInputData) error {
|
func (ws *WshServer) ControllerInputCommand(ctx context.Context, data wshrpc.CommandBlockInputData) error {
|
||||||
@ -701,11 +698,16 @@ func (ws *WshServer) BlockInfoCommand(ctx context.Context, blockId string) (*wsh
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error finding window for tab: %w", err)
|
return nil, fmt.Errorf("error finding window for tab: %w", err)
|
||||||
}
|
}
|
||||||
|
fileList, err := filestore.WFS.ListFiles(ctx, blockId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error listing blockfiles: %w", err)
|
||||||
|
}
|
||||||
return &wshrpc.BlockInfoData{
|
return &wshrpc.BlockInfoData{
|
||||||
BlockId: blockId,
|
BlockId: blockId,
|
||||||
TabId: tabId,
|
TabId: tabId,
|
||||||
WorkspaceId: workspaceId,
|
WorkspaceId: workspaceId,
|
||||||
Block: blockData,
|
Block: blockData,
|
||||||
|
Files: fileList,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +46,10 @@ func (wc *WslCmd) GetProcessState() *os.ProcessState {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wc *WslCmd) ExitCode() int {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
func (c *WslCmd) SetStdin(stdin io.Reader) {
|
func (c *WslCmd) SetStdin(stdin io.Reader) {
|
||||||
c.Stdin = stdin
|
c.Stdin = stdin
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,13 @@ func (c *WslCmd) Wait() (err error) {
|
|||||||
}
|
}
|
||||||
return c.waitErr
|
return c.waitErr
|
||||||
}
|
}
|
||||||
|
func (c *WslCmd) ExitCode() int {
|
||||||
|
state := c.c.ProcessState
|
||||||
|
if state == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return state.ExitCode()
|
||||||
|
}
|
||||||
func (c *WslCmd) GetProcess() *os.Process {
|
func (c *WslCmd) GetProcess() *os.Process {
|
||||||
return c.c.Process
|
return c.c.Process
|
||||||
}
|
}
|
||||||
|
@ -350,12 +350,28 @@ func DBFindTabForBlockId(ctx context.Context, blockId string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DBFindWorkspaceForTabId(ctx context.Context, tabId string) (string, error) {
|
func DBFindWorkspaceForTabId(ctx context.Context, tabId string) (string, error) {
|
||||||
|
log.Printf("DBFindWorkspaceForTabId tabId: %s\n", tabId)
|
||||||
return WithTxRtn(ctx, func(tx *TxWrap) (string, error) {
|
return WithTxRtn(ctx, func(tx *TxWrap) (string, error) {
|
||||||
query := `
|
query := `
|
||||||
|
WITH variable(value) AS (
|
||||||
|
SELECT ?
|
||||||
|
)
|
||||||
SELECT w.oid
|
SELECT w.oid
|
||||||
FROM db_workspace w, json_each(data->'tabids') je
|
FROM db_workspace w, variable
|
||||||
WHERE je.value = ?`
|
WHERE EXISTS (
|
||||||
return tx.GetString(query, tabId), nil
|
SELECT 1
|
||||||
|
FROM json_each(w.data, '$.tabids') AS je
|
||||||
|
WHERE je.value = variable.value
|
||||||
|
)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM json_each(w.data, '$.pinnedtabids') AS je
|
||||||
|
WHERE je.value = variable.value
|
||||||
|
);
|
||||||
|
`
|
||||||
|
wsId := tx.GetString(query, tabId)
|
||||||
|
log.Printf("DBFindWorkspaceForTabId wsId: %s\n", wsId)
|
||||||
|
return wsId, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user