waveterm/pkg/wcore/workspace.go
Evan Simkowitz f368a61c70
Fix close active tab behavior (#1367)
This fixes a bug where closing the active tab would clean up the closed
tab view before switching to an un-closed tab view, putting the window
into an unrecoverable state. This also moves around some of the CloseTab
logic so that it's more standardized and reduces unnecessary
frontend-backend comms and DB writes
2024-12-02 12:59:03 -08:00

250 lines
6.8 KiB
Go

package wcore
import (
"context"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wstore"
)
func CreateWorkspace(ctx context.Context, name string, icon string, color string) (*waveobj.Workspace, error) {
log.Println("CreateWorkspace")
ws := &waveobj.Workspace{
OID: uuid.NewString(),
TabIds: []string{},
Name: name,
Icon: icon,
Color: color,
}
wstore.DBInsert(ctx, ws)
return ws, nil
}
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 {
log.Printf("Ignoring DeleteWorkspace for workspace %s as it is named\n", workspaceId)
return false, nil
}
for _, tabId := range workspace.TabIds {
log.Printf("deleting tab %s\n", tabId)
_, err := DeleteTab(ctx, workspaceId, tabId)
if err != nil {
return false, fmt.Errorf("error closing tab: %w", err)
}
}
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)
return true, nil
}
func GetWorkspace(ctx context.Context, wsID string) (*waveobj.Workspace, error) {
return wstore.DBMustGet[*waveobj.Workspace](ctx, wsID)
}
func createTabObj(ctx context.Context, workspaceId string, name string) (*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,
}
ws.TabIds = append(ws.TabIds, tab.OID)
wstore.DBInsert(ctx, tab)
wstore.DBInsert(ctx, layoutState)
wstore.DBUpdate(ctx, ws)
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.
// Also deletes LayoutState.
// Returns new active tab id, error.
func DeleteTab(ctx context.Context, workspaceId string, tabId string) (string, error) {
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
if ws == nil {
return "", fmt.Errorf("workspace not found: %q", workspaceId)
}
tab, _ := wstore.DBGet[*waveobj.Tab](ctx, tabId)
if tab == nil {
return "", fmt.Errorf("tab not found: %q", tabId)
}
// close blocks (sends events + stops block controllers)
for _, blockId := range tab.BlockIds {
err := DeleteBlock(ctx, blockId)
if err != nil {
return "", fmt.Errorf("error deleting block %s: %w", blockId, err)
}
}
tabIdx := utilfn.FindStringInSlice(ws.TabIds, tabId)
if tabIdx == -1 {
return "", nil
}
ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...)
newActiveTabId := ws.ActiveTabId
if len(ws.TabIds) > 0 {
if ws.ActiveTabId == tabId {
newActiveTabId = ws.TabIds[max(0, min(tabIdx-1, len(ws.TabIds)-1))]
}
} else {
newActiveTabId = ""
}
ws.ActiveTabId = newActiveTabId
wstore.DBUpdate(ctx, ws)
wstore.DBDelete(ctx, waveobj.OType_Tab, tabId)
wstore.DBDelete(ctx, waveobj.OType_LayoutState, tab.LayoutState)
return newActiveTabId, nil
}
func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error {
if tabId != "" {
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 UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []string) error {
ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId)
if ws == nil {
return fmt.Errorf("workspace not found: %q", workspaceId)
}
ws.TabIds = tabIds
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
}