Skip to content

Commit fa70785

Browse files
synleclaude
andcommitted
feat: pre-warm cache on startup, 5-min TTL, DEV build tag in red
1. Pre-warm sidecar cache on startup (background thread) so first popup open is instant instead of waiting 8s for sidecar calls 2. Increase cache TTL from 2 to 5 minutes 3. Dev build version shows "DEV - MM/DD/YYYY HH:MM" in red in the header (works in both dark and light mode) 4. Make DjDisplay, into_monitor, merge_with_configs public for cache pre-warming from lib.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 88b2d7a commit fa70785

8 files changed

Lines changed: 139 additions & 58 deletions

File tree

src-tauri/build.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ fn expose_app_version() {
4949
let app_version = if is_release {
5050
version.to_string()
5151
} else {
52-
// Get short commit SHA for dev/beta builds
53-
let short_sha = Command::new("git")
54-
.args(["rev-parse", "--short", "HEAD"])
55-
.output()
56-
.ok()
57-
.and_then(|o| String::from_utf8(o.stdout).ok())
58-
.map(|s| s.trim().to_string())
59-
.unwrap_or_else(|| "unknown".to_string());
60-
format!("{version} [beta - {short_sha}]")
52+
// Build timestamp for dev builds: MM/DD/YYYY HH:MM
53+
let secs = SystemTime::now()
54+
.duration_since(SystemTime::UNIX_EPOCH)
55+
.unwrap()
56+
.as_secs();
57+
let days = secs / 86400;
58+
let (y, m, d) = civil_from_days(days as i64);
59+
let day_secs = secs % 86400;
60+
let hh = day_secs / 3600;
61+
let mm = (day_secs % 3600) / 60;
62+
format!("{version} [DEV - {m:02}/{d:02}/{y:04} {hh:02}:{mm:02}]")
6163
};
6264

6365
println!("cargo:rustc-env=APP_VERSION={app_version}");

src-tauri/src/display.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub struct Monitor {
2222

2323
/// Response from the display-dj HTTP server's /get_all and /list endpoints.
2424
#[derive(Deserialize, Debug)]
25-
struct DjDisplay {
25+
pub struct DjDisplay {
2626
id: String,
2727
name: String,
2828
display_type: String,
@@ -33,7 +33,7 @@ struct DjDisplay {
3333
impl DjDisplay {
3434
/// Converts a sidecar API response into the app's Monitor struct,
3535
/// computing the composite UID and defaulting brightness to 50 if unknown.
36-
fn into_monitor(self) -> Monitor {
36+
pub fn into_monitor(self) -> Monitor {
3737
let is_built_in = self.display_type == "builtin";
3838
let uid = format!("{}::{}", self.id, self.name);
3939
Monitor {
@@ -121,7 +121,7 @@ async fn set_all_monitors_contrast(value: u32) -> Result<(), String> {
121121

122122
/// Applies saved metadata (custom labels, hidden state) to detected monitors
123123
/// and sorts them by the user's configured sort order.
124-
fn merge_with_configs(
124+
pub fn merge_with_configs(
125125
monitors: Vec<Monitor>,
126126
configs: &[crate::config::MonitorMetadata],
127127
) -> Vec<Monitor> {

src-tauri/src/lib.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,40 @@ pub fn run() {
514514
}
515515
// Fetch initial dark mode and volume state for tray icon indicators
516516
fetch_initial_tray_state(&startup_handle, port);
517+
// Pre-warm the sidecar cache so the first popup open is instant.
518+
// Uses the same endpoints as fetch_all_state but blocking.
519+
{
520+
let base = format!("http://127.0.0.1:{}", port);
521+
let state = startup_handle.state::<AppState>();
522+
523+
// Cache monitors
524+
if let Ok(resp) = reqwest::blocking::get(format!("{}/get_all", base)) {
525+
if let Ok(displays) = resp.json::<Vec<display::DjDisplay>>() {
526+
let monitors: Vec<display::Monitor> = displays.into_iter().map(|d| d.into_monitor()).collect();
527+
let prefs = state.preferences.lock().unwrap();
528+
let merged = display::merge_with_configs(monitors, &prefs.monitor_configs);
529+
state.sidecar_cache.set_monitors(merged);
530+
}
531+
}
532+
// Cache dark mode
533+
if let Ok(resp) = reqwest::blocking::get(format!("{}/theme", base)) {
534+
if let Ok(text) = resp.text() {
535+
state.sidecar_cache.set_dark_mode(text.contains("dark"));
536+
}
537+
}
538+
// Cache volume
539+
if let Ok(resp) = reqwest::blocking::get(format!("{}/get_volume", base)) {
540+
if let Ok(text) = resp.text() {
541+
if let Some(v) = text.split("volume\":")
542+
.nth(1)
543+
.and_then(|s| s.trim().trim_end_matches('}').trim().parse::<u32>().ok())
544+
{
545+
state.sidecar_cache.set_volume(v);
546+
}
547+
}
548+
}
549+
log::info!("sidecar cache pre-warmed on startup");
550+
}
517551
// Resume wallpaper slideshow if it was enabled before shutdown
518552
let wp_state = startup_handle.state::<AppState>();
519553
wallpaper::resume_slideshow_if_enabled(&wp_state);

src-tauri/src/sidecar_cache.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::sync::Mutex;
1010
use std::time::{Duration, Instant};
1111

1212
/// Cache TTL: entries older than this are considered stale.
13-
const CACHE_TTL: Duration = Duration::from_secs(120); // 2 minutes
13+
const CACHE_TTL: Duration = Duration::from_secs(300); // 5 minutes
1414

1515
/// A single cached value with a timestamp.
1616
struct CacheEntry<T> {

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/nicoverbruggen/tauri-v2-docs/refs/heads/main/schemas/config.schema.json",
33
"productName": "Display DJ",
4-
"version": "6.3.18",
4+
"version": "6.3.19",
55
"identifier": "com.synle.display-dj",
66
"build": {
77
"beforeDevCommand": "npm run dev",

src/App.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ body,
8282
font-weight: 400;
8383
}
8484

85+
.header-dev-tag {
86+
color: #e74c3c;
87+
font-weight: 600;
88+
}
89+
8590
.header-toggle {
8691
background: none;
8792
border: none;

src/App.test.tsx

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ beforeEach(() => {
1010
mockInvoke.mockReset();
1111
mockInvoke.mockImplementation((cmd: string) => {
1212
switch (cmd) {
13+
case 'fetch_all_state':
14+
return Promise.resolve({
15+
monitors: [
16+
{
17+
id: 'builtin-0',
18+
uid: 'builtin-0::Built-in Display',
19+
name: 'Built-in Display',
20+
originalName: 'Built-in Display',
21+
brightness: 50,
22+
supportsBrightness: true,
23+
isBuiltIn: true,
24+
},
25+
],
26+
isDark: false,
27+
volume: 50,
28+
});
1329
case 'get_monitors':
1430
return Promise.resolve([
1531
{
@@ -92,9 +108,9 @@ describe('App smoke test', () => {
92108
it('fetches initial data on mount', async () => {
93109
render(<App />);
94110
await waitFor(() => {
95-
expect(mockInvoke).toHaveBeenCalledWith('get_monitors');
96-
expect(mockInvoke).toHaveBeenCalledWith('get_dark_mode');
97-
expect(mockInvoke).toHaveBeenCalledWith('get_volume');
111+
expect(mockInvoke).toHaveBeenCalledWith('fetch_all_state');
112+
expect(mockInvoke).toHaveBeenCalledWith('get_preferences');
113+
expect(mockInvoke).toHaveBeenCalledWith('get_keep_awake');
98114
expect(mockInvoke).toHaveBeenCalledWith('get_app_version');
99115
});
100116
});
@@ -187,38 +203,45 @@ describe('App smoke test', () => {
187203

188204
it('renders with multiple monitors without JS errors', async () => {
189205
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
206+
const multiMonitors = [
207+
{
208+
id: 'builtin-0',
209+
uid: 'builtin-0::Built-in Display',
210+
name: 'Built-in Display',
211+
originalName: 'Built-in Display',
212+
brightness: 100,
213+
supportsBrightness: true,
214+
isBuiltIn: true,
215+
},
216+
{
217+
id: '1',
218+
uid: '1::Dell U2723QE',
219+
name: 'Dell U2723QE',
220+
originalName: 'Dell U2723QE',
221+
brightness: 80,
222+
supportsBrightness: true,
223+
isBuiltIn: false,
224+
},
225+
{
226+
id: '2',
227+
uid: '2::LG 27UK850',
228+
name: '',
229+
originalName: 'LG 27UK850',
230+
brightness: 60,
231+
supportsBrightness: true,
232+
isBuiltIn: false,
233+
},
234+
];
190235
mockInvoke.mockImplementation((cmd: string) => {
191236
switch (cmd) {
237+
case 'fetch_all_state':
238+
return Promise.resolve({
239+
monitors: multiMonitors,
240+
isDark: true,
241+
volume: 75,
242+
});
192243
case 'get_monitors':
193-
return Promise.resolve([
194-
{
195-
id: 'builtin-0',
196-
uid: 'builtin-0::Built-in Display',
197-
name: 'Built-in Display',
198-
originalName: 'Built-in Display',
199-
brightness: 100,
200-
supportsBrightness: true,
201-
isBuiltIn: true,
202-
},
203-
{
204-
id: '1',
205-
uid: '1::Dell U2723QE',
206-
name: 'Dell U2723QE',
207-
originalName: 'Dell U2723QE',
208-
brightness: 80,
209-
supportsBrightness: true,
210-
isBuiltIn: false,
211-
},
212-
{
213-
id: '2',
214-
uid: '2::LG 27UK850',
215-
name: '',
216-
originalName: 'LG 27UK850',
217-
brightness: 60,
218-
supportsBrightness: true,
219-
isBuiltIn: false,
220-
},
221-
]);
244+
return Promise.resolve(multiMonitors);
222245
case 'get_dark_mode':
223246
return Promise.resolve(true);
224247
case 'get_volume':
@@ -279,20 +302,27 @@ describe('App smoke test', () => {
279302
});
280303

281304
it('calls set_brightness with API id (not uid)', async () => {
305+
const singleMonitor = [
306+
{
307+
id: '1',
308+
uid: '1::Dell U2723QE',
309+
name: 'Dell U2723QE',
310+
originalName: 'Dell U2723QE',
311+
brightness: 50,
312+
supportsBrightness: true,
313+
isBuiltIn: false,
314+
},
315+
];
282316
mockInvoke.mockImplementation((cmd: string) => {
283317
switch (cmd) {
318+
case 'fetch_all_state':
319+
return Promise.resolve({
320+
monitors: singleMonitor,
321+
isDark: false,
322+
volume: 50,
323+
});
284324
case 'get_monitors':
285-
return Promise.resolve([
286-
{
287-
id: '1',
288-
uid: '1::Dell U2723QE',
289-
name: 'Dell U2723QE',
290-
originalName: 'Dell U2723QE',
291-
brightness: 50,
292-
supportsBrightness: true,
293-
isBuiltIn: false,
294-
},
295-
]);
325+
return Promise.resolve(singleMonitor);
296326
case 'get_dark_mode':
297327
return Promise.resolve(false);
298328
case 'get_volume':

src/components/Header.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@ interface HeaderProps {
66

77
/** App header with version display and settings gear button. */
88
export default function Header({ version, onSettingsToggle, settingsOpen }: HeaderProps) {
9+
// Split version into base (e.g. "6.3.19") and tag (e.g. "[DEV - 04/23/2026 08:30]")
10+
const tagMatch = version.match(/^([\d.]+)\s*(\[.+\])$/);
11+
const baseVersion = tagMatch ? tagMatch[1] : version;
12+
const devTag = tagMatch ? tagMatch[2] : '';
13+
914
return (
1015
<div className='header'>
1116
<div>
1217
<span className='header-title'>Display DJ</span>
13-
{version && <span className='header-version'>v{version}</span>}
18+
{version && (
19+
<span className='header-version'>
20+
v{baseVersion}
21+
{devTag && <span className='header-dev-tag'> {devTag}</span>}
22+
</span>
23+
)}
1424
</div>
1525
<div className='header-actions'>
1626
<button

0 commit comments

Comments
 (0)