fix
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled

This commit is contained in:
2026-03-16 10:11:23 -04:00
parent 5e076b8682
commit f81c8a20d2
2 changed files with 148 additions and 9 deletions

View File

@@ -177,7 +177,12 @@ fn setup_bridge(
action_tx: mpsc::Sender<ModifiedAction>, action_tx: mpsc::Sender<ModifiedAction>,
event_rx: broadcast::Receiver<MenuEvent>, event_rx: broadcast::Receiver<MenuEvent>,
) { ) {
let _ = action_tx.blocking_send(ModifiedAction::new(Action::Resize { // try_send rather than blocking_send: this is the first
// message on a fresh channel so it always has capacity,
// and try_send works regardless of whether we're inside
// a tokio runtime context (macOS calls this from the
// main thread outside block_on).
let _ = action_tx.try_send(ModifiedAction::new(Action::Resize {
height: VIEWPORT_HEIGHT, height: VIEWPORT_HEIGHT,
})); }));

View File

@@ -365,14 +365,23 @@ fn main() {
}); });
// STEP 4: Branch on headless vs interactive // STEP 4: Branch on headless vs interactive
let result = if let Some(script) = script {
rt.block_on(run_headless(items, &cli, script, start_mode, column_config))
} else {
let history_entries = history.as_ref().map(|h| h.entries().to_vec());
#[cfg(unix)] #[cfg(unix)]
let saved_fd = saved_stdin_fd; let saved_fd = saved_stdin_fd;
#[cfg(not(unix))] #[cfg(not(unix))]
let saved_fd: Option<i32> = None; let saved_fd: Option<i32> = None;
let result = if let Some(script) = script {
rt.block_on(run_headless(items, &cli, script, start_mode, column_config))
} else if cfg!(target_os = "macos") && frontend_mode == FrontendMode::Gui {
// macOS requires the GUI event loop on the main thread
// (AppKit/winit constraint). We can't run it inside
// block_on, so the GUI gets the main thread and tokio
// work is spawned onto the runtime's worker threads.
run_gui_main_thread(
&rt, items, &cli, start_mode, column_config, streaming, saved_fd,
)
} else {
let history_entries = history.as_ref().map(|h| h.entries().to_vec());
rt.block_on(run_interactive( rt.block_on(run_interactive(
items, items,
&cli, &cli,
@@ -662,13 +671,11 @@ async fn run_interactive(
result result
} }
FrontendMode::Gui => { FrontendMode::Gui => {
// GUI runs on the main thread (Wayland requirement).
// Spawn the core event loop on a background task. // Spawn the core event loop on a background task.
let menu_handle = tokio::spawn(menu.run()); let menu_handle = tokio::spawn(menu.run());
// pikl_gui::run blocks until the user confirms/cancels. // On Linux, spawn_blocking is fine since Wayland doesn't
// Run it on a blocking thread so we don't stall the // have the main-thread restriction.
// tokio runtime.
let gui_handle = tokio::task::spawn_blocking(move || { let gui_handle = tokio::task::spawn_blocking(move || {
pikl_gui::run(action_tx, event_rx) pikl_gui::run(action_tx, event_rx)
.map_err(|e| PiklError::Io(std::io::Error::other(e.to_string()))) .map_err(|e| PiklError::Io(std::io::Error::other(e.to_string())))
@@ -687,6 +694,133 @@ async fn run_interactive(
} }
} }
/// Run GUI on the main thread for macOS. AppKit requires
/// the event loop on the main thread, so we can't use
/// block_on (which would nest the iced event loop inside
/// tokio's reactor). Instead, spawn all background work
/// onto the runtime's worker threads and run the GUI
/// directly.
fn run_gui_main_thread(
rt: &tokio::runtime::Runtime,
items: Vec<Item>,
cli: &Cli,
start_mode: Mode,
column_config: Option<ColumnConfig>,
streaming: bool,
saved_stdin_fd: Option<i32>,
) -> Result<MenuResult, PiklError> {
let (mut menu, action_tx) = MenuRunner::new(build_menu(items, cli, column_config));
menu.set_initial_mode(start_mode);
menu.set_multi(cli.multi);
menu.set_selection_order(cli.selection_order);
menu.set_streaming(streaming);
if let Some((_handler, dispatcher)) = build_hook_handler(cli, &action_tx) {
menu.set_dispatcher(dispatcher);
}
let event_rx = menu.subscribe();
// Start IPC server if requested
let _ipc_server = if cli.ipc {
let sock_path = ipc::socket_path(cli.session.as_deref())?;
let server = ipc::server::IpcServer::new(
sock_path,
action_tx.clone(),
menu.event_sender(),
);
let ipc_event_rx = menu.subscribe();
server.start(ipc_event_rx).map_err(PiklError::Io)?;
Some(server)
} else {
None
};
// Enter the runtime context so tokio::spawn works
// without being inside block_on.
let _guard = rt.enter();
// Signal handler
let signal_tx = action_tx.clone();
tokio::spawn(async move {
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
Ok(mut sigterm) => {
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = sigterm.recv() => {}
}
}
Err(e) => {
tracing::warn!(%e, "failed to register SIGTERM handler, falling back to SIGINT only");
let _ = tokio::signal::ctrl_c().await;
}
}
let _ = signal_tx.send(ModifiedAction::new(Action::Cancel)).await;
});
// Streaming stdin reader
if let Some(fd) = saved_stdin_fd {
let stream_tx = action_tx.clone();
tokio::task::spawn_blocking(move || {
#[cfg(unix)]
{
use std::io::BufRead;
use std::os::unix::io::FromRawFd;
let file = unsafe { std::fs::File::from_raw_fd(fd) };
let mut reader = std::io::BufReader::new(file);
let mut batch = Vec::with_capacity(100);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) => break,
Ok(_) => {
let trimmed = line.trim_end();
if !trimmed.is_empty() {
batch.push(parse_line_to_value(trimmed));
}
if (batch.len() >= 100 || reader.buffer().is_empty())
&& !batch.is_empty()
{
let _ = stream_tx.blocking_send(ModifiedAction::new(
Action::AddItems(std::mem::take(&mut batch)),
));
}
}
Err(e) => {
tracing::warn!(%e, "stdin read error");
break;
}
}
}
if !batch.is_empty() {
let _ =
stream_tx.blocking_send(ModifiedAction::new(Action::AddItems(batch)));
}
let _ =
stream_tx.blocking_send(ModifiedAction::new(Action::StreamingDone));
}
});
}
// Core menu on a background task
let menu_handle = tokio::spawn(menu.run());
// GUI on the main thread (the whole point of this function)
pikl_gui::run(action_tx, event_rx)
.map_err(|e| PiklError::Io(std::io::Error::other(e.to_string())))?;
// GUI exited. Collect the menu result.
rt.block_on(async {
match menu_handle.await {
Ok(result) => result,
Err(e) => Err(PiklError::Io(std::io::Error::other(e.to_string()))),
}
})
}
/// Process the menu result: print output to stdout and /// Process the menu result: print output to stdout and
/// exit with the appropriate code. /// exit with the appropriate code.
fn handle_result(result: Result<MenuResult, PiklError>, cli: &Cli) { fn handle_result(result: Result<MenuResult, PiklError>, cli: &Cli) {