feat: show dynamic build number in status bar (bNNN format) (#163)

* feat: show dynamic build number in status bar (bNNN format)

Replace hardcoded v0.4.2 with a dynamic build number derived from
git rev-list --count HEAD at compile time. The count is embedded via
build.rs and exposed through a get_build_number Tauri command.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* design: add build-number-status-bar wireframes (status bar with dynamic b-number)

* fix: use runtime env var for BUILD_NUMBER (compile-time env! not available in CI)

* style: rustfmt format get_build_number

---------

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Luca Rossi
2026-03-02 01:50:41 +01:00
committed by GitHub
parent 36a421d42a
commit 1a18de2340
9 changed files with 184 additions and 5 deletions

View File

@@ -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"
}
]
}
]
}

View File

@@ -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()
}

View File

@@ -112,6 +112,14 @@ fn git_commit(vault_path: String, message: String) -> Result<String, String> {
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<Option<LastCommitInfo>, 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,

View File

@@ -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() {
</div>
</div>
<UpdateBanner status={updateStatus} actions={updateActions} />
<StatusBar noteCount={vault.entries.length} modifiedCount={vault.modifiedFiles.length} vaultPath={vaultSwitcher.vaultPath} vaults={vaultSwitcher.allVaults} onSwitchVault={vaultSwitcher.switchVault} onOpenSettings={dialogs.openSettings} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onConnectGitHub={dialogs.openGitHubVault} onClickPending={() => 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} />
<StatusBar noteCount={vault.entries.length} modifiedCount={vault.modifiedFiles.length} vaultPath={vaultSwitcher.vaultPath} vaults={vaultSwitcher.allVaults} onSwitchVault={vaultSwitcher.switchVault} onOpenSettings={dialogs.openSettings} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onConnectGitHub={dialogs.openGitHubVault} onClickPending={() => 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} />
<Toast message={toastMessage} onDismiss={() => setToastMessage(null)} />
<QuickOpenPalette open={dialogs.showQuickOpen} entries={vault.entries} onSelect={notes.handleSelectNote} onClose={dialogs.closeQuickOpen} />
<CommandPalette open={dialogs.showCommandPalette} commands={commands} onClose={dialogs.closeCommandPalette} />

View File

@@ -25,9 +25,14 @@ describe('StatusBar', () => {
expect(screen.getByText('9,200 notes')).toBeInTheDocument()
})
it('displays version info', () => {
it('displays build number when provided', () => {
render(<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} buildNumber="b223" />)
expect(screen.getByText('b223')).toBeInTheDocument()
})
it('displays fallback build number when not provided', () => {
render(<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} />)
expect(screen.getByText('v0.4.2')).toBeInTheDocument()
expect(screen.getByText('b?')).toBeInTheDocument()
})
it('does not display branch name', () => {

View File

@@ -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
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<VaultMenu vaults={vaults} vaultPath={vaultPath} onSwitchVault={onSwitchVault} onOpenLocalFolder={onOpenLocalFolder} onConnectGitHub={onConnectGitHub} hasGitHub={hasGitHub} />
<span style={SEP_STYLE}>|</span>
<span style={ICON_STYLE}><Package size={13} />v0.4.2</span>
<span style={ICON_STYLE} data-testid="status-build-number"><Package size={13} />{buildNumber ?? 'b?'}</span>
<span style={SEP_STYLE}>|</span>
<span
role="button"

View File

@@ -0,0 +1,25 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { useBuildNumber } from './useBuildNumber'
vi.mock('@tauri-apps/api/core', () => ({ 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?'))
})
})

View File

@@ -0,0 +1,19 @@
import { useState, useEffect } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { isTauri, mockInvoke } from '../mock-tauri'
function tauriCall<T>(cmd: string): Promise<T> {
return isTauri() ? invoke<T>(cmd) : mockInvoke<T>(cmd)
}
export function useBuildNumber(): string | undefined {
const [buildNumber, setBuildNumber] = useState<string>()
useEffect(() => {
tauriCall<string>('get_build_number').then(setBuildNumber).catch(() => {
setBuildNumber('b?')
})
}, [])
return buildNumber
}

View File

@@ -162,6 +162,7 @@ export const mockHandlers: Record<string, (args: any) => 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',