Skip to content

Commit f18bf01

Browse files
authored
Fix/launcher focus bugs (#457)
* fix(launcher): focus search bar from Settings + show weekday in calculator When a sibling window from the same app (Settings, Extension Store, etc.) was the key window, opening the launcher panel via the hotkey left the search input unfocused — mainWindow.focus() alone can't take key status from a regular activated window in the same app. Detect that case and activate the launcher with app.focus({ steal: true }); this only steals from our own window so selection-capture behavior is unaffected. Calculator date results now include the day of the week. SoulverCore's stringValue strips the year and never includes a weekday (e.g. "24 may 2026" → "May 24"), so the Swift bridge now returns the underlying Foundation.Date as an iso field and the renderer reformats date answers with toLocaleDateString({ weekday, day, month, year }) → "Sunday, 24 May 2026". * fix(launcher): keep success toast visible for full 3s after no-view command The inline toast raised by showHUD inside the launcher footer (e.g. "Your Mac is now caffeinated" after running Caffeinate) was disappearing almost immediately. Root cause: NoViewRunner unmounts the ExtensionView ~600 ms after the command finishes, and ExtensionView's unmount cleanup called Toast.dismissActive() unconditionally — overriding the toast's own 3000 ms auto-hide. dismissActive now only hides Animated (loading) toasts, which have no timer and must be cleaned up so they don't linger; Success/Failure toasts keep their auto-hide timer. Also fix a related race in the bottom-left memoryStatusWindow badge: the 200 ms fade-out finalization setTimeout in hideMemoryStatusBar wasn't tracked, so a fresh showMemoryStatusBar arriving during the fade couldn't cancel it — the orphan win.hide() then yanked the new badge off-screen. The finalize timeout is now stored, cleared by subsequent hide() calls, and gated on the current renderSeq so a show during the fade reliably keeps the window visible.
1 parent f865bbb commit f18bf01

7 files changed

Lines changed: 94 additions & 8 deletions

File tree

src/main/main.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2450,6 +2450,7 @@ let promptRendererReady = false;
24502450
let pendingPromptWindowShown: { mode: string; selectedTextSnapshot: string } | null = null;
24512451
let memoryStatusWindow: InstanceType<typeof BrowserWindow> | null = null;
24522452
let memoryStatusHideTimer: NodeJS.Timeout | null = null;
2453+
let memoryStatusFadeFinalizeTimer: NodeJS.Timeout | null = null;
24532454
let memoryStatusRenderSeq = 0;
24542455
let memoryStatusHideTimerSeq = 0;
24552456
let confettiWindow: InstanceType<typeof BrowserWindow> | null = null;
@@ -2651,7 +2652,16 @@ function hideMemoryStatusBar(): void {
26512652
win.webContents.executeJavaScript('window.__scFadeOut && window.__scFadeOut()').catch(() => {});
26522653
}
26532654
} catch {}
2654-
setTimeout(() => {
2655+
// Track the fade-out finalization timeout so a fresh showMemoryStatusBar
2656+
// arriving during the 200 ms fade can cancel it. Without this, the
2657+
// win.hide() below fires unconditionally and yanks the freshly-shown
2658+
// badge off-screen ~200 ms after the new show — making rapid
2659+
// processing → success transitions appear to flash for only a moment.
2660+
if (memoryStatusFadeFinalizeTimer) clearTimeout(memoryStatusFadeFinalizeTimer);
2661+
const finalizeSeq = memoryStatusRenderSeq;
2662+
memoryStatusFadeFinalizeTimer = setTimeout(() => {
2663+
memoryStatusFadeFinalizeTimer = null;
2664+
if (finalizeSeq !== memoryStatusRenderSeq) return;
26552665
if (!win.isDestroyed()) {
26562666
try { win.hide(); } catch {}
26572667
}
@@ -8885,7 +8895,28 @@ async function showWindow(options?: { systemCommandId?: string }): Promise<void>
88858895
} catch {}
88868896
}
88878897

8888-
const shouldActivateLauncherWindow = process.platform !== 'darwin' || launcherMode === 'onboarding';
8898+
// When a sibling window from our own app (Settings, Extension Store, Notes,
8899+
// Canvas, etc.) is currently the key window, the launcher panel's
8900+
// mainWindow.focus() alone is not enough to take key status away from a
8901+
// regular activated window in the same app — so the search input never
8902+
// actually receives keystrokes. Detect that case and fully activate the
8903+
// launcher window via app.focus({ steal: true }). This only steals focus
8904+
// from our own sibling window, not from another app, so the selection
8905+
// snapshot behavior (which only matters when another app is frontmost)
8906+
// is unaffected.
8907+
const ownAppSiblingWindowFocused =
8908+
process.platform === 'darwin' &&
8909+
BrowserWindow.getAllWindows().some(
8910+
(win: InstanceType<typeof BrowserWindow>) =>
8911+
win !== mainWindow &&
8912+
!win.isDestroyed() &&
8913+
win.isVisible() &&
8914+
win.isFocused()
8915+
);
8916+
const shouldActivateLauncherWindow =
8917+
process.platform !== 'darwin' ||
8918+
launcherMode === 'onboarding' ||
8919+
ownAppSiblingWindowFocused;
88898920
let selectionSnapshotPromise: Promise<string> | null = null;
88908921

88918922
// Capture the frontmost app BEFORE showing our window.

src/main/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const electronAPI = {
4343
value: string | null;
4444
raw: number | null;
4545
type: string;
46+
iso: string | null;
4647
error: string | null;
4748
}> => ipcRenderer.invoke('calculator-evaluate', expression),
4849

src/main/soulver-calculator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface SoulverResponse {
1919
value: string | null;
2020
raw: number | null;
2121
type: string;
22+
iso: string | null;
2223
error: string | null;
2324
}
2425

@@ -108,7 +109,7 @@ function ensureChild(): ChildProcessWithoutNullStreams {
108109
export function evaluate(expr: string): Promise<SoulverResponse> {
109110
const trimmed = expr.trim();
110111
if (!trimmed) {
111-
return Promise.resolve({ id: 0, value: null, raw: null, type: 'unknown', error: 'empty' });
112+
return Promise.resolve({ id: 0, value: null, raw: null, type: 'unknown', iso: null, error: 'empty' });
112113
}
113114

114115
return new Promise<SoulverResponse>((resolve, reject) => {

src/native/soulver-calculator/Sources/main.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import SoulverCore
66
// Request: {"id": <number>, "expr": "<string>"}
77
// Response: {"id": <number>, "value": "<string>", "raw": <number|null>,
88
// "type": "<math|unit|currency|percentage|date|duration|string|unknown>",
9+
// "iso": "<ISO8601 string|null>",
910
// "error": "<string|null>"}
1011
//
1112
// One long-lived process per launcher. Requests arrive one per line.
@@ -54,9 +55,31 @@ struct Response: Encodable {
5455
let value: String?
5556
let raw: Double?
5657
let type: String
58+
let iso: String?
5759
let error: String?
5860
}
5961

62+
// ISO8601 formatter (UTC) used for the optional date payload sent to JS.
63+
let iso8601Formatter: ISO8601DateFormatter = {
64+
let f = ISO8601DateFormatter()
65+
f.formatOptions = [.withInternetDateTime]
66+
return f
67+
}()
68+
69+
// Extract the underlying Foundation.Date from date-typed EvaluationResults so
70+
// the renderer can format with a weekday/year even though SoulverCore's
71+
// stringValue may omit them (e.g. "May 24" for "24 may 2026").
72+
func isoDate(from eval: EvaluationResult) -> String? {
73+
switch eval {
74+
case .date(let stamp):
75+
return iso8601Formatter.string(from: stamp.date)
76+
case .datespan(let span):
77+
return iso8601Formatter.string(from: span.startDate)
78+
default:
79+
return nil
80+
}
81+
}
82+
6083
let encoder = JSONEncoder()
6184
let decoder = JSONDecoder()
6285
let stdoutHandle = FileHandle.standardOutput
@@ -123,6 +146,7 @@ func evaluate(_ expr: String, id: Int) -> Response {
123146

124147
if result.isEmptyResult || result.isFailedResult {
125148
return Response(id: id, value: nil, raw: nil, type: "unknown",
149+
iso: nil,
126150
error: result.isFailedResult ? "failed" : "empty")
127151
}
128152

@@ -131,6 +155,7 @@ func evaluate(_ expr: String, id: Int) -> Response {
131155
value: result.stringValue,
132156
raw: rawDouble(from: result.evaluationResult),
133157
type: classify(result),
158+
iso: isoDate(from: result.evaluationResult),
134159
error: nil
135160
)
136161
}

src/renderer/src/raycast-api/index.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -873,11 +873,22 @@ export class Toast {
873873
return Promise.resolve();
874874
}
875875

876-
/** Dismiss any active toast — called when leaving an extension view. */
876+
/**
877+
* Dismiss any active toast — called when leaving an extension view.
878+
*
879+
* Only Animated (loading) toasts are dismissed: they have no auto-hide
880+
* timer of their own, so if the extension view unmounts mid-load they
881+
* would otherwise linger forever. Success/Failure toasts (the kind
882+
* showHUD raises after a no-view command finishes) already auto-hide
883+
* after 3 s — dismissing them on view unmount would cut their visible
884+
* lifetime down to the ~600 ms NoViewRunner close delay, making
885+
* "Your Mac is now caffeinated" flash and disappear.
886+
*/
877887
static dismissActive() {
878-
if (Toast._activeToast) {
879-
void Toast._activeToast.hide();
880-
}
888+
const active = Toast._activeToast;
889+
if (!active) return;
890+
if (active._style !== ToastStyle.Animated) return;
891+
void active.hide();
881892
}
882893
}
883894

src/renderer/src/smart-calculator.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,27 @@ export async function tryCalculateAsync(query: string): Promise<CalcResult | nul
8080
if (!kind) return null;
8181

8282
const labels = KIND_LABELS[kind];
83+
// For date results, SoulverCore's stringValue omits the year and weekday
84+
// (e.g. "24 may 2026" → "May 24"). Use the iso payload added to the
85+
// bridge response to format the resolved date with weekday + year so the
86+
// user can see what day of the week the input falls on.
87+
let resultValue = response.value;
88+
if (kind === 'date' && typeof response.iso === 'string' && response.iso) {
89+
const parsed = new Date(response.iso);
90+
if (!Number.isNaN(parsed.getTime())) {
91+
resultValue = parsed.toLocaleDateString(undefined, {
92+
weekday: 'long',
93+
day: 'numeric',
94+
month: 'long',
95+
year: 'numeric',
96+
});
97+
}
98+
}
8399
return {
84100
kind,
85101
input: trimmed,
86102
inputLabel: labels.input,
87-
result: response.value,
103+
result: resultValue,
88104
resultLabel: labels.result,
89105
};
90106
} catch {

src/renderer/types/electron.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,7 @@ export interface SoulverResponse {
801801
value: string | null;
802802
raw: number | null;
803803
type: string;
804+
iso: string | null;
804805
error: string | null;
805806
}
806807

0 commit comments

Comments
 (0)