From f2ca01e187e5d97657d9113cf6f4deb98fd09a3b Mon Sep 17 00:00:00 2001 From: LoricAndre <57358788+LoricAndre@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:35:50 +0200 Subject: [PATCH] feat!: feature-gate listen and image to allow opting out (#1103) * feat!: feature-gate listen and image to allow opting out This is breaking since disabling the default features now also disables those. It is NOT breaking for cli users, only for library ones. * fix: add warn on listener transfer failure --- .githooks/pre-commit | 6 +++- .github/workflows/test.yml | 12 +++++--- ARCHITECTURE.md | 21 +++++++++++++- Cargo.toml | 20 +++++++------ src/bin/main.rs | 5 ++++ src/manpage.rs | 2 ++ src/options.rs | 10 +++++++ src/skim.rs | 57 +++++++++++++++++++++++++++----------- src/tui/app.rs | 3 ++ src/tui/event.rs | 5 ++-- src/tui/preview.rs | 12 ++++++++ src/tui/preview_tests.rs | 9 ++++++ tests/listen.rs | 2 +- 13 files changed, 131 insertions(+), 33 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 949f87f0..d333f564 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -2,4 +2,8 @@ set -xeuo pipefail cargo +nightly fmt --check --all cargo clippy --all-targets -- -Dwarnings -cargo check --no-default-features +cargo clippy --no-default-features -- -Dwarnings +cargo clippy --no-default-features --features cli -- -Dwarnings +cargo clippy --no-default-features --features image -- -Dwarnings +cargo clippy --no-default-features --features listen -- -Dwarnings +cargo clippy --no-default-features --features frizbee -- -Dwarnings diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c33436e9..4171c5fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -138,6 +138,7 @@ jobs: path: target/llvm-cov/html deploy-coverage-page: + if: *if-master needs: coverage runs-on: ubuntu-latest permissions: @@ -149,7 +150,6 @@ jobs: steps: - name: "Deploy gh pages" id: deployment - if: *if-master uses: actions/deploy-pages@v4 clippy: @@ -174,7 +174,7 @@ jobs: run: | cargo fmt --all -- --check - build-no-default-features: + clippy-no-default-features: runs-on: ${{matrix.os}} strategy: matrix: *matrix @@ -182,9 +182,13 @@ jobs: - *checkout - *toolchain - *cache - - name: Build without any feature + - name: Run clippy with specific features run: | - cargo build --no-default-features + cargo clippy --no-default-features -- -Dwarnings + cargo clippy --no-default-features --features cli -- -Dwarnings + cargo clippy --no-default-features --features image -- -Dwarnings + cargo clippy --no-default-features --features listen -- -Dwarnings + cargo clippy --no-default-features --features frizbee -- -Dwarnings msrv: diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e504408a..c224d055 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -169,6 +169,17 @@ The single crate exports: The `cli` feature gates `clap`, `clap_complete`, `shlex`, `env_logger`, and `clap_mangen`. +The `image` feature (enabled by default) gates image preview support, including the +`image` and `ratatui-image` dependencies, the `ImageProtocol` enum, the +`SkimOptions::image` / `SkimOptions::image_picker` fields, and the +`PreviewContent::Image` rendering path. With the feature off, the `--image` flag and +its supporting code are compiled out entirely and neither image crate is pulled in. + +The `listen` feature (enabled by default) gates the IPC socket that lets other processes +drive skim via `--listen` / `--remote`, including the `interprocess`, `ron`, and `serde` +dependencies, the `SkimOptions::listen` / `SkimOptions::remote` fields, and the `serde` +derives on `Action`. See [IPC / Listen Socket](#ipc--listen-socket). + --- ## Entry Points @@ -799,7 +810,7 @@ Pre-selection is applied when items first appear: `DefaultSkimSelector::should_s **PTY mode** (`--preview-window pty`): creates a real pseudo-terminal pair via `portable_pty`. The child process sees a properly sized terminal (via `ROWS`/`COLUMNS` env and PTY dimensions). Output is parsed by a `vt100::Parser` with a scrollback buffer, stored as `PreviewContent::Terminal(Arc>)`. This enables interactive preview programs (e.g. `bat`, `delta`). -**Image mode** (`--image[=detect|halfblocks]`): treats the expanded preview command as an image path instead of executing it. A worker thread decodes the image with the `image` crate and stores `PreviewContent::Image { source, protocol, size }`. Rendering uses `ratatui_image`; `detect` builds an image protocol picker after entering the alternate screen, while `halfblocks` skips terminal capability detection and uses the portable half-block renderer. The protocol is rebuilt when the preview area changes so the image keeps its aspect ratio within the pane. +**Image mode** (`--image[=detect|halfblocks]`, requires the default `image` feature): treats the expanded preview command as an image path instead of executing it. A worker thread decodes the image with the `image` crate and stores `PreviewContent::Image { source, protocol, size }`. Rendering uses `ratatui_image`; `detect` builds an image protocol picker after entering the alternate screen, while `halfblocks` skips terminal capability detection and uses the portable half-block renderer. The protocol is rebuilt when the preview area changes so the image keeps its aspect ratio within the pane. `Preview::spawn()`: ``` @@ -996,6 +1007,14 @@ Exit codes: `0` = items selected, `1` = no items selected, `130` = abort, `135` ## IPC / Listen Socket +This subsystem is gated behind the default `listen` Cargo feature, which also pulls in the +`interprocess`, `ron`, and `serde` dependencies. With the feature off, the `--listen` / +`--remote` flags, the `Skim::listener` field, the `select!` listener branch, and the +`serde` derives on `Action` are all compiled out. The `select!` branch cannot carry a +`#[cfg]` attribute (tokio's macro rejects it), so it stays in place but its future becomes +a never-resolving `pending()` and its handler is unreachable (the stream type alias +`RemoteStream` is uninhabited). + When `--listen ` is set, `Skim::init_listener()` creates an `interprocess` local socket. The main event loop's `select!` accepts connections and spawns Tokio tasks to read RON-encoded `Action` values line by line: ``` diff --git a/Cargo.toml b/Cargo.toml index 6fbba45d..e23e6682 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,11 +24,15 @@ required-features = ["cli"] [features] # Default is destined to the CLI, not to library usage. -default = ["cli", "frizbee"] +default = ["cli", "frizbee", "image", "listen"] # Everything needed to use skim as a cli (argument parsing, shell integrations...). This should not be needed for most libraries. -cli = ["dep:clap", "dep:clap_complete", "dep:shlex", "dep:env_logger", "dep:clap_mangen"] +cli = ["dep:clap", "dep:clap_complete", "dep:clap_complete_nushell", "dep:shlex", "dep:env_logger", "dep:clap_mangen"] # Include frizbee as a matching algorithm frizbee = ["dep:frizbee"] +# Enable image previews (renders the preview argument as an image) +image = ["dep:image", "dep:ratatui-image"] +# Enable the IPC socket (--listen / --remote), driving skim from other processes +listen = ["dep:interprocess", "dep:ron", "dep:serde"] # Enable gungraun (Valgrind-based) benchmarks gungraun = ["dep:gungraun"] @@ -61,7 +65,7 @@ ansi-to-tui = "8.0.1" assert_enum_variants = "0.1.2" clap = { version = "4.6.1" , optional = true, features = ["cargo", "derive", "unstable-markdown"] } clap_complete = { version = "4.6.5", optional = true } -clap_complete_nushell = "4.6.0" +clap_complete_nushell = { version = "4.6.0", optional = true } clap_mangen = { version = "0.3.0", optional = true } color-eyre = "0.6.5" # Crossterm's version is selected by ratatui @@ -71,9 +75,9 @@ derive_more = { version = "2.1.1", features = ["debug", "eq"] } env_logger = { version = "0.11.10", optional = true, features = ["humantime"] } futures = "0.3.32" gungraun = { version = "0.19.1", optional = true } -image = "0.25.10" +image = { version = "0.25.10", optional = true } indexmap = "2.13.1" -interprocess = { version = "2.4.2", features = ["tokio"] } +interprocess = { version = "2.4.2", features = ["tokio"], optional = true } kanal = "0.1.1" log = "0.4.31" memchr = "2.8.1" @@ -81,11 +85,11 @@ mimalloc = { version = "0.1.48", features = ["v3"] } nix = { version = "0.31.3", features = ["fs", "poll"] } portable-pty = "0.9.0" ratatui = "0.30.0" -ratatui-image = { version = "11.0.4", features = ["image-defaults", "crossterm"], default-features = false } +ratatui-image = { version = "11.0.4", features = ["image-defaults", "crossterm"], default-features = false, optional = true } regex = "1.12.3" roff = "1.1.1" -ron = "0.12.1" -serde = { version = "1.0.228", features = ["derive"] } +ron = { version = "0.12.1", optional = true } +serde = { version = "1.0.228", features = ["derive"], optional = true } shell-quote = "0.7.2" shlex = { version = "2.0.1", optional = true } tempfile = "3.27.0" diff --git a/src/bin/main.rs b/src/bin/main.rs index 3a593b74..418cb524 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -11,10 +11,14 @@ extern crate skim; use color_eyre::Result; use color_eyre::eyre::eyre; +#[cfg(feature = "listen")] use interprocess::bound_util::RefWrite; +#[cfg(feature = "listen")] use interprocess::local_socket::ToNsName as _; +#[cfg(feature = "listen")] use interprocess::local_socket::traits::Stream as _; use log::trace; +#[cfg(feature = "listen")] use skim::binds::parse_action_chain; use skim::reader::CommandCollector; use std::fs::File; @@ -90,6 +94,7 @@ fn main() -> Result<()> { return Ok(()); } + #[cfg(feature = "listen")] if let Some(remote) = opts.remote { let ns_name = remote .to_ns_name::() diff --git a/src/manpage.rs b/src/manpage.rs index 24ecb381..0e4239ec 100644 --- a/src/manpage.rs +++ b/src/manpage.rs @@ -184,6 +184,7 @@ const ACTIONS_SS: &str = " * yank: ctrl-y "; +#[cfg(feature = "listen")] const REMOTE_SECTION: &str = " skim can be controlled from other processes, using the `--listen` (and optionally `--remote`) flags. @@ -356,6 +357,7 @@ Example: section(&mut custom, "THEME", THEME_SECTION); + #[cfg(feature = "listen")] section(&mut custom, "LISTEN/REMOTE", REMOTE_SECTION); section(&mut custom, "EXIT CODES", EXIT_CODES_SECTION); diff --git a/src/options.rs b/src/options.rs index 27ca865d..9adc601b 100644 --- a/src/options.rs +++ b/src/options.rs @@ -8,6 +8,7 @@ use std::rc::Rc; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use derive_builder::Builder; +#[cfg(feature = "image")] use ratatui_image::picker::Picker; use regex::Regex; @@ -62,6 +63,7 @@ pub enum MatchScheme { } /// Image rendering protocols +#[cfg(feature = "image")] #[derive(Default, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "cli", derive(clap::ValueEnum))] pub enum ImageProtocol { @@ -709,6 +711,7 @@ pub struct SkimOptions { /// /// Note: the backend detection **will not** work when piping data into skim, use /// `SKIM_DEFAULT_COMMAND="find . -type f" sk --image` instead of `find . -type f | sk --image` + #[cfg(feature = "image")] #[cfg_attr( feature = "cli", arg(long, help_heading = "Preview", value_enum, default_missing_value = "detect", num_args=0..) @@ -717,6 +720,7 @@ pub struct SkimOptions { /// Terminal image protocol picker, queried after entering the alternate screen. /// Built from `options.image` and an stdio detection if needed + #[cfg(feature = "image")] #[cfg_attr(feature = "cli", clap(skip))] #[builder(setter(skip))] #[debug(skip)] @@ -842,6 +846,7 @@ pub struct SkimOptions { /// /// The socket expects Actions in Ron format (similar to Rust code), see `./src/tui/event.rs` for all possible Actions /// To write to it, see the `--remote` option or the man page + #[cfg(feature = "listen")] #[cfg_attr(feature = "cli", arg(long, help_heading = "Scripting", default_missing_value = "sk", num_args=0..))] pub listen: Option, @@ -850,6 +855,7 @@ pub struct SkimOptions { /// The commands are read from stdin, one per line, in the same format as the actions in the /// bind flag. They can also be chained using `+` as a separator. /// All other arguments will be ignored + #[cfg(feature = "listen")] #[cfg_attr(feature = "cli", arg(long, help_heading = "Scripting", default_missing_value = "sk", num_args=0..))] pub remote: Option, @@ -1056,7 +1062,9 @@ impl Default for SkimOptions { no_strip_ansi: false, wrap_items: false, multiline: None, + #[cfg(feature = "listen")] listen: None, + #[cfg(feature = "listen")] remote: None, print_header: false, print_current: false, @@ -1115,7 +1123,9 @@ impl Default for SkimOptions { cmd_history_size: 1000, preview: Default::default(), preview_window: PreviewLayout::default(), + #[cfg(feature = "image")] image: None, + #[cfg(feature = "image")] image_picker: None, query: Default::default(), cmd_query: Default::default(), diff --git a/src/skim.rs b/src/skim.rs index 3af68be3..692d6168 100644 --- a/src/skim.rs +++ b/src/skim.rs @@ -5,6 +5,7 @@ use std::time::Duration; use color_eyre::eyre::{self, OptionExt, Result}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +#[cfg(feature = "image")] use ratatui_image::picker::Picker; use tokio::runtime::Handle; use tokio::select; @@ -15,6 +16,13 @@ use crate::tui::event::Action; use crate::tui::{App, Event, Size, TICK_RATE, Tui}; use crate::{SkimItem, SkimItemReceiver, SkimOptions, SkimOutput}; +/// Stream type yielded by the IPC listener. With the `listen` feature disabled the +/// listener branch can never fire, so its payload type is uninhabited. +#[cfg(feature = "listen")] +type RemoteStream = interprocess::local_socket::tokio::Stream; +#[cfg(not(feature = "listen"))] +type RemoteStream = std::convert::Infallible; + /// Main entry point for running skim pub struct Skim>> where @@ -29,6 +37,7 @@ where initial_cmd: String, reader_control: Option, matcher_interval: Option, + #[cfg(feature = "listen")] listener: Option, final_event: Event, final_key: KeyEvent, @@ -175,6 +184,7 @@ where tui: None, reader_control: None, matcher_interval: None, + #[cfg(feature = "listen")] listener: None, final_event: Event::Quit, final_key: KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()), @@ -350,6 +360,7 @@ where .expect("TUI needs to be initialized using Skim::init_tui before entering"); tui.enter_terminal()?; + #[cfg(feature = "image")] if self.app.options.image == Some(crate::options::ImageProtocol::Detect) { if !tui.is_fullscreen { crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?; @@ -466,7 +477,9 @@ where /// Initialize the IPC socket listener /// This needs to be called from an async context despite being sync + #[cfg_attr(not(feature = "listen"), allow(clippy::unnecessary_wraps, clippy::unused_self))] fn init_listener(&mut self) -> Result<()> { + #[cfg(feature = "listen")] if let Some(socket_name) = &self.app.options.listen { self.listener = Some( interprocess::local_socket::ListenerOptions::new() @@ -628,26 +641,38 @@ where self.app.restart_matcher(false); self.try_flush_render(); } + // IPC listener branch. `tokio::select!` does not allow `#[cfg]` on a + // branch, so the branch stays but its body is gated: with the `listen` + // feature off the future is a never-resolving `pending()` and the handler + // is unreachable (`RemoteStream` is uninhabited). Ok(stream) = async { - match &self.listener { - Some(l) => interprocess::local_socket::traits::tokio::Listener::accept(l).await, - None => std::future::pending().await, + #[cfg(feature = "listen")] + if let Some(l) = self.listener.as_ref() { + return interprocess::local_socket::traits::tokio::Listener::accept(l).await; } + std::future::pending::>().await } => { - debug!("Listener accepted a connection"); - let event_tx_clone_ipc = self.tui.as_ref().expect("TUI should be initialized before listening").event_tx.clone(); - tokio::spawn(async move { - use tokio::io::AsyncBufReadExt; - let reader = tokio::io::BufReader::new(stream); - let mut lines = reader.lines(); - while let Ok(Some(line)) = lines.next_line().await { - debug!("listener: got {line}"); - if let Ok(act) = ron::from_str::(&line) { - debug!("listener: parsed into action {act:?}"); - _ = event_tx_clone_ipc.try_send(Event::Action(act)); + #[cfg(feature = "listen")] + { + debug!("Listener accepted a connection"); + let event_tx_clone_ipc = self.tui.as_ref().expect("TUI should be initialized before listening").event_tx.clone(); + tokio::spawn(async move { + use tokio::io::AsyncBufReadExt; + let reader = tokio::io::BufReader::new(stream); + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + debug!("listener: got {line}"); + if let Ok(act) = ron::from_str::(&line) { + debug!("listener: parsed into action {act:?}"); + if let Err(e) = event_tx_clone_ipc.try_send(Event::Action(act)) { + warn!("listener: failed to send action to backend: {e:?}"); + } + } } - } - }); + }); + } + #[cfg(not(feature = "listen"))] + match stream {} } } diff --git a/src/tui/app.rs b/src/tui/app.rs index 6d8b3eda..449b8467 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -466,7 +466,10 @@ impl App { preview, ItemPreview::Global | ItemPreview::Command(_) | ItemPreview::CommandWithPos(_, _) ); + #[cfg(feature = "image")] let quote_cmd = self.options.image.is_none(); + #[cfg(not(feature = "image"))] + let quote_cmd = true; match preview { ItemPreview::Command(cmd) => self.preview.spawn(tui, &self.expand_cmd(&cmd, quote_cmd))?, ItemPreview::Text(t) | ItemPreview::AnsiText(t) => { diff --git a/src/tui/event.rs b/src/tui/event.rs index 021d1c8a..44cef358 100644 --- a/src/tui/event.rs +++ b/src/tui/event.rs @@ -151,7 +151,8 @@ pub enum Event { } /// Actions that can be performed in skim -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "listen", derive(serde::Serialize, serde::Deserialize))] pub enum Action { /// Abort and exit with error Abort, @@ -293,7 +294,7 @@ pub enum Action { #[debug("custom")] #[eq(skip)] #[partial_eq(skip)] - #[serde(skip)] + #[cfg_attr(feature = "listen", serde(skip))] Custom(ActionCallback), } diff --git a/src/tui/preview.rs b/src/tui/preview.rs index 9f0ffdfe..c9522e56 100644 --- a/src/tui/preview.rs +++ b/src/tui/preview.rs @@ -6,7 +6,9 @@ use ratatui::prelude::Backend; use ratatui::style::Stylize; use ratatui::text::{Line, Text}; use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget}; +#[cfg(feature = "image")] use ratatui_image::picker::Picker; +#[cfg(feature = "image")] use ratatui_image::protocol::Protocol as ImageProtocol; use tui_term::vt100; use tui_term::widget::PseudoTerminal; @@ -37,6 +39,7 @@ pub(crate) enum PreviewContent { /// Terminal screen (for PTY previews with cursor positioning) Terminal(Arc>), /// Image + #[cfg(feature = "image")] Image { source: image::DynamicImage, protocol: Option, @@ -90,7 +93,9 @@ pub struct Preview { pub wrap: bool, pty: Option, pty_child: Option>, + #[cfg(feature = "image")] image: bool, + #[cfg(feature = "image")] image_picker: Option, pub total_lines: u16, loading: bool, @@ -104,6 +109,7 @@ impl Default for Preview { } impl Preview { + #[cfg(feature = "image")] fn image_protocol( picker: Option<&Picker>, source: image::DynamicImage, @@ -138,6 +144,7 @@ impl Preview { picker.new_protocol(source, size, ratatui_image::Resize::Scale(None)) } + #[cfg(feature = "image")] pub(crate) fn set_image_picker(&mut self, picker: Option) { self.image_picker = picker; } @@ -331,6 +338,7 @@ impl Preview { let event_tx_clone = tui.event_tx.clone(); let content = self.content.clone(); + #[cfg(feature = "image")] if self.image { let cmd = self.cmd.clone(); @@ -606,6 +614,7 @@ impl Preview { } total_lines } + #[cfg(feature = "image")] fn render_image( &self, mut outer: Block, @@ -667,7 +676,9 @@ impl SkimWidget for Preview { interrupt_tx: None, pty: None, pty_child: None, + #[cfg(feature = "image")] image: options.image.is_some(), + #[cfg(feature = "image")] image_picker: options.image_picker.clone(), total_lines: 0, loading: false, @@ -717,6 +728,7 @@ impl SkimWidget for Preview { match &mut *content { PreviewContent::Text(text) => self.total_lines = self.render_text(block, area, buf, text), PreviewContent::Terminal(parser) => self.total_lines = self.render_pty(block, area, buf, parser.as_ref()), + #[cfg(feature = "image")] PreviewContent::Image { source, protocol, size } => { self.render_image(block, area, buf, source, protocol, size); } diff --git a/src/tui/preview_tests.rs b/src/tui/preview_tests.rs index 3a62cb12..cd1fa6ef 100644 --- a/src/tui/preview_tests.rs +++ b/src/tui/preview_tests.rs @@ -1,13 +1,18 @@ +#[cfg(feature = "image")] use image::{DynamicImage, RgbaImage}; +#[cfg(feature = "image")] use ratatui::layout::Size; +#[cfg(feature = "image")] use ratatui_image::picker::Picker; use super::Preview; +#[cfg(feature = "image")] fn image(width: u32, height: u32) -> DynamicImage { DynamicImage::ImageRgba8(RgbaImage::new(width, height)) } +#[cfg(feature = "image")] #[test] fn image_protocol_constrains_by_width() { let protocol = Preview::image_protocol(Some(&Picker::halfblocks()), image(400, 200), Size::new(20, 10)) @@ -16,6 +21,7 @@ fn image_protocol_constrains_by_width() { assert_eq!(protocol.size(), Size::new(20, 5)); } +#[cfg(feature = "image")] #[test] fn image_protocol_constrains_by_height() { let protocol = Preview::image_protocol(Some(&Picker::halfblocks()), image(200, 400), Size::new(20, 10)) @@ -24,6 +30,7 @@ fn image_protocol_constrains_by_height() { assert_eq!(protocol.size(), Size::new(10, 10)); } +#[cfg(feature = "image")] #[test] fn image_protocol_keeps_at_least_one_cell() { let protocol = Preview::image_protocol(Some(&Picker::halfblocks()), image(1000, 1), Size::new(1, 1)) @@ -32,6 +39,7 @@ fn image_protocol_keeps_at_least_one_cell() { assert_eq!(protocol.size(), Size::new(1, 1)); } +#[cfg(feature = "image")] #[test] fn image_protocol_uses_halfblocks_picker_when_none_is_provided() { let protocol = Preview::image_protocol(None, image(400, 200), Size::new(20, 10)) @@ -160,6 +168,7 @@ fn size_to_offset_resolves_each_variant() { assert_eq!(p.size_to_offset(super::super::Size::Neg(10), false), 70); } +#[cfg(feature = "image")] #[test] fn set_image_picker_sets_and_clears() { let mut p = Preview::default(); diff --git a/tests/listen.rs b/tests/listen.rs index b27924cf..e9ca97d4 100644 --- a/tests/listen.rs +++ b/tests/listen.rs @@ -1,7 +1,7 @@ // TODO: automate listen tests on windows // Maybe using smaller tests ? actions processing is already tested, only the IPC part needs testing #![allow(missing_docs, clippy::pedantic)] -#![cfg(unix)] +#![cfg(all(unix, feature = "listen"))] #[allow(dead_code)] #[macro_use] mod common;