refactor: Eliminate unnecessary empty Vec constructions.

This commit is contained in:
2026-03-14 11:43:48 -04:00
parent 0ebb5e79dd
commit e89c6cb16f
7 changed files with 40 additions and 49 deletions

View File

@@ -29,7 +29,7 @@ pub enum DebounceMode {
/// behavior. Each event kind can have its own mode.
pub struct DebouncedDispatcher {
handler: Arc<dyn HookHandler>,
action_tx: mpsc::Sender<Action>,
_action_tx: mpsc::Sender<Action>,
modes: HashMap<HookEventKind, DebounceMode>,
in_flight: HashMap<HookEventKind, JoinHandle<()>>,
}
@@ -41,7 +41,7 @@ impl DebouncedDispatcher {
) -> Self {
Self {
handler,
action_tx,
_action_tx: action_tx,
modes: HashMap::new(),
in_flight: HashMap::new(),
}
@@ -95,17 +95,9 @@ impl DebouncedDispatcher {
fn fire_now(&self, event: HookEvent) {
let handler = Arc::clone(&self.handler);
let action_tx = self.action_tx.clone();
tokio::spawn(async move {
match handler.handle(event) {
Ok(responses) => {
for resp in responses {
let _ = action_tx.send(Action::ProcessHookResponse(resp)).await;
}
}
Err(e) => {
tracing::warn!(error = %e, "hook handler error");
}
if let Err(e) = handler.handle(event) {
tracing::warn!(error = %e, "hook handler error");
}
});
}
@@ -117,18 +109,10 @@ impl DebouncedDispatcher {
}
let handler = Arc::clone(&self.handler);
let action_tx = self.action_tx.clone();
let handle = tokio::spawn(async move {
tokio::time::sleep(delay).await;
match handler.handle(event) {
Ok(responses) => {
for resp in responses {
let _ = action_tx.send(Action::ProcessHookResponse(resp)).await;
}
}
Err(e) => {
tracing::warn!(error = %e, "hook handler error");
}
if let Err(e) = handler.handle(event) {
tracing::warn!(error = %e, "hook handler error");
}
});
@@ -165,11 +149,11 @@ mod tests {
}
impl HookHandler for RecordingHandler {
fn handle(&self, event: HookEvent) -> Result<Vec<HookResponse>, PiklError> {
fn handle(&self, event: HookEvent) -> Result<(), PiklError> {
if let Ok(mut events) = self.events.lock() {
events.push(event.kind());
}
Ok(vec![])
Ok(())
}
}

View File

@@ -65,16 +65,15 @@ pub enum HookResponse {
}
/// Handler trait for lifecycle hooks. Implementations
/// receive events and optionally return responses.
/// Exec hooks return empty vecs. Handler hooks send
/// responses back through the action channel asynchronously
/// and also return empty vecs.
/// receive events and may produce side effects (spawning
/// processes, sending to channels). Responses flow through
/// the action channel, not the return value.
///
/// This is deliberately synchronous for dyn-compatibility.
/// Implementations that need async work (spawning processes,
/// writing to channels) should use `tokio::spawn` internally.
pub trait HookHandler: Send + Sync {
fn handle(&self, event: HookEvent) -> Result<Vec<HookResponse>, PiklError>;
fn handle(&self, event: HookEvent) -> Result<(), PiklError>;
}
/// Parse a single line of JSON as a [`HookResponse`].

View File

@@ -11,7 +11,7 @@ use crate::debounce::{hook_response_to_action, DebouncedDispatcher};
use crate::error::PiklError;
use crate::event::{Action, MenuEvent, MenuResult, Mode, ViewState, VisibleItem};
use crate::hook::{HookEvent, HookHandler};
use crate::model::traits::Menu;
use crate::model::traits::MutableMenu;
use crate::navigation::Viewport;
use serde_json::Value;
@@ -37,7 +37,7 @@ pub enum ActionOutcome {
/// drives it with an action/event channel loop. Create one,
/// grab the action sender and event subscriber, then call
/// [`MenuRunner::run`] to start the event loop.
pub struct MenuRunner<M: Menu> {
pub struct MenuRunner<M: MutableMenu> {
menu: M,
viewport: Viewport,
filter_text: Arc<str>,
@@ -46,9 +46,10 @@ pub struct MenuRunner<M: Menu> {
event_tx: broadcast::Sender<MenuEvent>,
dispatcher: Option<DebouncedDispatcher>,
previous_cursor: Option<usize>,
generation: u64,
}
impl<M: Menu> MenuRunner<M> {
impl<M: MutableMenu> MenuRunner<M> {
/// Create a menu runner wrapping the given menu backend.
/// Returns the runner and an action sender. Call
/// [`subscribe`](Self::subscribe) to get an event handle,
@@ -69,6 +70,7 @@ impl<M: Menu> MenuRunner<M> {
event_tx,
dispatcher: None,
previous_cursor: None,
generation: 0,
};
(runner, action_tx)
}
@@ -105,7 +107,8 @@ impl<M: Menu> MenuRunner<M> {
/// Build a [`ViewState`] snapshot from the current filter
/// results and viewport position.
fn build_view_state(&self) -> ViewState {
fn build_view_state(&mut self) -> ViewState {
self.generation += 1;
let range = self.viewport.visible_range();
let visible_items: Vec<VisibleItem> = range
.clone()
@@ -134,14 +137,14 @@ impl<M: Menu> MenuRunner<M> {
total_items: self.menu.total(),
total_filtered: self.menu.filtered_count(),
mode: self.mode,
generation: self.generation,
}
}
/// Send the current view state to all subscribers.
fn broadcast_state(&self) {
let _ = self
.event_tx
.send(MenuEvent::StateChanged(self.build_view_state()));
fn broadcast_state(&mut self) {
let vs = self.build_view_state();
let _ = self.event_tx.send(MenuEvent::StateChanged(vs));
}
/// Emit a hook event through the dispatcher, if one is set.
@@ -390,6 +393,7 @@ mod tests {
use super::*;
use crate::event::MenuEvent;
use crate::item::Item;
use crate::model::traits::Menu;
use crate::runtime::json_menu::JsonMenu;
fn test_menu() -> (MenuRunner<JsonMenu>, mpsc::Sender<Action>) {
@@ -1099,16 +1103,16 @@ mod tests {
#[tokio::test]
async fn hook_events_fire_on_lifecycle() {
use crate::hook::{HookEvent, HookEventKind, HookHandler, HookResponse};
use crate::hook::{HookEvent, HookEventKind, HookHandler};
use std::sync::Mutex;
struct Recorder(Mutex<Vec<HookEventKind>>);
impl HookHandler for Recorder {
fn handle(&self, event: HookEvent) -> Result<Vec<HookResponse>, PiklError> {
fn handle(&self, event: HookEvent) -> Result<(), PiklError> {
if let Ok(mut v) = self.0.lock() {
v.push(event.kind());
}
Ok(vec![])
Ok(())
}
}

View File

@@ -13,7 +13,7 @@ use tokio::sync::mpsc;
use pikl_core::error::PiklError;
use pikl_core::event::Action;
use pikl_core::hook::{
parse_hook_response, HookEvent, HookEventKind, HookHandler, HookResponse,
parse_hook_response, HookEvent, HookEventKind, HookHandler,
};
/// A persistent handler hook process. Spawns a child process,
@@ -77,13 +77,13 @@ impl ShellHandlerHook {
}
impl HookHandler for ShellHandlerHook {
fn handle(&self, event: HookEvent) -> Result<Vec<HookResponse>, PiklError> {
fn handle(&self, event: HookEvent) -> Result<(), PiklError> {
let kind = event.kind();
if let Some(tx) = self.event_txs.get(&kind) {
// Non-blocking send. If the channel is full, drop the event.
let _ = tx.try_send(event);
}
Ok(vec![])
Ok(())
}
}

View File

@@ -9,7 +9,7 @@ use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use pikl_core::error::PiklError;
use pikl_core::hook::{HookEvent, HookEventKind, HookHandler, HookResponse};
use pikl_core::hook::{HookEvent, HookEventKind, HookHandler};
/// Duplicate stderr as a [`Stdio`] handle for use as a
/// child process's stdout. Keeps hook output on stderr
@@ -18,8 +18,12 @@ fn stderr_as_stdio() -> std::process::Stdio {
#[cfg(unix)]
{
use std::os::unix::io::FromRawFd;
// SAFETY: STDERR_FILENO is a valid open fd in any running process.
// dup() returns a new fd or -1, which we check below.
let fd = unsafe { libc::dup(libc::STDERR_FILENO) };
if fd >= 0 {
// SAFETY: fd is valid (checked >= 0 above) and we take exclusive
// ownership here (no aliasing). The File will close it on drop.
return unsafe { std::process::Stdio::from(std::fs::File::from_raw_fd(fd)) };
}
}
@@ -112,7 +116,7 @@ impl ShellExecHandler {
}
impl HookHandler for ShellExecHandler {
fn handle(&self, event: HookEvent) -> Result<Vec<HookResponse>, PiklError> {
fn handle(&self, event: HookEvent) -> Result<(), PiklError> {
let kind = event.kind();
if let Some(cmd) = self.commands.get(&kind) {
let cmd = cmd.clone();
@@ -125,7 +129,7 @@ impl HookHandler for ShellExecHandler {
}
});
}
Ok(vec![])
Ok(())
}
}

View File

@@ -310,14 +310,14 @@ impl HookHandler for CompositeHookHandler {
fn handle(
&self,
event: pikl_core::hook::HookEvent,
) -> Result<Vec<pikl_core::hook::HookResponse>, PiklError> {
) -> Result<(), PiklError> {
// Both fire. Exec is fire-and-forget, handler may
// send responses through action_tx.
let _ = self.exec.handle(event.clone());
if let Some(ref h) = self.handler {
h.handle(event)?;
}
Ok(vec![])
Ok(())
}
}