feat: workspace snapshot persistence — save/load layout + clean shutdown detection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
325cebbd01
commit
ae50bef795
66
internal/app/workspace.go
Normal file
66
internal/app/workspace.go
Normal file
@ -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")
|
||||
}
|
||||
74
internal/app/workspace_test.go
Normal file
74
internal/app/workspace_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user