Worker thread + tray icon repaint fix
- processUtterance runs on a detached std.Thread; Qt event loop stays responsive during whisper/letta/melo (icon repaints, menu works) - setState marshals to main thread via threading.async with thread-id check - rapid double-click race guarded: state claimed before spawn - icon: QPainter-drawn antialiased circle (dark rim, transparent bg) — bare pixmap.fill rendered blank on the KDE SNI host - new qt libs: qbrush, qpainter, qrect, qt6zigthreading 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code <noreply@letta.com>
This commit is contained in:
1 parent
f4c08cae74
commit
5b050c09e3
2 files changed
+60
-3
No files matched your search
@@ -16,11 +16,15 @@ pub fn build(b: *std.Build) void {
|
|||||||
"qcoreapplication",
|
"qcoreapplication",
|
||||||
"qguiapplication",
|
"qguiapplication",
|
||||||
"qaction",
|
"qaction",
|
||||||
|
"qbrush",
|
||||||
"qcolor",
|
"qcolor",
|
||||||
"qicon",
|
"qicon",
|
||||||
|
"qpainter",
|
||||||
"qpixmap",
|
"qpixmap",
|
||||||
"qmenu",
|
"qmenu",
|
||||||
|
"qrect",
|
||||||
"qsystemtrayicon",
|
"qsystemtrayicon",
|
||||||
|
"qt6zigthreading",
|
||||||
"qwidget",
|
"qwidget",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+56
-3
@@ -14,6 +14,10 @@ const QAction = qt6.QAction;
|
|||||||
const QPixmap = qt6.QPixmap;
|
const QPixmap = qt6.QPixmap;
|
||||||
const QIcon = qt6.QIcon;
|
const QIcon = qt6.QIcon;
|
||||||
const QColor = qt6.QColor;
|
const QColor = qt6.QColor;
|
||||||
|
const QPainter = qt6.QPainter;
|
||||||
|
const QBrush = qt6.QBrush;
|
||||||
|
const QRectF = qt6.QRectF;
|
||||||
|
const threading = qt6.threading;
|
||||||
const letta = @import("letta.zig");
|
const letta = @import("letta.zig");
|
||||||
|
|
||||||
// ------------------------------------------------------------- constants ---
|
// ------------------------------------------------------------- constants ---
|
||||||
@@ -75,15 +79,39 @@ fn setLastUserText(text: []u8) void {
|
|||||||
var current_icon: ?QIcon = null;
|
var current_icon: ?QIcon = null;
|
||||||
var current_pixmap: ?QPixmap = null;
|
var current_pixmap: ?QPixmap = null;
|
||||||
|
|
||||||
fn setState(next: State) void {
|
var main_thread_id: std.Thread.Id = 0;
|
||||||
|
var pending_state: State = .idle;
|
||||||
|
|
||||||
|
/// Must run on the Qt main thread (touches QSystemTrayIcon / QPixmap).
|
||||||
|
fn setStateMainThread(next: State) void {
|
||||||
state = next;
|
state = next;
|
||||||
|
|
||||||
|
// Circle icon drawn with QPainter — a bare pixmap.fill rendered blank
|
||||||
|
// on the KDE SNI host.
|
||||||
const c = next.color();
|
const c = next.color();
|
||||||
const color = QColor.new5(c[0], c[1], c[2]);
|
const color = QColor.new5(c[0], c[1], c[2]);
|
||||||
defer color.delete();
|
defer color.delete();
|
||||||
|
const pen_color = QColor.new5(20, 20, 26);
|
||||||
|
defer pen_color.delete();
|
||||||
|
const transparent = QColor.new8("transparent");
|
||||||
|
defer transparent.delete();
|
||||||
|
|
||||||
const pixmap = QPixmap.new2(128, 128);
|
const pixmap = QPixmap.new2(128, 128);
|
||||||
pixmap.fill1(color);
|
pixmap.fill1(transparent);
|
||||||
|
|
||||||
|
const brush = QBrush.new3(color);
|
||||||
|
defer brush.delete();
|
||||||
|
|
||||||
|
const painter = QPainter.new();
|
||||||
|
_ = painter.begin(pixmap);
|
||||||
|
painter.setRenderHint(1); // Antialiasing
|
||||||
|
painter.setPen(pen_color);
|
||||||
|
painter.setBrush(brush);
|
||||||
|
const rect = QRectF.new6(16, 16, 96, 96);
|
||||||
|
defer rect.delete();
|
||||||
|
painter.drawEllipse(rect);
|
||||||
|
_ = painter.end();
|
||||||
|
painter.delete();
|
||||||
|
|
||||||
const icon = QIcon.new2(pixmap);
|
const icon = QIcon.new2(pixmap);
|
||||||
|
|
||||||
@@ -99,6 +127,21 @@ fn setState(next: State) void {
|
|||||||
tray_icon.setToolTip(tip);
|
tray_icon.setToolTip(tip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn onStateFromWorker(_: ?*anyopaque) callconv(.c) void {
|
||||||
|
setStateMainThread(pending_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Thread-safe state transition: runs directly on the main thread, or
|
||||||
|
/// marshals through Qt's event loop when called from the pipeline worker.
|
||||||
|
fn setState(next: State) void {
|
||||||
|
if (std.Thread.getCurrentId() == main_thread_id) {
|
||||||
|
setStateMainThread(next);
|
||||||
|
} else {
|
||||||
|
pending_state = next;
|
||||||
|
threading.async(null, onStateFromWorker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Play a WAV via pw-play (blocking; audio length). Fine for v1 — same
|
/// Play a WAV via pw-play (blocking; audio length). Fine for v1 — same
|
||||||
/// thread already blocks on whisper/letta. UI-thread decoupling comes later.
|
/// thread already blocks on whisper/letta. UI-thread decoupling comes later.
|
||||||
fn playWav(path: []const u8) void {
|
fn playWav(path: []const u8) void {
|
||||||
@@ -258,7 +301,16 @@ fn onTrayActivated(_: QSystemTrayIcon, reason: i32) callconv(.c) void {
|
|||||||
},
|
},
|
||||||
.recording => {
|
.recording => {
|
||||||
stopRecording();
|
stopRecording();
|
||||||
processUtterance(); // chains transcribe -> infer -> speak -> re-listen
|
setState(.transcribing); // claim the pipeline slot before spawning
|
||||||
|
// Pipeline runs on a worker thread; the Qt event loop stays
|
||||||
|
// responsive (icon repaints, menu works) while whisper/letta/melo
|
||||||
|
// block. setState marshals back to the main thread.
|
||||||
|
const worker = std.Thread.spawn(.{}, processUtterance, .{}) catch |e| {
|
||||||
|
std.debug.print("inferon: failed to spawn worker: {s}\n", .{@errorName(e)});
|
||||||
|
setState(.error_backend);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
worker.detach();
|
||||||
},
|
},
|
||||||
// Clicks during the pipeline are no-ops now — states advance on their own.
|
// Clicks during the pipeline are no-ops now — states advance on their own.
|
||||||
.transcribing, .thinking, .speaking => {},
|
.transcribing, .thinking, .speaking => {},
|
||||||
@@ -347,6 +399,7 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
|
|
||||||
setState(.idle);
|
setState(.idle);
|
||||||
tray_icon.show();
|
tray_icon.show();
|
||||||
|
main_thread_id = std.Thread.getCurrentId();
|
||||||
|
|
||||||
// Own the whisper-server lifecycle: spawn AFTER the tray exists (setState
|
// Own the whisper-server lifecycle: spawn AFTER the tray exists (setState
|
||||||
// touches tray_icon). Create the log dir first.
|
// touches tray_icon). Create the log dir first.
|
||||||
|
|||||||
Reference in new issue
Block a user