diff --git a/design/build-number-status-bar.pen b/design/build-number-status-bar.pen new file mode 100644 index 00000000..ea0475d8 --- /dev/null +++ b/design/build-number-status-bar.pen @@ -0,0 +1,96 @@ +{ + "children": [ + { + "type": "frame", + "id": "build_number_status_bar", + "name": "Build Number — Status Bar", + "x": 0, + "y": 0, + "width": 1200, + "height": 36, + "fill": "$--background", + "layout": "horizontal", + "gap": 12, + "padding": [ + 0, + 16 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "status_vault", + "content": "vault-name", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12 + }, + { + "type": "text", + "id": "status_sep1", + "content": "|", + "fill": "$--border", + "fontFamily": "Inter", + "fontSize": 12 + }, + { + "type": "text", + "id": "status_build", + "content": "b223", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12 + }, + { + "type": "text", + "id": "status_note", + "content": "← dynamic build number from package.json, replacing hardcoded v0.4.2", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "fontStyle": "italic" + } + ] + }, + { + "type": "frame", + "id": "build_number_fallback", + "name": "Build Number — Fallback (no build info)", + "x": 0, + "y": 60, + "width": 400, + "height": 36, + "fill": "$--background", + "layout": "horizontal", + "gap": 12, + "padding": [ + 0, + 16 + ], + "theme": { + "Mode": "Light" + }, + "children": [ + { + "type": "text", + "id": "fallback_build", + "content": "b?", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 12 + }, + { + "type": "text", + "id": "fallback_note", + "content": "← shows b? when build number unavailable", + "fill": "$--muted-foreground", + "fontFamily": "Inter", + "fontSize": 11, + "fontStyle": "italic" + } + ] + } + ] +} \ No newline at end of file diff --git a/src-tauri/build.rs b/src-tauri/build.rs index d860e1e6..18915739 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,14 @@ fn main() { + let count = std::process::Command::new("git") + .args(["rev-list", "--count", "HEAD"]) + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "DEV".to_string()); + + println!("cargo:rustc-env=BUILD_NUMBER={}", count); + tauri_build::build() } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index be074436..143c2fb6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -112,6 +112,14 @@ fn git_commit(vault_path: String, message: String) -> Result { git::git_commit(&vault_path, &message) } +#[tauri::command] +fn get_build_number() -> String { + { + let n = std::env::var("BUILD_NUMBER").unwrap_or_else(|_| "0".to_string()); + format!("b{}", n) + } +} + #[tauri::command] fn get_last_commit_info(vault_path: String) -> Result, String> { let vault_path = expand_tilde(&vault_path); @@ -435,6 +443,16 @@ mod tests { let result = expand_tilde("/home/~user/path"); assert_eq!(result, "/home/~user/path"); } + + #[test] + fn get_build_number_returns_prefixed_value() { + let result = get_build_number(); + assert!( + result.starts_with('b'), + "expected 'b' prefix, got: {}", + result + ); + } } #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -498,6 +516,7 @@ pub fn run() { get_file_diff, get_file_diff_at_commit, git_commit, + get_build_number, get_last_commit_info, git_pull, git_push, diff --git a/src/App.tsx b/src/App.tsx index 68021935..cabef7d5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,6 +29,7 @@ import { useUpdater } from './hooks/useUpdater' import { useNavigationHistory } from './hooks/useNavigationHistory' import { useAutoSync } from './hooks/useAutoSync' import { useZoom } from './hooks/useZoom' +import { useBuildNumber } from './hooks/useBuildNumber' import { useOnboarding } from './hooks/useOnboarding' import { useThemeManager } from './hooks/useThemeManager' import { UpdateBanner } from './components/UpdateBanner' @@ -260,6 +261,7 @@ function App() { const { setViewMode, sidebarVisible, noteListVisible } = useViewMode() const zoom = useZoom() + const buildNumber = useBuildNumber() const commands = useAppCommands({ activeTabPath: notes.activeTabPath, activeTabPathRef: notes.activeTabPathRef, @@ -384,7 +386,7 @@ function App() { - setSelection({ kind: 'filter', filter: 'changes' })} hasGitHub={!!settings.github_token} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} lastCommitInfo={autoSync.lastCommitInfo} onTriggerSync={autoSync.triggerSync} zoomLevel={zoom.zoomLevel} onZoomReset={zoom.zoomReset} /> + setSelection({ kind: 'filter', filter: 'changes' })} hasGitHub={!!settings.github_token} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} lastCommitInfo={autoSync.lastCommitInfo} onTriggerSync={autoSync.triggerSync} zoomLevel={zoom.zoomLevel} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} /> setToastMessage(null)} /> diff --git a/src/components/StatusBar.test.tsx b/src/components/StatusBar.test.tsx index 4ce455d5..e2a4ce54 100644 --- a/src/components/StatusBar.test.tsx +++ b/src/components/StatusBar.test.tsx @@ -25,9 +25,14 @@ describe('StatusBar', () => { expect(screen.getByText('9,200 notes')).toBeInTheDocument() }) - it('displays version info', () => { + it('displays build number when provided', () => { + render() + expect(screen.getByText('b223')).toBeInTheDocument() + }) + + it('displays fallback build number when not provided', () => { render() - expect(screen.getByText('v0.4.2')).toBeInTheDocument() + expect(screen.getByText('b?')).toBeInTheDocument() }) it('does not display branch name', () => { diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 16e8bb0f..7f50b531 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -26,6 +26,7 @@ interface StatusBarProps { onTriggerSync?: () => void zoomLevel?: number onZoomReset?: () => void + buildNumber?: string } function VaultMenuItem({ vault, isActive, onSelect }: { vault: VaultOption; isActive: boolean; onSelect: () => void }) { @@ -155,7 +156,7 @@ function CommitBadge({ info }: { info: LastCommitInfo }) { ) } -export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onSwitchVault, onOpenSettings, onOpenLocalFolder, onConnectGitHub, onClickPending, hasGitHub, syncStatus = 'idle', lastSyncTime = null, conflictCount = 0, lastCommitInfo, onTriggerSync, zoomLevel = 100, onZoomReset }: StatusBarProps) { +export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onSwitchVault, onOpenSettings, onOpenLocalFolder, onConnectGitHub, onClickPending, hasGitHub, syncStatus = 'idle', lastSyncTime = null, conflictCount = 0, lastCommitInfo, onTriggerSync, zoomLevel = 100, onZoomReset, buildNumber }: StatusBarProps) { // Force re-render every 30s to keep relative time label fresh const [, setTick] = useState(0) useEffect(() => { @@ -171,7 +172,7 @@ export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onS
| - v0.4.2 + {buildNumber ?? 'b?'} | ({ invoke: vi.fn() })) +vi.mock('../mock-tauri', () => ({ + isTauri: () => false, + mockInvoke: vi.fn().mockResolvedValue('b223'), +})) + +beforeEach(() => { vi.clearAllMocks() }) + +describe('useBuildNumber', () => { + it('returns build number from mock invoke', async () => { + const { result } = renderHook(() => useBuildNumber()) + await waitFor(() => expect(result.current).toBe('b223')) + }) + + it('returns fallback on error', async () => { + const { mockInvoke } = await import('../mock-tauri') + vi.mocked(mockInvoke).mockRejectedValueOnce(new Error('fail')) + const { result } = renderHook(() => useBuildNumber()) + await waitFor(() => expect(result.current).toBe('b?')) + }) +}) diff --git a/src/hooks/useBuildNumber.ts b/src/hooks/useBuildNumber.ts new file mode 100644 index 00000000..5bc6fc12 --- /dev/null +++ b/src/hooks/useBuildNumber.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'react' +import { invoke } from '@tauri-apps/api/core' +import { isTauri, mockInvoke } from '../mock-tauri' + +function tauriCall(cmd: string): Promise { + return isTauri() ? invoke(cmd) : mockInvoke(cmd) +} + +export function useBuildNumber(): string | undefined { + const [buildNumber, setBuildNumber] = useState() + + useEffect(() => { + tauriCall('get_build_number').then(setBuildNumber).catch(() => { + setBuildNumber('b?') + }) + }, []) + + return buildNumber +} diff --git a/src/mock-tauri/mock-handlers.ts b/src/mock-tauri/mock-handlers.ts index 94c5ca01..14ebb42d 100644 --- a/src/mock-tauri/mock-handlers.ts +++ b/src/mock-tauri/mock-handlers.ts @@ -162,6 +162,7 @@ export const mockHandlers: Record any> = { mockSavedSinceCommit.clear() return `[main abc1234] ${args.message}\n ${count} files changed` }, + get_build_number: () => 'bDEV', get_last_commit_info: (): LastCommitInfo => ({ shortHash: 'a1b2c3d', commitUrl: 'https://github.com/lucaong/laputa-vault/commit/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0' }), git_pull: (): GitPullResult => ({ status: 'up_to_date', message: 'Already up to date', updatedFiles: [], conflictFiles: [] }), git_push: () => 'Everything up-to-date',