mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
cf578b1d89
Fixes a bug where if you deleted all but one character in the workspace name, you couldn't delete the final character. To fix this, I have made the workspace editor save a separate entry from the backend. The backend will also only update its DB value and notify the frontend if something was actually edited. If you delete all the characters in the name and don't put anything new in, though, the name will be whatever the last character you had was, since the name of a saved workspace cannot be empty.
435 lines
13 KiB
Go
435 lines
13 KiB
Go
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package wcore
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/wavetermdev/waveterm/pkg/eventbus"
|
|
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
|
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
|
"github.com/wavetermdev/waveterm/pkg/wps"
|
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
|
"github.com/wavetermdev/waveterm/pkg/wstore"
|
|
)
|
|
|
|
var WorkspaceColors = [...]string{
|
|
"#58C142", // Green (accent)
|
|
"#00FFDB", // Teal
|
|
"#429DFF", // Blue
|
|
"#BF55EC", // Purple
|
|
"#FF453A", // Red
|
|
"#FF9500", // Orange
|
|
"#FFE900", // Yellow
|
|
}
|
|
|
|
var WorkspaceIcons = [...]string{
|
|
"custom@wave-logo-solid",
|
|
"triangle",
|
|
"star",
|
|
"heart",
|
|
"bolt",
|
|
"solid@cloud",
|
|
"moon",
|
|
"layer-group",
|
|
"rocket",
|
|
"flask",
|
|
"paperclip",
|
|
"chart-line",
|
|
"graduation-cap",
|
|
"mug-hot",
|
|
}
|
|
|
|
func CreateWorkspace(ctx context.Context, name string, icon string, color string, applyDefaults bool, isInitialLaunch bool) (*waveobj.Workspace, error) {
|
|
ws := &waveobj.Workspace{
|
|
OID: uuid.NewString(),
|
|
TabIds: []string{},
|
|
PinnedTabIds: []string{},
|
|
Name: "",
|
|
Icon: "",
|
|
Color: "",
|
|
}
|
|
err := wstore.DBInsert(ctx, ws)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error inserting workspace: %w", err)
|
|
}
|
|
_, err = CreateTab(ctx, ws.OID, "", true, false, isInitialLaunch)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating tab: %w", err)
|
|
}
|
|
|
|
wps.Broker.Publish(wps.WaveEvent{
|
|
Event: wps.Event_WorkspaceUpdate})
|
|
|
|
ws, _, err = UpdateWorkspace(ctx, ws.OID, name, icon, color, applyDefaults)
|
|
return ws, err
|
|
}
|
|
|
|
// Returns updated workspace, whether it was updated, error.
|
|
func UpdateWorkspace(ctx context.Context, workspaceId string, name string, icon string, color string, applyDefaults bool) (*waveobj.Workspace, bool, error) {
|
|
ws, err := GetWorkspace(ctx, workspaceId)
|
|
updated := false
|
|
if err != nil {
|
|
return nil, updated, fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
|
}
|
|
if name != "" {
|
|
ws.Name = name
|
|
updated = true
|
|
} else if applyDefaults && ws.Name == "" {
|
|
ws.Name = fmt.Sprintf("New Workspace (%s)", ws.OID[0:5])
|
|
updated = true
|
|
}
|
|
if icon != "" {
|
|
ws.Icon = icon
|
|
updated = true
|
|
} else if applyDefaults && ws.Icon == "" {
|
|
ws.Icon = WorkspaceIcons[0]
|
|
updated = true
|
|
}
|
|
if color != "" {
|
|
ws.Color = color
|
|
updated = true
|
|
} else if applyDefaults && ws.Color == "" {
|
|
wsList, err := ListWorkspaces(ctx)
|
|
if err != nil {
|
|
log.Printf("error listing workspaces: %v", err)
|
|
wsList = waveobj.WorkspaceList{}
|
|
}
|
|
ws.Color = WorkspaceColors[len(wsList)%len(WorkspaceColors)]
|
|
updated = true
|
|
}
|
|
if updated {
|
|
wstore.DBUpdate(ctx, ws)
|
|
}
|
|
return ws, updated, nil
|
|
}
|
|
|
|
// If force is true, it will delete even if workspace is named.
|
|
// If workspace is empty, it will be deleted, even if it is named.
|
|
// Returns true if workspace was deleted, false if it was not deleted.
|
|
func DeleteWorkspace(ctx context.Context, workspaceId string, force bool) (bool, error) {
|
|
log.Printf("DeleteWorkspace %s\n", workspaceId)
|
|
workspace, err := wstore.DBMustGet[*waveobj.Workspace](ctx, workspaceId)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error getting workspace: %w", err)
|
|
}
|
|
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)
|
|
return false, nil
|
|
}
|
|
|
|
// delete all pinned and unpinned tabs
|
|
for _, tabId := range append(workspace.TabIds, workspace.PinnedTabIds...) {
|
|
log.Printf("deleting tab %s\n", tabId)
|
|
_, err := DeleteTab(ctx, workspaceId, tabId, false)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error closing tab: %w", err)
|
|
}
|
|
}
|
|
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
|
|
err = wstore.DBDelete(ctx, waveobj.OType_Workspace, workspaceId)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error deleting workspace: %w", err)
|
|
}
|
|
log.Printf("deleted workspace %s\n", workspaceId)
|
|
wps.Broker.Publish(wps.WaveEvent{
|
|
Event: wps.Event_WorkspaceUpdate})
|
|
if windowId != "" {
|
|
err = CloseWindow(ctx, windowId, false)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error closing window: %w", err)
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func GetWorkspace(ctx context.Context, wsID string) (*waveobj.Workspace, error) {
|
|
return wstore.DBMustGet[*waveobj.Workspace](ctx, wsID)
|
|
}
|
|
|
|
func getTabPresetMeta() (waveobj.MetaMapType, error) {
|
|
settings := wconfig.GetWatcher().GetFullConfig()
|
|
tabPreset := settings.Settings.TabPreset
|
|
if tabPreset == "" {
|
|
return nil, nil
|
|
}
|
|
presetMeta := settings.Presets[tabPreset]
|
|
return presetMeta, nil
|
|
}
|
|
|
|
// returns tabid
|
|
func CreateTab(ctx context.Context, workspaceId string, tabName string, activateTab bool, pinned bool, isInitialLaunch 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)
|
|
}
|
|
|
|
// The initial tab for the initial launch should be pinned
|
|
tab, err := createTabObj(ctx, workspaceId, tabName, pinned || isInitialLaunch)
|
|
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)
|
|
}
|
|
}
|
|
|
|
// No need to apply an initial layout for the initial launch, since the starter layout will get applied after TOS modal dismissal
|
|
if !isInitialLaunch {
|
|
err = ApplyPortableLayout(ctx, tab.OID, GetNewTabLayout())
|
|
if err != nil {
|
|
return tab.OID, fmt.Errorf("error applying new tab layout: %w", err)
|
|
}
|
|
presetMeta, presetErr := getTabPresetMeta()
|
|
if presetErr != nil {
|
|
log.Printf("error getting tab preset meta: %v\n", presetErr)
|
|
} else if presetMeta != nil && len(presetMeta) > 0 {
|
|
tabORef := waveobj.ORefFromWaveObj(tab)
|
|
wstore.UpdateObjectMeta(ctx, *tabORef, presetMeta, true)
|
|
}
|
|
}
|
|
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)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
|
}
|
|
layoutStateId := uuid.NewString()
|
|
tab := &waveobj.Tab{
|
|
OID: uuid.NewString(),
|
|
Name: name,
|
|
BlockIds: []string{},
|
|
LayoutState: layoutStateId,
|
|
}
|
|
layoutState := &waveobj.LayoutState{
|
|
OID: layoutStateId,
|
|
}
|
|
if pinned {
|
|
ws.PinnedTabIds = append(ws.PinnedTabIds, tab.OID)
|
|
} else {
|
|
ws.TabIds = append(ws.TabIds, tab.OID)
|
|
}
|
|
wstore.DBInsert(ctx, tab)
|
|
wstore.DBInsert(ctx, layoutState)
|
|
wstore.DBUpdate(ctx, ws)
|
|
return tab, nil
|
|
}
|
|
|
|
// Must delete all blocks individually first.
|
|
// Also deletes LayoutState.
|
|
// recursive: if true, will recursively close parent window, workspace, if they are empty.
|
|
// Returns new active tab id, error.
|
|
func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive bool) (string, error) {
|
|
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
|
if ws == nil {
|
|
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)
|
|
if tab == nil {
|
|
return "", fmt.Errorf("tab not found: %q", tabId)
|
|
}
|
|
for _, blockId := range tab.BlockIds {
|
|
err := DeleteBlock(ctx, blockId, false)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error deleting block %s: %w", blockId, err)
|
|
}
|
|
}
|
|
|
|
// if the tab is active, determine new active tab
|
|
newActiveTabId := ws.ActiveTabId
|
|
if ws.ActiveTabId == tabId {
|
|
if len(ws.TabIds) > 0 && tabIdx != -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 = ""
|
|
}
|
|
}
|
|
ws.ActiveTabId = newActiveTabId
|
|
|
|
wstore.DBUpdate(ctx, ws)
|
|
wstore.DBDelete(ctx, waveobj.OType_Tab, tabId)
|
|
wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState)
|
|
|
|
// if no tabs remaining, close window
|
|
if recursive && newActiveTabId == "" {
|
|
log.Printf("no tabs remaining in workspace %s, closing window\n", workspaceId)
|
|
windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId)
|
|
if err != nil {
|
|
return newActiveTabId, fmt.Errorf("unable to find window for workspace id %v: %w", workspaceId, err)
|
|
}
|
|
err = CloseWindow(ctx, windowId, false)
|
|
if err != nil {
|
|
return newActiveTabId, err
|
|
}
|
|
}
|
|
return newActiveTabId, nil
|
|
}
|
|
|
|
func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
|
|
if tabId != "" && workspaceId != "" {
|
|
workspace, err := GetWorkspace(ctx, workspaceId)
|
|
if err != nil {
|
|
return fmt.Errorf("workspace %s not found: %w", workspaceId, err)
|
|
}
|
|
tab, _ := wstore.DBGet[*waveobj.Tab](ctx, tabId)
|
|
if tab == nil {
|
|
return fmt.Errorf("tab not found: %q", tabId)
|
|
}
|
|
workspace.ActiveTabId = tabId
|
|
wstore.DBUpdate(ctx, workspace)
|
|
}
|
|
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) {
|
|
eventbus.SendEventToElectron(eventbus.WSEventType{
|
|
EventType: eventbus.WSEvent_ElectronUpdateActiveTab,
|
|
Data: &waveobj.ActiveTabUpdate{WorkspaceId: workspaceId, NewActiveTabId: newActiveTabId},
|
|
})
|
|
}
|
|
|
|
func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string, pinnedTabIds []string) error {
|
|
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
|
if ws == nil {
|
|
return fmt.Errorf("workspace not found: %q", workspaceId)
|
|
}
|
|
ws.TabIds = tabIds
|
|
ws.PinnedTabIds = pinnedTabIds
|
|
wstore.DBUpdate(ctx, ws)
|
|
return nil
|
|
}
|
|
|
|
func ListWorkspaces(ctx context.Context) (waveobj.WorkspaceList, error) {
|
|
workspaces, err := wstore.DBGetAllObjsByType[*waveobj.Workspace](ctx, waveobj.OType_Workspace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Println("got workspaces")
|
|
|
|
windows, err := wstore.DBGetAllObjsByType[*waveobj.Window](ctx, waveobj.OType_Window)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
workspaceToWindow := make(map[string]string)
|
|
for _, window := range windows {
|
|
workspaceToWindow[window.WorkspaceId] = window.OID
|
|
}
|
|
|
|
var wl waveobj.WorkspaceList
|
|
for _, workspace := range workspaces {
|
|
if workspace.Name == "" || workspace.Icon == "" || workspace.Color == "" {
|
|
continue
|
|
}
|
|
windowId, ok := workspaceToWindow[workspace.OID]
|
|
if !ok {
|
|
windowId = ""
|
|
}
|
|
wl = append(wl, &waveobj.WorkspaceListEntry{
|
|
WorkspaceId: workspace.OID,
|
|
WindowId: windowId,
|
|
})
|
|
}
|
|
return wl, nil
|
|
}
|
|
|
|
func SetIcon(workspaceId string, icon string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
ws, e := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if ws == nil {
|
|
return fmt.Errorf("workspace not found: %q", workspaceId)
|
|
}
|
|
ws.Icon = icon
|
|
wstore.DBUpdate(ctx, ws)
|
|
return nil
|
|
}
|
|
|
|
func SetColor(workspaceId string, color string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
ws, e := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if ws == nil {
|
|
return fmt.Errorf("workspace not found: %q", workspaceId)
|
|
}
|
|
ws.Color = color
|
|
wstore.DBUpdate(ctx, ws)
|
|
return nil
|
|
}
|
|
|
|
func SetName(workspaceId string, name string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
ws, e := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if ws == nil {
|
|
return fmt.Errorf("workspace not found: %q", workspaceId)
|
|
}
|
|
ws.Name = name
|
|
wstore.DBUpdate(ctx, ws)
|
|
return nil
|
|
}
|