diff --git a/internal/app/workspace.go b/internal/app/workspace.go new file mode 100644 index 0000000..3b5be4b --- /dev/null +++ b/internal/app/workspace.go @@ -0,0 +1,66 @@ +package app + +import ( + "encoding/json" + + "github.com/vstockwell/wraith/internal/settings" +) + +type WorkspaceSnapshot struct { + Tabs []WorkspaceTab `json:"tabs"` + SidebarWidth int `json:"sidebarWidth"` + SidebarMode string `json:"sidebarMode"` + ActiveTab int `json:"activeTab"` +} + +type WorkspaceTab struct { + ConnectionID int64 `json:"connectionId"` + Protocol string `json:"protocol"` + Position int `json:"position"` +} + +type WorkspaceService struct { + settings *settings.SettingsService +} + +func NewWorkspaceService(s *settings.SettingsService) *WorkspaceService { + return &WorkspaceService{settings: s} +} + +// Save serializes the workspace snapshot to settings +func (w *WorkspaceService) Save(snapshot *WorkspaceSnapshot) error { + data, err := json.Marshal(snapshot) + if err != nil { + return err + } + return w.settings.Set("workspace_snapshot", string(data)) +} + +// Load reads the last saved workspace snapshot +func (w *WorkspaceService) Load() (*WorkspaceSnapshot, error) { + data, err := w.settings.Get("workspace_snapshot") + if err != nil || data == "" { + return nil, nil // no saved workspace + } + var snapshot WorkspaceSnapshot + if err := json.Unmarshal([]byte(data), &snapshot); err != nil { + return nil, err + } + return &snapshot, nil +} + +// MarkCleanShutdown saves a flag indicating clean exit +func (w *WorkspaceService) MarkCleanShutdown() error { + return w.settings.Set("clean_shutdown", "true") +} + +// WasCleanShutdown checks if last exit was clean +func (w *WorkspaceService) WasCleanShutdown() bool { + val, _ := w.settings.Get("clean_shutdown") + return val == "true" +} + +// ClearCleanShutdown removes the clean shutdown flag (called on startup) +func (w *WorkspaceService) ClearCleanShutdown() error { + return w.settings.Delete("clean_shutdown") +} diff --git a/internal/app/workspace_test.go b/internal/app/workspace_test.go new file mode 100644 index 0000000..2f6906f --- /dev/null +++ b/internal/app/workspace_test.go @@ -0,0 +1,74 @@ +package app + +import ( + "path/filepath" + "testing" + + "github.com/vstockwell/wraith/internal/db" + "github.com/vstockwell/wraith/internal/settings" +) + +func setupWorkspaceService(t *testing.T) *WorkspaceService { + t.Helper() + d, err := db.Open(filepath.Join(t.TempDir(), "test.db")) + if err != nil { + t.Fatal(err) + } + if err := db.Migrate(d); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { d.Close() }) + return NewWorkspaceService(settings.NewSettingsService(d)) +} + +func TestSaveAndLoadWorkspace(t *testing.T) { + svc := setupWorkspaceService(t) + snapshot := &WorkspaceSnapshot{ + Tabs: []WorkspaceTab{ + {ConnectionID: 1, Protocol: "ssh", Position: 0}, + {ConnectionID: 5, Protocol: "rdp", Position: 1}, + }, + SidebarWidth: 240, + SidebarMode: "connections", + ActiveTab: 0, + } + if err := svc.Save(snapshot); err != nil { + t.Fatal(err) + } + loaded, err := svc.Load() + if err != nil { + t.Fatal(err) + } + if len(loaded.Tabs) != 2 { + t.Errorf("len(Tabs) = %d, want 2", len(loaded.Tabs)) + } + if loaded.SidebarWidth != 240 { + t.Errorf("SidebarWidth = %d, want 240", loaded.SidebarWidth) + } +} + +func TestLoadEmptyWorkspace(t *testing.T) { + svc := setupWorkspaceService(t) + loaded, err := svc.Load() + if err != nil { + t.Fatal(err) + } + if loaded != nil { + t.Error("should return nil for empty workspace") + } +} + +func TestCleanShutdownFlag(t *testing.T) { + svc := setupWorkspaceService(t) + if svc.WasCleanShutdown() { + t.Error("should not be clean initially") + } + svc.MarkCleanShutdown() + if !svc.WasCleanShutdown() { + t.Error("should be clean after marking") + } + svc.ClearCleanShutdown() + if svc.WasCleanShutdown() { + t.Error("should not be clean after clearing") + } +}