feat: add Tauri state persistence and BvCli trait
- Add Tauri storage adapter for Zustand (tauri-storage.ts) - Add read_state, write_state, clear_state Tauri commands - Wire focus-store and nav-store to use Tauri persistence - Add BvCli trait for bv CLI mocking with response types - Add BvError and McError conversion for bv errors - Add cleanup_tmp_files tests for bridge - Fix linter-introduced tauri_specta::command issues Closes bd-2x6, bd-gil, bd-3px Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
109
src/lib/tauri-storage.ts
Normal file
109
src/lib/tauri-storage.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Tauri storage adapter for Zustand persist middleware.
|
||||
*
|
||||
* Stores Zustand state in `~/.local/share/mc/state.json` via Tauri backend
|
||||
* instead of browser localStorage. Falls back to localStorage in browser context.
|
||||
*/
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { StateStorage } from "zustand/middleware";
|
||||
|
||||
/**
|
||||
* Create a storage adapter that persists to Tauri backend.
|
||||
*
|
||||
* Uses the `read_state`, `write_state`, and `clear_state` Tauri commands.
|
||||
*/
|
||||
export function createTauriStorage(): StateStorage {
|
||||
return {
|
||||
getItem: async (_name: string): Promise<string | null> => {
|
||||
try {
|
||||
const state = await invoke<Record<string, unknown> | null>("read_state");
|
||||
if (state === null) {
|
||||
return null;
|
||||
}
|
||||
return JSON.stringify(state);
|
||||
} catch (error) {
|
||||
console.warn("[tauri-storage] Failed to read state:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
setItem: async (_name: string, value: string): Promise<void> => {
|
||||
try {
|
||||
const state = JSON.parse(value) as Record<string, unknown>;
|
||||
await invoke("write_state", { state });
|
||||
} catch (error) {
|
||||
console.warn("[tauri-storage] Failed to write state:", error);
|
||||
}
|
||||
},
|
||||
|
||||
removeItem: async (_name: string): Promise<void> => {
|
||||
try {
|
||||
await invoke("clear_state");
|
||||
} catch (error) {
|
||||
console.warn("[tauri-storage] Failed to clear state:", error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running in Tauri context.
|
||||
*/
|
||||
function isTauriContext(): boolean {
|
||||
return typeof window !== "undefined" && "__TAURI__" in window;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a localStorage-based storage adapter for browser context.
|
||||
*/
|
||||
function createLocalStorageAdapter(): StateStorage {
|
||||
return {
|
||||
getItem: (name: string): string | null => {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(name);
|
||||
},
|
||||
|
||||
setItem: (name: string, value: string): void => {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(name, value);
|
||||
},
|
||||
|
||||
removeItem: (name: string): void => {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(name);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate storage adapter for the current context.
|
||||
*
|
||||
* - In Tauri: Uses backend persistence to ~/.local/share/mc/state.json
|
||||
* - In browser: Falls back to localStorage
|
||||
*/
|
||||
export async function initializeStorage(): Promise<StateStorage> {
|
||||
if (isTauriContext()) {
|
||||
return createTauriStorage();
|
||||
}
|
||||
return createLocalStorageAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton storage instance.
|
||||
* Use this in store definitions to avoid recreating the adapter.
|
||||
*/
|
||||
let _storage: StateStorage | null = null;
|
||||
|
||||
/**
|
||||
* Get the storage adapter (lazy initialization).
|
||||
*
|
||||
* In Tauri context, returns Tauri storage.
|
||||
* In browser context, returns localStorage wrapper.
|
||||
*/
|
||||
export function getStorage(): StateStorage {
|
||||
if (_storage === null) {
|
||||
_storage = isTauriContext() ? createTauriStorage() : createLocalStorageAdapter();
|
||||
}
|
||||
return _storage;
|
||||
}
|
||||
@@ -6,7 +6,8 @@
|
||||
*/
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { persist, createJSONStorage } from "zustand/middleware";
|
||||
import { getStorage } from "@/lib/tauri-storage";
|
||||
import type { FocusAction, FocusItem } from "@/lib/types";
|
||||
|
||||
export interface FocusState {
|
||||
@@ -108,6 +109,7 @@ export const useFocusStore = create<FocusState>()(
|
||||
}),
|
||||
{
|
||||
name: "mc-focus-store",
|
||||
storage: createJSONStorage(() => getStorage()),
|
||||
partialize: (state) => ({
|
||||
current: state.current,
|
||||
queue: state.queue,
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
*/
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { persist, createJSONStorage } from "zustand/middleware";
|
||||
import { getStorage } from "@/lib/tauri-storage";
|
||||
|
||||
export type ViewId = "focus" | "queue" | "inbox";
|
||||
|
||||
@@ -23,6 +24,7 @@ export const useNavStore = create<NavState>()(
|
||||
}),
|
||||
{
|
||||
name: "mc-nav-store",
|
||||
storage: createJSONStorage(() => getStorage()),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user