initial commit

This commit is contained in:
Zacharias-Brohn
2025-12-12 22:11:20 +01:00
commit 5d47177fbf
19 changed files with 11695 additions and 0 deletions
+798
View File
@@ -0,0 +1,798 @@
//! ZTerm Client - GPU-accelerated terminal emulator that connects to the daemon.
use zterm::client::DaemonClient;
use zterm::config::{Action, Config};
use zterm::keyboard::{FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers};
use zterm::protocol::{ClientMessage, DaemonMessage, Direction, PaneId, PaneInfo, PaneSnapshot, WindowState};
use zterm::renderer::Renderer;
use polling::{Event, Events, Poller};
use std::collections::HashMap;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
use winit::event::{ElementState, KeyEvent, Modifiers as WinitModifiers, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
use winit::keyboard::{Key, NamedKey};
use winit::platform::wayland::EventLoopBuilderExtWayland;
use winit::window::{Window, WindowId};
/// Main application state.
struct App {
window: Option<Arc<Window>>,
renderer: Option<Renderer>,
daemon_client: Option<DaemonClient>,
/// Current window state (tabs info) from daemon.
window_state: Option<WindowState>,
/// All pane snapshots from daemon.
panes: Vec<PaneSnapshot>,
/// Whether we need to redraw.
dirty: bool,
/// Current modifier state.
modifiers: WinitModifiers,
/// Keyboard state for encoding (tracks protocol mode from daemon).
keyboard_state: KeyboardState,
/// Application configuration.
config: Config,
/// Keybinding action map.
action_map: HashMap<(bool, bool, bool, bool, String), Action>,
/// Event loop proxy for waking from daemon poll thread.
event_loop_proxy: Option<EventLoopProxy<()>>,
/// Shutdown signal for daemon poll thread.
shutdown: Arc<AtomicBool>,
}
const DAEMON_SOCKET_KEY: usize = 1;
impl App {
fn new() -> Self {
let config = Config::load();
log::info!("Config: font_size={}", config.font_size);
// Build action map from keybindings
let action_map = config.keybindings.build_action_map();
Self {
window: None,
renderer: None,
daemon_client: None,
window_state: None,
panes: Vec::new(),
dirty: true,
modifiers: WinitModifiers::default(),
keyboard_state: KeyboardState::new(),
config,
action_map,
event_loop_proxy: None,
shutdown: Arc::new(AtomicBool::new(false)),
}
}
fn set_event_loop_proxy(&mut self, proxy: EventLoopProxy<()>) {
self.event_loop_proxy = Some(proxy);
}
fn initialize(&mut self, event_loop: &ActiveEventLoop) {
let init_start = std::time::Instant::now();
// Create window first so it appears immediately
let mut window_attributes = Window::default_attributes()
.with_title("ZTerm")
.with_inner_size(PhysicalSize::new(800, 600));
// Enable transparency if background opacity is less than 1.0
if self.config.background_opacity < 1.0 {
window_attributes = window_attributes.with_transparent(true);
}
let window = Arc::new(
event_loop
.create_window(window_attributes)
.expect("Failed to create window"),
);
log::debug!("Window created in {:?}", init_start.elapsed());
// Start daemon connection in parallel with renderer initialization
let daemon_start = std::time::Instant::now();
let mut daemon_client = match DaemonClient::connect() {
Ok(client) => client,
Err(e) => {
log::error!("Failed to connect to daemon: {}", e);
event_loop.exit();
return;
}
};
log::debug!("Daemon connected in {:?}", daemon_start.elapsed());
// Create renderer (this is the slow part - GPU initialization)
let renderer_start = std::time::Instant::now();
let renderer = pollster::block_on(Renderer::new(window.clone(), &self.config));
log::debug!("Renderer created in {:?}", renderer_start.elapsed());
// Calculate terminal size based on window size
let (cols, rows) = renderer.terminal_size();
// Send hello with our size
if let Err(e) = daemon_client.hello(cols, rows) {
log::error!("Failed to send hello: {}", e);
event_loop.exit();
return;
}
// Wait for initial state
match daemon_client.recv() {
Ok(DaemonMessage::FullState { window: win_state, panes }) => {
log::debug!("Received initial state with {} tabs, {} panes",
win_state.tabs.len(), panes.len());
self.window_state = Some(win_state);
self.panes = panes;
}
Ok(msg) => {
log::warn!("Unexpected initial message: {:?}", msg);
}
Err(e) => {
log::error!("Failed to receive initial state: {}", e);
event_loop.exit();
return;
}
}
// Switch to non-blocking mode for the event loop
if let Err(e) = daemon_client.set_nonblocking() {
log::error!("Failed to set non-blocking mode: {}", e);
event_loop.exit();
return;
}
// Set up polling for daemon socket in a background thread
// This thread will wake the event loop when data is available
if let Some(proxy) = self.event_loop_proxy.clone() {
let daemon_fd = daemon_client.as_raw_fd();
let shutdown = self.shutdown.clone();
std::thread::spawn(move || {
let poller = match Poller::new() {
Ok(p) => p,
Err(e) => {
log::error!("Failed to create poller: {}", e);
return;
}
};
// SAFETY: daemon_fd is valid for the lifetime of the daemon_client,
// and we signal shutdown before dropping daemon_client
unsafe {
if let Err(e) = poller.add(daemon_fd, Event::readable(DAEMON_SOCKET_KEY)) {
log::error!("Failed to add daemon socket to poller: {}", e);
return;
}
}
let mut events = Events::new();
while !shutdown.load(Ordering::Relaxed) {
events.clear();
// Wait for data with a timeout so we can check shutdown
match poller.wait(&mut events, Some(Duration::from_millis(100))) {
Ok(_) if !events.is_empty() => {
// Wake the event loop by sending an empty event
let _ = proxy.send_event(());
// Re-register for more events
// SAFETY: daemon_fd is still valid
unsafe {
let _ = poller.modify(
std::os::fd::BorrowedFd::borrow_raw(daemon_fd),
Event::readable(DAEMON_SOCKET_KEY)
);
}
}
Ok(_) => {} // Timeout, no events
Err(e) => {
log::error!("Poller error: {}", e);
break;
}
}
}
log::debug!("Daemon poll thread exiting");
});
}
self.window = Some(window);
self.renderer = Some(renderer);
self.daemon_client = Some(daemon_client);
log::info!("Client initialized in {:?}: {}x{} cells", init_start.elapsed(), cols, rows);
}
/// Gets all pane snapshots with their layout info for the active tab.
/// Returns (panes_with_info, active_pane_id) with cloned/owned data.
fn active_tab_panes(&self) -> (Vec<(PaneSnapshot, PaneInfo)>, PaneId) {
let Some(win) = self.window_state.as_ref() else {
return (Vec::new(), 0);
};
let Some(tab) = win.tabs.get(win.active_tab) else {
return (Vec::new(), 0);
};
let active_pane_id = tab.panes.get(tab.active_pane)
.map(|p| p.id)
.unwrap_or(0);
let panes_with_info: Vec<(PaneSnapshot, PaneInfo)> = tab.panes.iter()
.filter_map(|pane_info| {
self.panes.iter()
.find(|snap| snap.pane_id == pane_info.id)
.map(|snap| (snap.clone(), pane_info.clone()))
})
.collect();
(panes_with_info, active_pane_id)
}
/// Gets the active pane ID and its snapshot.
fn get_active_pane(&self) -> Option<(PaneId, &PaneSnapshot)> {
let win = self.window_state.as_ref()?;
let tab = win.tabs.get(win.active_tab)?;
let pane_info = tab.panes.get(tab.active_pane)?;
let snapshot = self.panes.iter().find(|s| s.pane_id == pane_info.id)?;
Some((pane_info.id, snapshot))
}
fn poll_daemon(&mut self) {
let Some(client) = &mut self.daemon_client else { return };
// Read all available messages (non-blocking)
// The background thread wakes us when data is available
let mut messages = Vec::new();
loop {
match client.try_recv() {
Ok(Some(msg)) => {
messages.push(msg);
}
Ok(None) => break,
Err(e) => {
log::error!("Daemon connection error: {}", e);
// Daemon disconnected - we'll handle this after the loop
messages.push(DaemonMessage::Shutdown);
break;
}
}
}
// Now process messages without holding the client borrow
for msg in messages {
self.handle_daemon_message(msg);
}
}
fn handle_daemon_message(&mut self, msg: DaemonMessage) {
match msg {
DaemonMessage::FullState { window, panes } => {
log::debug!("Received full state: {} tabs, {} panes",
window.tabs.len(), panes.len());
self.window_state = Some(window);
self.panes = panes;
self.dirty = true;
}
DaemonMessage::PaneUpdate { pane_id, cells, cursor } => {
log::debug!("Received pane update for pane {}", pane_id);
if let Some(pane) = self.panes.iter_mut().find(|p| p.pane_id == pane_id) {
pane.cells = cells;
pane.cursor = cursor;
} else {
// New pane
self.panes.push(PaneSnapshot {
pane_id,
cells,
cursor,
scroll_offset: 0,
scrollback_len: 0,
});
}
self.dirty = true;
}
DaemonMessage::TabChanged { active_tab } => {
log::debug!("Tab changed to {}", active_tab);
if let Some(ref mut win) = self.window_state {
win.active_tab = active_tab;
}
self.dirty = true;
}
DaemonMessage::TabCreated { tab } => {
log::debug!("Tab created: {:?}", tab);
if let Some(ref mut win) = self.window_state {
win.tabs.push(tab);
}
self.dirty = true;
}
DaemonMessage::TabClosed { tab_id } => {
log::debug!("Tab closed: {}", tab_id);
if let Some(ref mut win) = self.window_state {
win.tabs.retain(|t| t.id != tab_id);
// Adjust active tab if needed
if win.active_tab >= win.tabs.len() && !win.tabs.is_empty() {
win.active_tab = win.tabs.len() - 1;
}
}
// Remove panes for closed tab
// Note: daemon should send updated panes, but we clean up just in case
self.dirty = true;
}
DaemonMessage::PaneCreated { tab_id, pane } => {
log::debug!("Pane created in tab {}: {:?}", tab_id, pane);
if let Some(ref mut win) = self.window_state {
if let Some(tab) = win.tabs.iter_mut().find(|t| t.id == tab_id) {
tab.panes.push(pane);
}
}
self.dirty = true;
}
DaemonMessage::PaneClosed { tab_id, pane_id } => {
log::debug!("Pane {} closed in tab {}", pane_id, tab_id);
if let Some(ref mut win) = self.window_state {
if let Some(tab) = win.tabs.iter_mut().find(|t| t.id == tab_id) {
tab.panes.retain(|p| p.id != pane_id);
}
}
// Also remove from pane snapshots
self.panes.retain(|p| p.pane_id != pane_id);
self.dirty = true;
}
DaemonMessage::PaneFocused { tab_id, active_pane } => {
log::debug!("Pane focus changed in tab {}: pane {}", tab_id, active_pane);
if let Some(ref mut win) = self.window_state {
if let Some(tab) = win.tabs.iter_mut().find(|t| t.id == tab_id) {
tab.active_pane = active_pane;
}
}
self.dirty = true;
}
DaemonMessage::Shutdown => {
log::info!("Daemon shutting down");
self.daemon_client = None;
}
}
}
/// Checks if the key event matches a keybinding and executes the action.
/// Returns true if the key was consumed by a keybinding.
fn check_keybinding(&mut self, event: &KeyEvent) -> bool {
// Only process key presses, not releases or repeats
if event.state != ElementState::Pressed || event.repeat {
return false;
}
let mod_state = self.modifiers.state();
let ctrl = mod_state.control_key();
let alt = mod_state.alt_key();
let shift = mod_state.shift_key();
let super_key = mod_state.super_key();
// Get the key name
let key_name = match &event.logical_key {
Key::Named(named) => {
match named {
NamedKey::Tab => "tab".to_string(),
NamedKey::Enter => "enter".to_string(),
NamedKey::Escape => "escape".to_string(),
NamedKey::Backspace => "backspace".to_string(),
NamedKey::Delete => "delete".to_string(),
NamedKey::Insert => "insert".to_string(),
NamedKey::Home => "home".to_string(),
NamedKey::End => "end".to_string(),
NamedKey::PageUp => "pageup".to_string(),
NamedKey::PageDown => "pagedown".to_string(),
NamedKey::ArrowUp => "up".to_string(),
NamedKey::ArrowDown => "down".to_string(),
NamedKey::ArrowLeft => "left".to_string(),
NamedKey::ArrowRight => "right".to_string(),
NamedKey::Space => " ".to_string(),
NamedKey::F1 => "f1".to_string(),
NamedKey::F2 => "f2".to_string(),
NamedKey::F3 => "f3".to_string(),
NamedKey::F4 => "f4".to_string(),
NamedKey::F5 => "f5".to_string(),
NamedKey::F6 => "f6".to_string(),
NamedKey::F7 => "f7".to_string(),
NamedKey::F8 => "f8".to_string(),
NamedKey::F9 => "f9".to_string(),
NamedKey::F10 => "f10".to_string(),
NamedKey::F11 => "f11".to_string(),
NamedKey::F12 => "f12".to_string(),
_ => return false,
}
}
Key::Character(c) => c.to_lowercase(),
_ => return false,
};
// Look up the action
let lookup = (ctrl, alt, shift, super_key, key_name);
let Some(action) = self.action_map.get(&lookup).copied() else {
return false;
};
// Execute the action
self.execute_action(action);
true
}
fn execute_action(&mut self, action: Action) {
let Some(client) = &mut self.daemon_client else { return };
match action {
Action::NewTab => {
log::debug!("Action: NewTab");
let _ = client.create_tab();
}
Action::NextTab => {
log::debug!("Action: NextTab");
let _ = client.next_tab();
}
Action::PrevTab => {
log::debug!("Action: PrevTab");
let _ = client.prev_tab();
}
Action::Tab1 => { let _ = client.switch_tab_index(0); }
Action::Tab2 => { let _ = client.switch_tab_index(1); }
Action::Tab3 => { let _ = client.switch_tab_index(2); }
Action::Tab4 => { let _ = client.switch_tab_index(3); }
Action::Tab5 => { let _ = client.switch_tab_index(4); }
Action::Tab6 => { let _ = client.switch_tab_index(5); }
Action::Tab7 => { let _ = client.switch_tab_index(6); }
Action::Tab8 => { let _ = client.switch_tab_index(7); }
Action::Tab9 => { let _ = client.switch_tab_index(8); }
Action::SplitHorizontal => {
log::debug!("Action: SplitHorizontal");
let _ = client.split_horizontal();
}
Action::SplitVertical => {
log::debug!("Action: SplitVertical");
let _ = client.split_vertical();
}
Action::ClosePane => {
log::debug!("Action: ClosePane");
let _ = client.close_pane();
}
Action::FocusPaneUp => {
log::debug!("Action: FocusPaneUp");
let _ = client.focus_pane(Direction::Up);
}
Action::FocusPaneDown => {
log::debug!("Action: FocusPaneDown");
let _ = client.focus_pane(Direction::Down);
}
Action::FocusPaneLeft => {
log::debug!("Action: FocusPaneLeft");
let _ = client.focus_pane(Direction::Left);
}
Action::FocusPaneRight => {
log::debug!("Action: FocusPaneRight");
let _ = client.focus_pane(Direction::Right);
}
}
}
fn handle_keyboard_input(&mut self, event: KeyEvent) {
// First check if this is a keybinding
if self.check_keybinding(&event) {
return;
}
// Determine event type
let event_type = match event.state {
ElementState::Pressed => {
if event.repeat {
KeyEventType::Repeat
} else {
KeyEventType::Press
}
}
ElementState::Released => KeyEventType::Release,
};
// In legacy mode, ignore release events
if event_type == KeyEventType::Release && !self.keyboard_state.report_events() {
return;
}
// Build modifiers from the tracked state
let mod_state = self.modifiers.state();
let modifiers = Modifiers {
shift: mod_state.shift_key(),
alt: mod_state.alt_key(),
ctrl: mod_state.control_key(),
super_key: mod_state.super_key(),
hyper: false,
meta: false,
caps_lock: false,
num_lock: false,
};
let encoder = KeyEncoder::new(&self.keyboard_state);
let bytes: Option<Vec<u8>> = match &event.logical_key {
Key::Named(named) => {
let func_key = match named {
NamedKey::Enter => Some(FunctionalKey::Enter),
NamedKey::Backspace => Some(FunctionalKey::Backspace),
NamedKey::Tab => Some(FunctionalKey::Tab),
NamedKey::Escape => Some(FunctionalKey::Escape),
NamedKey::Space => None,
NamedKey::ArrowUp => Some(FunctionalKey::Up),
NamedKey::ArrowDown => Some(FunctionalKey::Down),
NamedKey::ArrowRight => Some(FunctionalKey::Right),
NamedKey::ArrowLeft => Some(FunctionalKey::Left),
NamedKey::Home => Some(FunctionalKey::Home),
NamedKey::End => Some(FunctionalKey::End),
NamedKey::PageUp => Some(FunctionalKey::PageUp),
NamedKey::PageDown => Some(FunctionalKey::PageDown),
NamedKey::Insert => Some(FunctionalKey::Insert),
NamedKey::Delete => Some(FunctionalKey::Delete),
NamedKey::F1 => Some(FunctionalKey::F1),
NamedKey::F2 => Some(FunctionalKey::F2),
NamedKey::F3 => Some(FunctionalKey::F3),
NamedKey::F4 => Some(FunctionalKey::F4),
NamedKey::F5 => Some(FunctionalKey::F5),
NamedKey::F6 => Some(FunctionalKey::F6),
NamedKey::F7 => Some(FunctionalKey::F7),
NamedKey::F8 => Some(FunctionalKey::F8),
NamedKey::F9 => Some(FunctionalKey::F9),
NamedKey::F10 => Some(FunctionalKey::F10),
NamedKey::F11 => Some(FunctionalKey::F11),
NamedKey::F12 => Some(FunctionalKey::F12),
NamedKey::CapsLock => Some(FunctionalKey::CapsLock),
NamedKey::ScrollLock => Some(FunctionalKey::ScrollLock),
NamedKey::NumLock => Some(FunctionalKey::NumLock),
NamedKey::PrintScreen => Some(FunctionalKey::PrintScreen),
NamedKey::Pause => Some(FunctionalKey::Pause),
NamedKey::ContextMenu => Some(FunctionalKey::Menu),
_ => None,
};
if let Some(key) = func_key {
Some(encoder.encode_functional(key, modifiers, event_type))
} else if *named == NamedKey::Space {
Some(encoder.encode_char(' ', modifiers, event_type))
} else {
None
}
}
Key::Character(c) => {
if let Some(ch) = c.chars().next() {
let key_char = ch.to_lowercase().next().unwrap_or(ch);
Some(encoder.encode_char(key_char, modifiers, event_type))
} else {
None
}
}
_ => None,
};
if let Some(bytes) = bytes {
// Check scroll offset before borrowing client mutably
let scroll_reset = self.get_active_pane()
.filter(|(_, snapshot)| snapshot.scroll_offset > 0)
.map(|(pane_id, snapshot)| (pane_id, snapshot.scroll_offset));
// Now borrow client mutably
if let Some(client) = &mut self.daemon_client {
let _ = client.send_input(bytes);
// Reset scroll position when typing (go back to live terminal)
if let Some((active_pane_id, scroll_offset)) = scroll_reset {
let _ = client.send(&ClientMessage::Scroll {
pane_id: active_pane_id,
delta: -(scroll_offset as i32)
});
}
}
}
}
fn resize(&mut self, new_size: PhysicalSize<u32>) {
if new_size.width == 0 || new_size.height == 0 {
return;
}
if let Some(renderer) = &mut self.renderer {
renderer.resize(new_size.width, new_size.height);
let (cols, rows) = renderer.terminal_size();
if let Some(client) = &mut self.daemon_client {
let _ = client.send_resize(cols, rows);
}
log::debug!("Resized to {}x{} cells", cols, rows);
self.dirty = true;
}
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_none() {
self.initialize(event_loop);
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {
log::info!("Window close requested");
event_loop.exit();
}
WindowEvent::Resized(new_size) => {
self.resize(new_size);
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
log::info!("Scale factor changed to {}", scale_factor);
if let Some(renderer) = &mut self.renderer {
if renderer.set_scale_factor(scale_factor) {
let (cols, rows) = renderer.terminal_size();
if let Some(client) = &mut self.daemon_client {
let _ = client.send_resize(cols, rows);
}
log::info!("Terminal resized to {}x{} cells after scale change", cols, rows);
}
if let Some(window) = &self.window {
window.request_redraw();
}
}
}
WindowEvent::ModifiersChanged(new_modifiers) => {
self.modifiers = new_modifiers;
}
WindowEvent::MouseWheel { delta, .. } => {
// Handle mouse wheel for scrollback
let lines = match delta {
MouseScrollDelta::LineDelta(_, y) => {
// y > 0 means scrolling up (into history), y < 0 means down
(y * 3.0) as i32 // 3 lines per scroll notch
}
MouseScrollDelta::PixelDelta(pos) => {
// Convert pixels to lines (rough approximation)
(pos.y / 20.0) as i32
}
};
if lines != 0 {
// Get the active pane ID to scroll
if let Some((active_pane_id, _)) = self.get_active_pane() {
if let Some(client) = &mut self.daemon_client {
let _ = client.send(&ClientMessage::Scroll {
pane_id: active_pane_id,
delta: lines
});
}
}
if let Some(window) = &self.window {
window.request_redraw();
}
}
}
WindowEvent::KeyboardInput { event, .. } => {
self.handle_keyboard_input(event);
if let Some(window) = &self.window {
window.request_redraw();
}
}
WindowEvent::RedrawRequested => {
// Gather all panes for the active tab with their layout info (cloned to avoid borrow conflict)
let (panes_with_info, active_pane_id) = self.active_tab_panes();
let tabs = self.window_state.as_ref().map(|w| w.tabs.clone());
let active_tab = self.window_state.as_ref().map(|w| w.active_tab).unwrap_or(0);
if let Some(renderer) = &mut self.renderer {
if !panes_with_info.is_empty() {
let tabs = tabs.unwrap_or_default();
// Convert owned data to references for the renderer
let pane_refs: Vec<(&PaneSnapshot, &PaneInfo)> = panes_with_info.iter()
.map(|(snap, info)| (snap, info))
.collect();
match renderer.render_with_tabs(&pane_refs, active_pane_id, &tabs, active_tab) {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => {
renderer.resize(renderer.width, renderer.height);
}
Err(wgpu::SurfaceError::OutOfMemory) => {
log::error!("Out of GPU memory!");
event_loop.exit();
}
Err(e) => {
log::error!("Render error: {:?}", e);
}
}
self.dirty = false;
}
}
}
_ => {}
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
// Check if daemon is still connected
if self.daemon_client.is_none() {
log::info!("Lost connection to daemon, exiting");
event_loop.exit();
return;
}
// Poll daemon for updates
self.poll_daemon();
// Request redraw if we have new content
if self.dirty {
if let Some(window) = &self.window {
window.request_redraw();
}
}
// Use WaitUntil to wake up periodically and check for daemon messages
// This is more compatible than relying on send_event across threads
event_loop.set_control_flow(ControlFlow::WaitUntil(
std::time::Instant::now() + Duration::from_millis(16)
));
}
fn user_event(&mut self, _event_loop: &ActiveEventLoop, _event: ()) {
// Daemon poll thread woke us up - poll for messages
self.poll_daemon();
// Request redraw if we have new content
if self.dirty {
if let Some(window) = &self.window {
window.request_redraw();
}
}
}
}
impl Drop for App {
fn drop(&mut self) {
// Signal the daemon poll thread to exit
self.shutdown.store(true, Ordering::Relaxed);
}
}
fn main() {
// Initialize logging
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
log::info!("Starting ZTerm client");
// Create event loop with Wayland preference
let event_loop = EventLoop::builder()
.with_any_thread(true)
.build()
.expect("Failed to create event loop");
// Use Wait instead of Poll to avoid busy-looping
// The daemon poll thread will wake us when data is available
event_loop.set_control_flow(ControlFlow::Wait);
let mut app = App::new();
// Give the app a proxy to wake the event loop from the daemon poll thread
app.set_event_loop_proxy(event_loop.create_proxy());
event_loop.run_app(&mut app).expect("Event loop error");
}