Repository: zhiburt/expectrl (language: rust)

Issue:
Capture waidpid result in interact()
Today when using `interact` the exit code is lost because `is_alive` will swallow it. It would be nice if the session could hold on to the `status()` so that it can be called again later to retrieve the first observed status.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index ab77c7f..1ca8dde 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "expectrl"
-version = "0.6.0"
+version = "0.7.0"
 authors = ["Maxim Zhiburt <zhiburt@gmail.com>"]
 edition = "2021"
 resolver = "2"
@@ -23,8 +23,8 @@ futures-lite = { version = "1.12.0", optional = true }
 futures-timer = { version = "3.0.2", optional = true }
 
 [target.'cfg(unix)'.dependencies]
-ptyprocess = "0.3.0"
-nix = "0.25.0"
+ptyprocess = "0.4.1"
+nix = "0.26"
 async-io = { version = "1.9.0", optional = true }
 polling = { version = "2.3.0", optional = true }
 
diff --git a/examples/check.rs b/examples/check.rs
index df12a7c..9968513 100644
--- a/examples/check.rs
+++ b/examples/check.rs
@@ -1,7 +1,5 @@
-#[cfg(unix)]
 use expectrl::{check, spawn, Error};
 
-#[cfg(unix)]
 #[cfg(not(feature = "async"))]
 fn main() {
     let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session");
@@ -24,7 +22,6 @@ fn main() {
     }
 }
 
-#[cfg(unix)]
 #[cfg(feature = "async")]
 fn main() {
     futures_lite::future::block_on(async {
@@ -50,8 +47,3 @@ fn main() {
         }
     })
 }
-
-#[cfg(windows)]
-fn main() {
-    panic!("An example doesn't supported on windows")
-}
diff --git a/examples/interact.rs b/examples/interact.rs
index 3d24f55..2623187 100644
--- a/examples/interact.rs
+++ b/examples/interact.rs
@@ -1,6 +1,6 @@
 //! To run an example run `cargo run --example interact`.
 
-use expectrl::{spawn, stream::stdin::Stdin};
+use expectrl::{interact::InteractOptions, spawn, stream::stdin::Stdin};
 use std::io::stdout;
 
 #[cfg(unix)]
@@ -9,7 +9,7 @@ const SHELL: &str = "sh";
 #[cfg(windows)]
 const SHELL: &str = "powershell";
 
-#[cfg(all(not(feature = "async"), not(all(windows, feature = "polling"))))]
+#[cfg(not(feature = "async"))]
 fn main() {
     let mut sh = spawn(SHELL).expect("Error while spawning sh");
 
@@ -19,7 +19,7 @@ fn main() {
     let mut stdin = Stdin::open().expect("Failed to create stdin");
 
     sh.interact(&mut stdin, stdout())
-        .spawn()
+        .spawn(&mut InteractOptions::default())
         .expect("Failed to start interact");
 
     stdin.close().expect("Failed to close a stdin");
@@ -29,37 +29,21 @@ fn main() {
 
 #[cfg(feature = "async")]
 fn main() {
-    use futures_lite::future::block_on;
+    futures_lite::future::block_on(async {
+        let mut sh = spawn(SHELL).expect("Error while spawning sh");
 
-    let mut sh = spawn(SHELL).expect("Error while spawning sh");
-
-    println!("Now you're in interacting mode");
-    println!("To return control back to main type CTRL-] combination");
+        println!("Now you're in interacting mode");
+        println!("To return control back to main type CTRL-] combination");
 
-    let mut stdin = Stdin::open().expect("Failed to create stdin");
+        let mut stdin = Stdin::open().expect("Failed to create stdin");
 
-    block_on(sh.interact(&mut stdin, stdout()).spawn()).expect("Failed to start interact");
+        sh.interact(&mut stdin, stdout())
+            .spawn(&mut InteractOptions::default())
+            .await
+            .expect("Failed to start interact");
 
-    stdin.close().expect("Failed to close a stdin");
+        stdin.close().expect("Failed to close a stdin");
 
-    println!("Exiting");
-}
-
-#[cfg(all(windows, feature = "polling"))]
-fn main() {
-    let mut sh = spawn(SHELL).expect("Error while spawning sh");
-
-    println!("Now you're in interacting mode");
-    println!("To return control back to main type CTRL-] combination");
-
-    let stdin = Stdin::open().expect("Failed to create stdin");
-
-    sh.interact(stdin, stdout())
-        .spawn()
-        .expect("Failed to start interact");
-
-    let stdin = Stdin::open().expect("Failed to create stdin");
-    stdin.close().expect("Failed to close a stdin");
-
-    println!("Exiting");
+        println!("Exiting");
+    });
 }
diff --git a/examples/interact_with_callback.rs b/examples/interact_with_callback.rs
index fdaaa5a..9888a8b 100644
--- a/examples/interact_with_callback.rs
+++ b/examples/interact_with_callback.rs
@@ -1,4 +1,9 @@
-use expectrl::{interact::actions::lookup::Lookup, spawn, stream::stdin::Stdin, Regex};
+use expectrl::{
+    interact::{actions::lookup::Lookup, InteractOptions},
+    spawn,
+    stream::stdin::Stdin,
+    Regex,
+};
 
 #[derive(Debug, Default)]
 struct State {
@@ -7,19 +12,12 @@ struct State {
     pressed_yes_on_continue: Option<bool>,
 }
 
-#[cfg(all(not(feature = "async"), not(all(windows, feature = "polling"))))]
+#[cfg(not(feature = "async"))]
 fn main() {
     let mut output_action = Lookup::new();
     let mut input_action = Lookup::new();
-
-    let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session");
-
-    let mut stdin = Stdin::open().unwrap();
-    let stdout = std::io::stdout();
-
-    let state = session
-        .interact(&mut stdin, stdout)
-        .set_state(State::default())
+    let mut state = State::default();
+    let opts = InteractOptions::new(&mut state)
         .on_output(|mut ctx| {
             let m = output_action.on(ctx.buf, ctx.eof, "Continue [y/n]:")?;
             if m.is_some() {
@@ -51,75 +49,26 @@ fn main() {
             }
 
             Ok(())
-        })
-        .spawn()
-        .expect("Failed to start interact");
-
-    stdin.close().unwrap();
-
-    println!("RESULTS...........");
-    println!(
-        "Number of time 'Y' was pressed = {}",
-        state.pressed_yes_on_continue.unwrap_or_default()
-    );
-    println!(
-        "Status counter = {}",
-        state.stutus_verification_counter.unwrap_or_default()
-    );
-}
-
-#[cfg(feature = "async")]
-fn main() {
-    use futures_lite::future::block_on;
-
-    let mut output_action = Lookup::new();
-    let mut input_action = Lookup::new();
+        });
 
     let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session");
 
     let mut stdin = Stdin::open().unwrap();
     let stdout = std::io::stdout();
 
-    let interact = session
-        .interact(&mut stdin, stdout)
-        .set_state(State::default())
-        .on_output(|mut ctx| {
-            let m = output_action.on(ctx.buf, ctx.eof, "Continue [y/n]:")?;
-            if m.is_some() {
-                ctx.state.wait_for_continue = Some(true);
-            };
-
-            let m = output_action.on(ctx.buf, ctx.eof, Regex("status:\\s*.*\\w+.*\\r\\n"))?;
-            if m.is_some() {
-                ctx.state.stutus_verification_counter =
-                    Some(ctx.state.stutus_verification_counter.map_or(1, |c| c + 1));
-            }
-
-            Ok(())
-        })
-        .on_input(|mut ctx| {
-            let m = input_action.on(ctx.buf, ctx.eof, "y")?;
-            if m.is_some() {
-                if let Some(_a @ true) = ctx.state.wait_for_continue {
-                    ctx.state.pressed_yes_on_continue = Some(true);
-                }
-            };
-
-            let m = input_action.on(ctx.buf, ctx.eof, "n")?;
-            if m.is_some() {
-                if let Some(_a @ true) = ctx.state.wait_for_continue {
-                    ctx.state.pressed_yes_on_continue = Some(false);
-                }
-            }
+    let mut interact = session.interact(&mut stdin, stdout);
 
-            Ok(())
-        });
+    let is_alive = interact.spawn(opts).expect("Failed to start interact");
 
-    let state = block_on(interact.spawn()).expect("Failed to start interact");
+    if !is_alive {
+        println!("The process was exited");
+        #[cfg(unix)]
+        println!("Status={:?}", interact.get_status());
+    }
 
     stdin.close().unwrap();
 
-    println!("RESULTS...........");
+    println!("RESULTS");
     println!(
         "Number of time 'Y' was pressed = {}",
         state.pressed_yes_on_continue.unwrap_or_default()
@@ -130,19 +79,12 @@ fn main() {
     );
 }
 
-#[cfg(all(windows, feature = "polling"))]
+#[cfg(feature = "async")]
 fn main() {
     let mut output_action = Lookup::new();
     let mut input_action = Lookup::new();
-
-    let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session");
-
-    let stdin = Stdin::open().unwrap();
-    let stdout = std::io::stdout();
-
-    let state = session
-        .interact(stdin, stdout)
-        .set_state(State::default())
+    let mut state = State::default();
+    let opts = InteractOptions::new(&mut state)
         .on_output(|mut ctx| {
             let m = output_action.on(ctx.buf, ctx.eof, "Continue [y/n]:")?;
             if m.is_some() {
@@ -174,14 +116,27 @@ fn main() {
             }
 
             Ok(())
-        })
-        .spawn()
-        .expect("Failed to start interact");
+        });
+
+    let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session");
+
+    let mut stdin = Stdin::open().unwrap();
+    let stdout = std::io::stdout();
+
+    let mut interact = session.interact(&mut stdin, stdout);
+
+    let is_alive =
+        futures_lite::future::block_on(interact.spawn(opts)).expect("Failed to start interact");
+
+    if !is_alive {
+        println!("The process was exited");
+        #[cfg(unix)]
+        println!("Status={:?}", interact.get_status());
+    }
 
-    let stdin = Stdin::open().unwrap();
     stdin.close().unwrap();
 
-    println!("RESULTS...........");
+    println!("RESULTS");
     println!(
         "Number of time 'Y' was pressed = {}",
         state.pressed_yes_on_continue.unwrap_or_default()
diff --git a/examples/bash_control.rs b/examples/ping.rs
similarity index 100%
rename from examples/bash_control.rs
rename to examples/ping.rs
diff --git a/src/error.rs b/src/error.rs
index aa76395..46337ad 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -27,6 +27,7 @@ pub enum Error {
 }
 
 impl Error {
+    #[cfg(unix)]
     pub(crate) fn unknown(message: impl Into<String>, err: impl Into<String>) -> Error {
         Self::Other {
             message: message.into(),
diff --git a/src/interact/context.rs b/src/interact/context.rs
index 5a874b9..a6e7996 100644
--- a/src/interact/context.rs
+++ b/src/interact/context.rs
@@ -6,28 +6,40 @@
 /// [`InteractSession`]: crate::interact::InteractSession
 /// [`Session`]: crate::session::Session
 #[derive(Debug)]
-pub struct Context<'a, Session, Output, State> {
+pub struct Context<'a, Session, Input, Output, State> {
     /// The field contains a &mut reference to a [`Session`].
     ///
     /// [`Session`]: crate::session::Session
-    pub session: Session,
+    pub session: &'a mut Session,
+    /// The field contains an input structure which was used in [`InteractSession`].
+    ///
+    /// [`InteractSession`]: crate::interact::InteractSession
+    pub input: &'a mut Input,
     /// The field contains an output structure which was used in [`InteractSession`].
     ///
     /// [`InteractSession`]: crate::interact::InteractSession
-    pub output: Output,
+    pub output: &'a mut Output,
+    /// The field contains a user defined data.
+    pub state: &'a mut State,
     /// The field contains a bytes which were consumed from a user or the running process.
     pub buf: &'a [u8],
     /// A flag for EOF of a user session or running process.
     pub eof: bool,
-    /// The field contains a user defined data.
-    pub state: State,
 }
 
-impl<'a, Session, Output, State> Context<'a, Session, Output, State> {
+impl<'a, Session, Input, Output, State> Context<'a, Session, Input, Output, State> {
     /// Creates a new [`Context`] structure.
-    pub fn new(session: Session, output: Output, buf: &'a [u8], eof: bool, state: State) -> Self {
+    pub fn new(
+        session: &'a mut Session,
+        input: &'a mut Input,
+        output: &'a mut Output,
+        state: &'a mut State,
+        buf: &'a [u8],
+        eof: bool,
+    ) -> Self {
         Self {
             session,
+            input,
             output,
             buf,
             eof,
diff --git a/src/interact/mod.rs b/src/interact/mod.rs
index 46ced7d..8114a53 100644
--- a/src/interact/mod.rs
+++ b/src/interact/mod.rs
@@ -2,7 +2,7 @@
 //!
 #![cfg_attr(all(unix, not(feature = "async")), doc = "```no_run")]
 #![cfg_attr(not(all(unix, not(feature = "async"))), doc = "```ignore")]
-//! use expectrl::{interact::actions::lookup::Lookup, spawn, stream::stdin::Stdin, Regex};
+//! use expectrl::{interact::{InteractOptions, actions::lookup::Lookup}, spawn, stream::stdin::Stdin, Regex};
 //!
 //! #[derive(Debug)]
 //! enum Answer {
@@ -18,9 +18,7 @@
 //! let mut stdin = Stdin::open().unwrap();
 //! let stdout = std::io::stdout();
 //!
-//! let answer = session
-//!     .interact(&mut stdin, stdout)
-//!     .set_state(Answer::Unrecognized)
+//! let mut opts = InteractOptions::new(Answer::Unrecognized)
 //!     .on_input(|mut ctx| {
 //!         let m = input_action.on(ctx.buf, ctx.eof, "yes")?;
 //!         if m.is_some() {
@@ -33,10 +31,14 @@
 //!         };
 //!
 //!         Ok(())
-//!     })
-//!     .spawn()
+//!     });
+//!
+//! session.interact(&mut stdin, stdout)
+//!     .spawn(&mut opts)
 //!     .expect("Failed to run an interact session");
 //!
+//! let answer = opts.into_inner();
+//!
 //! stdin.close().unwrap();
 //!
 //! println!("It was said {:?}", answer);
@@ -47,6 +49,8 @@
 pub mod actions;
 mod context;
 mod opts;
+mod session;
 
 pub use context::Context;
-pub use opts::{InteractSession, NoAction, NoFilter};
+pub use opts::{InteractOptions, NoAction, NoFilter};
+pub use session::InteractSession;
diff --git a/src/interact/opts.rs b/src/interact/opts.rs
index fb32034..4277502 100644
--- a/src/interact/opts.rs
+++ b/src/interact/opts.rs
@@ -1,201 +1,81 @@
-#![allow(clippy::type_complexity)]
-
-//! This module contains a [`InteractSession`] which runs an interact session with IO.
-
-use std::{borrow::Cow, io::Write};
-
-use crate::{process::Healthcheck, session::Proc, ControlCode, Error, Session};
-
-#[cfg(not(feature = "async"))]
-use std::io::{self, Read};
-
-#[cfg(all(not(feature = "async"), not(feature = "polling")))]
-use crate::process::NonBlocking;
+use std::borrow::Cow;
 
 use super::Context;
+use crate::{session::Proc, Error, Session};
+
+type Result<T> = std::result::Result<T, Error>;
 
-/// InteractConfig represents options of an interactive session.
+/// Interact options (aka callbacks you can set to be callled being in an interactive mode).
 #[derive(Debug)]
-pub struct InteractSession<
-    'a,
-    State,
-    Session,
-    Output,
-    Input,
-    InputFilter,
-    OutputFilter,
-    InputAction,
-    OutputAction,
-    IdleAction,
-> {
-    pub(crate) session: &'a mut Session,
-    pub(crate) output: Output,
-    pub(crate) input: Input,
-    pub(crate) state: State,
-    pub(crate) escape_character: u8,
-    pub(crate) input_filter: Option<InputFilter>,
-    pub(crate) output_filter: Option<OutputFilter>,
-    pub(crate) input_action: Option<InputAction>,
-    pub(crate) output_action: Option<OutputAction>,
-    pub(crate) idle_action: Option<IdleAction>,
+pub struct InteractOptions<C, IF, OF, IA, OA, WA> {
+    pub(crate) state: C,
+    pub(crate) input_filter: Option<IF>,
+    pub(crate) output_filter: Option<OF>,
+    pub(crate) input_action: Option<IA>,
+    pub(crate) output_action: Option<OA>,
+    pub(crate) idle_action: Option<WA>,
 }
 
-impl<'a, State, Session, Output, Input>
-    InteractSession<
-        'a,
-        State,
-        Session,
-        Output,
-        Input,
-        NoFilter,
-        NoFilter,
-        NoAction<Session, Output, State>,
-        NoAction<Session, Output, State>,
-        NoAction<Session, Output, State>,
-    >
-{
-    /// Creates a new object of [InteractSession].
-    pub(crate) fn new(
-        session: &'a mut Session,
-        output: Output,
-        input: Input,
-        state: State,
-    ) -> InteractSession<
-        '_,
-        State,
-        Session,
-        Output,
-        Input,
-        NoFilter,
-        NoFilter,
-        NoAction<Session, Output, State>,
-        NoAction<Session, Output, State>,
-        NoAction<Session, Output, State>,
-    > {
-        InteractSession {
-            input,
-            output,
-            session,
-            state,
-            escape_character: Self::default_escape_char(),
-            input_filter: None,
-            output_filter: None,
-            input_action: None,
-            output_action: None,
-            idle_action: None,
-        }
+type DefaultOps<S, I, O, C> = InteractOptions<
+    C,
+    NoFilter,
+    NoFilter,
+    NoAction<Session<Proc, S>, I, O, C>,
+    NoAction<Session<Proc, S>, I, O, C>,
+    NoAction<Session<Proc, S>, I, O, C>,
+>;
+
+impl<S, I, O> Default for DefaultOps<S, I, O, ()> {
+    fn default() -> Self {
+        Self::new(())
     }
 }
 
-impl<
-        'a,
-        State,
-        Session,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        InputAction,
-        OutputAction,
-        IdleAction,
-    >
-    InteractSession<
-        'a,
-        State,
-        Session,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        InputAction,
-        OutputAction,
-        IdleAction,
-    >
-{
+impl<S, I, O, C> DefaultOps<S, I, O, C> {
     /// Set a state.
-    pub fn set_state<S>(
-        self,
-        state: S,
-    ) -> InteractSession<
-        'a,
-        S,
-        Session,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        NoAction<Session, Output, S>,
-        NoAction<Session, Output, S>,
-        NoAction<Session, Output, S>,
-    > {
-        InteractSession {
+    pub fn new(state: C) -> Self {
+        Self {
             state,
-            input: self.input,
-            output: self.output,
-            session: self.session,
-            escape_character: self.escape_character,
-            input_filter: self.input_filter,
-            output_filter: self.output_filter,
+            input_filter: None,
+            output_filter: None,
             input_action: None,
             output_action: None,
             idle_action: None,
         }
     }
+}
 
+impl<C, IF, OF, IA, OA, WA> InteractOptions<C, IF, OF, IA, OA, WA> {
     /// Get a reference on state
-    pub fn get_state(&self) -> &State {
+    pub fn get_state(&self) -> &C {
         &self.state
     }
 
     /// Get a mut reference on state
-    pub fn get_state_mut(&mut self) -> &mut State {
+    pub fn get_state_mut(&mut self) -> &mut C {
         &mut self.state
     }
 
-    /// Sets an escape character after seen which the interact interactions will be stopped
-    /// and controll will be returned to a caller process.
-    pub fn set_escape_character(mut self, c: u8) -> Self {
-        self.escape_character = c;
-        self
-    }
-
-    fn default_escape_char() -> u8 {
-        ControlCode::GroupSeparator.into() // Ctrl-]
+    /// Returns a inner state.
+    pub fn into_inner(self) -> C {
+        self.state
     }
 
     /// Sets the output filter.
     /// The output_filter will be passed all the output from the child process.
     ///
     /// The filter isn't applied to user's `read` calls through the [`Context`] in callbacks.
-    pub fn output_filter<Filter>(
-        self,
-        f: Filter,
-    ) -> InteractSession<
-        'a,
-        State,
-        Session,
-        Output,
-        Input,
-        InputFilter,
-        Filter,
-        InputAction,
-        OutputAction,
-        IdleAction,
-    >
+    pub fn output_filter<F>(self, filter: F) -> InteractOptions<C, IF, F, IA, OA, WA>
     where
-        Filter: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+        F: FnMut(&[u8]) -> Result<Cow<'_, [u8]>>,
     {
-        InteractSession {
-            state: self.state,
-            input: self.input,
-            output: self.output,
-            session: self.session,
-            escape_character: self.escape_character,
+        InteractOptions {
             input_filter: self.input_filter,
-            output_filter: Some(f),
+            output_filter: Some(filter),
             input_action: self.input_action,
             output_action: self.output_action,
             idle_action: self.idle_action,
+            state: self.state,
         }
     }
 
@@ -204,993 +84,88 @@ impl<
     ///
     /// The input_filter is run BEFORE the check for the escape_character.
     /// The filter is called BEFORE calling a on_input callback if it's set.
-    pub fn input_filter<Filter>(
-        self,
-        f: Filter,
-    ) -> InteractSession<
-        'a,
-        State,
-        Session,
-        Output,
-        Input,
-        Filter,
-        OutputFilter,
-        InputAction,
-        OutputAction,
-        IdleAction,
-    >
+    pub fn input_filter<F>(self, filter: F) -> InteractOptions<C, F, OF, IA, OA, WA>
     where
-        Filter: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+        F: FnMut(&[u8]) -> Result<Cow<'_, [u8]>>,
     {
-        InteractSession {
-            state: self.state,
-            input: self.input,
-            output: self.output,
-            session: self.session,
-            escape_character: self.escape_character,
-            input_filter: Some(f),
+        InteractOptions {
+            input_filter: Some(filter),
             output_filter: self.output_filter,
             input_action: self.input_action,
             output_action: self.output_action,
             idle_action: self.idle_action,
+            state: self.state,
         }
     }
+}
 
+impl<S, I, O, C, IF, OF, OA, WA>
+    InteractOptions<C, IF, OF, NoAction<Session<Proc, S>, I, O, C>, OA, WA>
+{
     /// Puts a hanlder which will be called when users input is detected.
     ///
     /// Be aware that currently async version doesn't take a Session as an argument.
     /// See <https://github.com/zhiburt/expectrl/issues/16>.
-    pub fn on_input<Action>(
-        self,
-        f: Action,
-    ) -> InteractSession<
-        'a,
-        State,
-        Session,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        Action,
-        OutputAction,
-        IdleAction,
-    >
+    pub fn on_input<F>(self, action: F) -> InteractOptions<C, IF, OF, F, OA, WA>
     where
-        Action:
-            for<'b> FnMut(Context<'b, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
+        F: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<()>,
     {
-        InteractSession {
-            state: self.state,
-            input: self.input,
-            output: self.output,
-            session: self.session,
-            escape_character: self.escape_character,
+        InteractOptions {
             input_filter: self.input_filter,
             output_filter: self.output_filter,
-            input_action: Some(f),
+            input_action: Some(action),
             output_action: self.output_action,
             idle_action: self.idle_action,
+            state: self.state,
         }
     }
+}
 
+impl<S, I, O, C, IF, OF, IA, WA>
+    InteractOptions<C, IF, OF, IA, NoAction<Session<Proc, S>, I, O, C>, WA>
+{
     /// Puts a hanlder which will be called when process output is detected.
     ///
     /// IMPORTANT:
     ///
     /// Please be aware that your use of [Session::expect], [Session::check] and any `read` operation on session
     /// will cause the read bytes not to apeard in the output stream!
-    pub fn on_output<Action>(
-        self,
-        f: Action,
-    ) -> InteractSession<
-        'a,
-        State,
-        Session,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        InputAction,
-        Action,
-        IdleAction,
-    >
+    pub fn on_output<F>(self, action: F) -> InteractOptions<C, IF, OF, IA, F, WA>
     where
-        Action:
-            for<'b> FnMut(Context<'b, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
+        F: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<()>,
     {
-        InteractSession {
-            state: self.state,
-            input: self.input,
-            output: self.output,
-            session: self.session,
-            escape_character: self.escape_character,
+        InteractOptions {
             input_filter: self.input_filter,
             output_filter: self.output_filter,
             input_action: self.input_action,
-            output_action: Some(f),
+            output_action: Some(action),
             idle_action: self.idle_action,
+            state: self.state,
         }
     }
+}
 
+impl<S, I, O, C, IF, OF, IA, OA>
+    InteractOptions<C, IF, OF, IA, OA, NoAction<Session<Proc, S>, I, O, C>>
+{
     /// Puts a handler which will be called on each interaction when no input is detected.
-    pub fn on_idle<Action>(
-        self,
-        f: Action,
-    ) -> InteractSession<
-        'a,
-        State,
-        Session,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        InputAction,
-        OutputAction,
-        Action,
-    >
+    pub fn on_idle<F>(self, action: F) -> InteractOptions<C, IF, OF, IA, OA, F>
     where
-        Action:
-            for<'b> FnMut(Context<'b, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
+        F: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<()>,
     {
-        InteractSession {
-            state: self.state,
-            input: self.input,
-            output: self.output,
-            session: self.session,
-            escape_character: self.escape_character,
+        InteractOptions {
             input_filter: self.input_filter,
             output_filter: self.output_filter,
             input_action: self.input_action,
             output_action: self.output_action,
-            idle_action: Some(f),
+            idle_action: Some(action),
+            state: self.state,
         }
     }
 }
 
 /// A helper type to set a default action to [`InteractSession`].
-pub type NoAction<Session, Output, State> =
-    fn(Context<'_, &mut Session, &mut Output, &mut State>) -> Result<(), Error>;
+pub type NoAction<S, I, O, C> = fn(Context<'_, S, I, O, C>) -> Result<()>;
 
 /// A helper type to set a default filter to [`InteractSession`].
-pub type NoFilter = fn(&[u8]) -> Result<Cow<'_, [u8]>, Error>;
-
-impl<
-        State,
-        Stream,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        InputAction,
-        OutputAction,
-        IdleAction,
-    >
-    InteractSession<
-        '_,
-        State,
-        Session<Proc, Stream>,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        InputAction,
-        OutputAction,
-        IdleAction,
-    >
-{
-    /// Runs the session.
-    ///
-    /// See [`Session::interact`].
-    ///
-    /// [`Session::interact`]: crate::session::Session::interact
-    #[cfg(not(any(feature = "async", feature = "polling")))]
-    pub fn spawn(mut self) -> Result<State, Error>
-    where
-        Stream: NonBlocking + Write + Read,
-        Input: Read,
-        Output: Write,
-        InputFilter: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
-        OutputFilter: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
-        InputAction: FnMut(
-            Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-        ) -> Result<(), Error>,
-        OutputAction: FnMut(
-            Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-        ) -> Result<(), Error>,
-        IdleAction: FnMut(
-            Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-        ) -> Result<(), Error>,
-    {
-        #[cfg(unix)]
-        {
-            let is_echo = self
-                .session
-                .get_echo()
-                .map_err(|e| Error::unknown("failed to get echo", e.to_string()))?;
-            if !is_echo {
-                let _ = self.session.set_echo(true, None);
-            }
-
-            interact_buzy_loop(&mut self)?;
-
-            if !is_echo {
-                let _ = self.session.set_echo(false, None);
-            }
-        }
-
-        #[cfg(windows)]
-        {
-            interact_buzy_loop(&mut self)?;
-        }
-
-        Ok(self.state)
-    }
-
-    /// Runs the session.
-    ///
-    /// See [`Session::interact`].
-    ///
-    /// [`Session::interact`]: crate::session::Session::interact
-    #[cfg(all(unix, feature = "polling", not(feature = "async")))]
-    pub fn spawn(mut self) -> Result<State, Error>
-    where
-        Stream: Write + Read + std::os::unix::io::AsRawFd,
-        Input: Read + std::os::unix::io::AsRawFd,
-        Output: Write,
-        InputFilter: FnMut(&[u8]) -> Result<Cow<[u8]>, Error>,
-        OutputFilter: FnMut(&[u8]) -> Result<Cow<[u8]>, Error>,
-        InputAction: FnMut(
-            Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-        ) -> Result<(), Error>,
-        OutputAction: FnMut(
-            Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-        ) -> Result<(), Error>,
-        IdleAction: FnMut(
-            Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-        ) -> Result<(), Error>,
-    {
-        let is_echo = self
-            .session
-            .get_echo()
-            .map_err(|e| Error::unknown("failed to get echo", e))?;
-        if !is_echo {
-            let _ = self.session.set_echo(true, None);
-        }
-
-        interact_polling(&mut self)?;
-
-        if !is_echo {
-            let _ = self.session.set_echo(false, None);
-        }
-
-        Ok(self.state)
-    }
-
-    /// Runs the session.
-    ///
-    /// See [`Session::interact`].
-    ///
-    /// [`Session::interact`]: crate::session::Session::interact
-    #[cfg(feature = "async")]
-    pub async fn spawn(mut self) -> Result<State, Error>
-    where
-        Stream: futures_lite::AsyncRead + futures_lite::AsyncWrite + Unpin,
-        Input: futures_lite::AsyncRead + Unpin,
-        Output: Write,
-        InputFilter: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
-        OutputFilter: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
-        InputAction: FnMut(
-            Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-        ) -> Result<(), Error>,
-        OutputAction: FnMut(
-            Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-        ) -> Result<(), Error>,
-        IdleAction: FnMut(
-            Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-        ) -> Result<(), Error>,
-    {
-        {
-            #[cfg(unix)]
-            {
-                let is_echo = self
-                    .session
-                    .get_echo()
-                    .map_err(|e| Error::unknown("failed to get echo", e.to_string()))?;
-                if !is_echo {
-                    let _ = self.session.set_echo(true, None);
-                }
-
-                interact_async(&mut self).await?;
-
-                if !is_echo {
-                    let _ = self.session.set_echo(false, None);
-                }
-            }
-
-            #[cfg(windows)]
-            {
-                interact_async(&mut self).await?;
-            }
-
-            Ok(self.state)
-        }
-    }
-}
-
-impl<State, Output, Input, InputFilter, OutputFilter, InputAction, OutputAction, IdleAction>
-    InteractSession<
-        '_,
-        State,
-        Session,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        InputAction,
-        OutputAction,
-        IdleAction,
-    >
-{
-    /// Runs the session.
-    ///
-    /// See [`Session::interact`].
-    ///
-    /// [`Session::interact`]: crate::session::Session::interact
-    #[cfg(all(windows, feature = "polling", not(feature = "async")))]
-    pub fn spawn(mut self) -> Result<State, Error>
-    where
-        Input: Read + Send + 'static,
-        Output: Write,
-        InputFilter: FnMut(&[u8]) -> Result<Cow<[u8]>, Error>,
-        OutputFilter: FnMut(&[u8]) -> Result<Cow<[u8]>, Error>,
-        InputAction: FnMut(Context<'_, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
-        OutputAction:
-            FnMut(Context<'_, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
-        IdleAction: FnMut(Context<'_, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
-    {
-        interact_polling_on_thread(
-            self.session,
-            self.output,
-            self.input,
-            &mut self.state,
-            self.escape_character,
-            self.input_filter,
-            self.output_filter,
-            self.input_action,
-            self.output_action,
-            self.idle_action,
-        )?;
-
-        Ok(self.state)
-    }
-}
-
-#[cfg(all(not(feature = "async"), not(feature = "polling")))]
-fn interact_buzy_loop<
-    State,
-    Stream,
-    Output,
-    Input,
-    InputFilter,
-    OutputFilter,
-    InputAction,
-    OutputAction,
-    IdleAction,
->(
-    opts: &mut InteractSession<
-        '_,
-        State,
-        Session<Proc, Stream>,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        InputAction,
-        OutputAction,
-        IdleAction,
-    >,
-) -> Result<(), Error>
-where
-    Stream: NonBlocking + Write + Read,
-    Input: Read,
-    Output: Write,
-    InputFilter: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
-    OutputFilter: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
-    InputAction: FnMut(
-        Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-    ) -> Result<(), Error>,
-    OutputAction: FnMut(
-        Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-    ) -> Result<(), Error>,
-    IdleAction: FnMut(
-        Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-    ) -> Result<(), Error>,
-{
-    let mut buf = [0; 512];
-    loop {
-        // In case where proceses exits we are trying to
-        // fill buffer to run callbacks if there was something in.
-        //
-        // We ignore errors because there might be errors like EOCHILD etc.
-        let status = opts.session.is_alive();
-        if matches!(status, Ok(false)) {
-            return Ok(());
-        }
-
-        match opts.session.try_read(&mut buf) {
-            Ok(n) => {
-                let buf = &buf[..n];
-                let buf = if let Some(filter) = opts.output_filter.as_mut() {
-                    (filter)(buf)?
-                } else {
-                    Cow::Borrowed(buf)
-                };
-
-                let eof = n == 0;
-
-                if let Some(action) = opts.output_action.as_mut() {
-                    let ctx = Context::new(
-                        &mut *opts.session,
-                        &mut opts.output,
-                        &buf,
-                        eof,
-                        &mut opts.state,
-                    );
-                    (action)(ctx)?;
-                }
-
-                if eof {
-                    return Ok(());
-                }
-
-                spin_write(&mut opts.output, &buf)?;
-                spin_flush(&mut opts.output)?;
-            }
-            Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
-            Err(err) => return Err(err.into()),
-        }
-
-        // We dont't print user input back to the screen.
-        // In terminal mode it will be ECHOed back automatically.
-        // This way we preserve terminal seetings for example when user inputs password.
-        // The terminal must have been prepared before.
-        match opts.input.read(&mut buf) {
-            Ok(n) => {
-                let buf = &buf[..n];
-                let buf = if let Some(filter) = opts.input_filter.as_mut() {
-                    (filter)(buf)?
-                } else {
-                    Cow::Borrowed(buf)
-                };
-
-                let eof = n == 0;
-
-                if let Some(action) = opts.input_action.as_mut() {
-                    let ctx = Context::new(
-                        &mut *opts.session,
-                        &mut opts.output,
-                        &buf,
-                        eof,
-                        &mut opts.state,
-                    );
-                    (action)(ctx)?;
-                }
-
-                if eof {
-                    return Ok(());
-                }
-
-                let escape_char_position = buf.iter().position(|c| *c == opts.escape_character);
-                match escape_char_position {
-                    Some(pos) => {
-                        opts.session.write_all(&buf[..pos])?;
-                        return Ok(());
-                    }
-                    None => {
-                        opts.session.write_all(&buf[..])?;
-                    }
-                }
-            }
-            Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
-            Err(err) => return Err(err.into()),
-        }
-
-        if let Some(action) = opts.idle_action.as_mut() {
-            let ctx = Context::new(
-                &mut *opts.session,
-                &mut opts.output,
-                &[],
-                false,
-                &mut opts.state,
-            );
-            (action)(ctx)?;
-        }
-    }
-}
-
-#[cfg(all(unix, not(feature = "async"), feature = "polling"))]
-fn interact_polling<
-    State,
-    Stream,
-    Output,
-    Input,
-    InputFilter,
-    OutputFilter,
-    InputAction,
-    OutputAction,
-    IdleAction,
->(
-    opts: &mut InteractSession<
-        State,
-        Session<Proc, Stream>,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        InputAction,
-        OutputAction,
-        IdleAction,
-    >,
-) -> Result<(), Error>
-where
-    Stream: Write + Read + std::os::unix::io::AsRawFd,
-    Input: Read + std::os::unix::io::AsRawFd,
-    Output: Write,
-    InputFilter: FnMut(&[u8]) -> Result<Cow<[u8]>, Error>,
-    OutputFilter: FnMut(&[u8]) -> Result<Cow<[u8]>, Error>,
-    InputAction: FnMut(
-        Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-    ) -> Result<(), Error>,
-    OutputAction: FnMut(
-        Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-    ) -> Result<(), Error>,
-    IdleAction: FnMut(
-        Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-    ) -> Result<(), Error>,
-{
-    use polling::{Event, Poller};
-
-    // Create a poller and register interest in readability on the socket.
-    let poller = Poller::new()?;
-    poller.add(&opts.input.as_raw_fd(), Event::readable(0))?;
-    poller.add(&opts.session.get_stream().as_raw_fd(), Event::readable(1))?;
-
-    let mut buf = [0; 512];
-
-    // The event loop.
-    let mut events = Vec::new();
-    loop {
-        // In case where proceses exits we are trying to
-        // fill buffer to run callbacks if there was something in.
-        //
-        // We ignore errors because there might be errors like EOCHILD etc.
-        let status = opts.session.is_alive();
-        if matches!(status, Ok(false)) {
-            return Ok(());
-        }
-
-        // Wait for at least one I/O event.
-        events.clear();
-        let _ = poller.wait(&mut events, Some(std::time::Duration::from_secs(5)))?;
-
-        for ev in &events {
-            if ev.key == 0 {
-                // We dont't print user input back to the screen.
-                // In terminal mode it will be ECHOed back automatically.
-                // This way we preserve terminal seetings for example when user inputs password.
-                // The terminal must have been prepared before.
-                match opts.input.read(&mut buf) {
-                    Ok(n) => {
-                        let buf = &buf[..n];
-                        let buf = if let Some(filter) = opts.input_filter.as_mut() {
-                            (filter)(buf)?
-                        } else {
-                            Cow::Borrowed(buf)
-                        };
-
-                        let eof = n == 0;
-
-                        if let Some(action) = opts.input_action.as_mut() {
-                            let ctx = Context::new(
-                                &mut *opts.session,
-                                &mut opts.output,
-                                &buf,
-                                eof,
-                                &mut opts.state,
-                            );
-                            (action)(ctx)?;
-                        }
-
-                        if eof {
-                            return Ok(());
-                        }
-
-                        let escape_char_pos = buf.iter().position(|c| *c == opts.escape_character);
-                        match escape_char_pos {
-                            Some(pos) => {
-                                return opts.session.write_all(&buf[..pos]).map_err(|e| e.into())
-                            }
-                            None => opts.session.write_all(&buf[..])?,
-                        }
-                    }
-                    Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
-                    Err(err) => return Err(err.into()),
-                }
-
-                // Set interest in the next readability event.
-                poller.modify(&opts.input.as_raw_fd(), Event::readable(0))?;
-            }
-
-            if ev.key == 1 {
-                match opts.session.read(&mut buf) {
-                    Ok(n) => {
-                        let buf = &buf[..n];
-                        let buf = if let Some(filter) = opts.output_filter.as_mut() {
-                            (filter)(buf)?
-                        } else {
-                            Cow::Borrowed(buf)
-                        };
-
-                        let eof = n == 0;
-
-                        if let Some(action) = opts.output_action.as_mut() {
-                            let ctx = Context::new(
-                                &mut *opts.session,
-                                &mut opts.output,
-                                &buf,
-                                eof,
-                                &mut opts.state,
-                            );
-                            (action)(ctx)?;
-                        }
-
-                        if eof {
-                            return Ok(());
-                        }
-
-                        spin_write(&mut opts.output, &buf)?;
-                        spin_flush(&mut opts.output)?;
-                    }
-                    Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
-                    Err(err) => return Err(err.into()),
-                }
-
-                // Set interest in the next readability event.
-                poller.modify(&opts.session.get_stream().as_raw_fd(), Event::readable(1))?;
-            }
-        }
-
-        if let Some(action) = opts.idle_action.as_mut() {
-            let ctx = Context::new(
-                &mut *opts.session,
-                &mut opts.output,
-                &[],
-                false,
-                &mut opts.state,
-            );
-            (action)(ctx)?;
-        }
-    }
-}
-
-#[cfg(all(windows, not(feature = "async"), feature = "polling"))]
-fn interact_polling_on_thread<
-    State,
-    Output,
-    Input,
-    InputFilter,
-    OutputFilter,
-    InputAction,
-    OutputAction,
-    IdleAction,
->(
-    session: &mut Session,
-    mut output: Output,
-    input: Input,
-    state: &mut State,
-    escape_character: u8,
-    mut input_filter: Option<InputFilter>,
-    mut output_filter: Option<OutputFilter>,
-    mut input_action: Option<InputAction>,
-    mut output_action: Option<OutputAction>,
-    mut idle_action: Option<IdleAction>,
-) -> Result<(), Error>
-where
-    Input: Read + Send + 'static,
-    Output: Write,
-    InputFilter: FnMut(&[u8]) -> Result<Cow<[u8]>, Error>,
-    OutputFilter: FnMut(&[u8]) -> Result<Cow<[u8]>, Error>,
-    InputAction: FnMut(Context<'_, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
-    OutputAction: FnMut(Context<'_, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
-    IdleAction: FnMut(Context<'_, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
-{
-    use crate::{
-        error::to_io_error,
-        waiter::{Recv, Wait2},
-    };
-
-    // Create a poller and register interest in readability on the socket.
-    let stream = session.get_stream().try_clone().map_err(to_io_error(""))?;
-    let mut poller = Wait2::new(input, stream);
-
-    loop {
-        // In case where proceses exits we are trying to
-        // fill buffer to run callbacks if there was something in.
-        //
-        // We ignore errors because there might be errors like EOCHILD etc.
-        let status = session.is_alive();
-        if matches!(status, Ok(false)) {
-            return Ok(());
-        }
-
-        // Wait for at least one I/O event.
-        let event = poller.recv().map_err(to_io_error(""))?;
-        match event {
-            Recv::R1(b) => match b {
-                Ok(b) => {
-                    let eof = b.is_none();
-                    let n = if eof { 0 } else { 1 };
-                    let buf = b.map_or([0], |b| [b]);
-                    let buf = &buf[..n];
-
-                    let buf = if let Some(filter) = input_filter.as_mut() {
-                        (filter)(buf)?
-                    } else {
-                        Cow::Borrowed(buf)
-                    };
-
-                    if let Some(action) = input_action.as_mut() {
-                        let ctx = Context::new(&mut *session, &mut output, &buf, eof, &mut *state);
-                        (action)(ctx)?;
-                    }
-
-                    if eof {
-                        return Ok(());
-                    }
-
-                    let escape_char_pos = buf.iter().position(|c| *c == escape_character);
-                    match escape_char_pos {
-                        Some(pos) => {
-                            session.write_all(&buf[..pos])?;
-                            return Ok(());
-                        }
-                        None => session.write_all(&buf[..])?,
-                    }
-                }
-                Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
-                Err(err) => return Err(err.into()),
-            },
-            Recv::R2(b) => match b {
-                Ok(b) => {
-                    let eof = b.is_none();
-                    let n = if eof { 0 } else { 1 };
-                    let buf = b.map_or([0], |b| [b]);
-                    let buf = &buf[..n];
-
-                    let buf = if let Some(filter) = output_filter.as_mut() {
-                        (filter)(buf)?
-                    } else {
-                        Cow::Borrowed(buf)
-                    };
-
-                    if let Some(action) = output_action.as_mut() {
-                        let ctx = Context::new(&mut *session, &mut output, &buf, eof, &mut *state);
-                        (action)(ctx)?;
-                    }
-
-                    if eof {
-                        return Ok(());
-                    }
-
-                    output.write_all(&buf)?;
-                    output.flush()?;
-                }
-                Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
-                Err(err) => return Err(err.into()),
-            },
-            Recv::Timeout => {
-                if let Some(action) = idle_action.as_mut() {
-                    let ctx = Context::new(&mut *session, &mut output, &[], false, &mut *state);
-                    (action)(ctx)?;
-                }
-            }
-        }
-    }
-}
-
-#[cfg(feature = "async")]
-async fn interact_async<
-    State,
-    Stream,
-    Output,
-    Input,
-    InputFilter,
-    OutputFilter,
-    InputAction,
-    OutputAction,
-    IdleAction,
->(
-    opts: &mut InteractSession<
-        '_,
-        State,
-        Session<Proc, Stream>,
-        Output,
-        Input,
-        InputFilter,
-        OutputFilter,
-        InputAction,
-        OutputAction,
-        IdleAction,
-    >,
-) -> Result<(), Error>
-where
-    Stream: futures_lite::AsyncRead + futures_lite::AsyncWrite + Unpin,
-    Input: futures_lite::AsyncRead + Unpin,
-    Output: Write,
-    InputFilter: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
-    OutputFilter: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
-    InputAction: FnMut(
-        Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-    ) -> Result<(), Error>,
-    OutputAction: FnMut(
-        Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-    ) -> Result<(), Error>,
-    IdleAction: FnMut(
-        Context<'_, &mut Session<Proc, Stream>, &mut Output, &mut State>,
-    ) -> Result<(), Error>,
-{
-    use std::io;
-
-    use futures_lite::{AsyncReadExt, AsyncWriteExt};
-
-    let mut stdin_buf = [0; 512];
-    let mut proc_buf = [0; 512];
-    loop {
-        // In case where proceses exits we are trying to
-        // fill buffer to run callbacks if there was something in.
-        //
-        // We ignore errors because there might be errors like EOCHILD etc.
-        let status = opts.session.is_alive();
-        if matches!(status, Ok(false)) {
-            return Ok(());
-        }
-
-        #[derive(Debug)]
-        enum ReadFrom {
-            Stdin,
-            Process,
-            Timeout,
-        }
-
-        let read_process = async { (ReadFrom::Process, opts.session.read(&mut proc_buf).await) };
-        let read_stdin = async { (ReadFrom::Stdin, opts.input.read(&mut stdin_buf).await) };
-        let timeout = async {
-            (
-                ReadFrom::Timeout,
-                async {
-                    futures_timer::Delay::new(std::time::Duration::from_secs(5)).await;
-                    io::Result::Ok(0)
-                }
-                .await,
-            )
-        };
-
-        let read_fut = futures_lite::future::or(read_process, read_stdin);
-        let (read_from, result) = futures_lite::future::or(read_fut, timeout).await;
-
-        match read_from {
-            ReadFrom::Process => {
-                let n = result?;
-                let buf = &proc_buf[..n];
-                let buf = match opts.output_filter.as_mut() {
-                    Some(filter) => (filter)(buf)?,
-                    None => Cow::Borrowed(buf),
-                };
-
-                let eof = n == 0;
-
-                if let Some(action) = &mut opts.output_action {
-                    let ctx = Context::new(
-                        &mut *opts.session,
-                        &mut opts.output,
-                        &buf,
-                        eof,
-                        &mut opts.state,
-                    );
-                    (action)(ctx)?;
-                }
-
-                if eof {
-                    return Ok(());
-                }
-
-                spin_write(&mut opts.output, &buf)?;
-                spin_flush(&mut opts.output)?;
-            }
-            ReadFrom::Stdin => {
-                // We dont't print user input back to the screen.
-                // In terminal mode it will be ECHOed back automatically.
-                // This way we preserve terminal seetings for example when user inputs password.
-                // The terminal must have been prepared before.
-                match result {
-                    Ok(n) => {
-                        let buf = &stdin_buf[..n];
-                        let buf = match opts.input_filter.as_mut() {
-                            Some(filter) => (filter)(buf)?,
-                            None => Cow::Borrowed(buf),
-                        };
-
-                        let eof = n == 0;
-
-                        if let Some(action) = &mut opts.input_action {
-                            let ctx = Context::new(
-                                &mut *opts.session,
-                                &mut opts.output,
-                                &buf,
-                                eof,
-                                &mut opts.state,
-                            );
-                            (action)(ctx)?;
-                        }
-
-                        if eof {
-                            return Ok(());
-                        }
-
-                        let escape_char_pos = buf.iter().position(|c| *c == opts.escape_character);
-                        match escape_char_pos {
-                            Some(pos) => {
-                                opts.session.write_all(&buf[..pos]).await?;
-                                return Ok(());
-                            }
-                            None => opts.session.write_all(&buf[..]).await?,
-                        }
-                    }
-                    Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
-                    Err(err) => return Err(err.into()),
-                }
-            }
-            ReadFrom::Timeout => {
-                if let Some(action) = &mut opts.idle_action {
-                    let ctx = Context::new(
-                        &mut *opts.session,
-                        &mut opts.output,
-                        &[],
-                        false,
-                        &mut opts.state,
-                    );
-                    (action)(ctx)?;
-                }
-
-                // We need to check whether a process is alive;
-                continue;
-            }
-        }
-    }
-}
-
-fn spin_write<W>(mut writer: W, buf: &[u8]) -> std::io::Result<()>
-where
-    W: Write,
-{
-    loop {
-        match writer.write_all(buf) {
-            Ok(_) => return Ok(()),
-            Err(err) if err.kind() != std::io::ErrorKind::WouldBlock => return Err(err),
-            Err(_) => (),
-        }
-    }
-}
-
-fn spin_flush<W>(mut writer: W) -> std::io::Result<()>
-where
-    W: Write,
-{
-    loop {
-        match writer.flush() {
-            Ok(_) => return Ok(()),
-            Err(err) if err.kind() != std::io::ErrorKind::WouldBlock => return Err(err),
-            Err(_) => (),
-        }
-    }
-}
+pub type NoFilter = fn(&[u8]) -> Result<Cow<'_, [u8]>>;
diff --git a/src/interact/session.rs b/src/interact/session.rs
new file mode 100644
index 0000000..b91b18b
--- /dev/null
+++ b/src/interact/session.rs
@@ -0,0 +1,805 @@
+//! This module contains a [`InteractSession`] which runs an interact session with IO.
+
+use std::{
+    borrow::{BorrowMut, Cow},
+    io::{ErrorKind, Write},
+};
+
+use crate::{session::Proc, Error, Session};
+
+#[cfg(not(feature = "async"))]
+use std::io::Read;
+
+use super::{Context, InteractOptions};
+#[cfg(all(not(feature = "async"), not(feature = "polling")))]
+use crate::process::NonBlocking;
+
+/// InteractConfig represents options of an interactive session.
+#[derive(Debug)]
+pub struct InteractSession<Session, Input, Output> {
+    session: Session,
+    input: Input,
+    output: Output,
+    escape_character: u8,
+    #[cfg(unix)]
+    status: Option<crate::WaitStatus>,
+}
+
+impl<S, I, O> InteractSession<S, I, O> {
+    /// Default escape character.
+    pub const ESCAPE: u8 = 29; // Ctrl-]
+
+    /// Creates a new object of [InteractSession].
+    pub fn new(session: S, input: I, output: O) -> InteractSession<S, I, O> {
+        InteractSession {
+            input,
+            output,
+            session,
+            escape_character: Self::ESCAPE,
+            #[cfg(unix)]
+            status: None,
+        }
+    }
+
+    /// Sets an escape character after seen which the interact interactions will be stopped
+    /// and controll will be returned to a caller process.
+    pub fn set_escape_character(mut self, c: u8) -> Self {
+        self.escape_character = c;
+        self
+    }
+
+    /// Returns a status of spawned session if it was exited.
+    ///
+    /// If [`Self::spawn`] returns false but this method returns None it means that a child process was shutdown by various reasons.
+    /// Which sometimes happens and it's not considered to be a valid [`WaitStatus`], so None is returned.
+    ///
+    /// [`Self::spawn`]: crate::interact::InteractSession::spawn
+    /// [`WaitStatus`]: crate::WaitStatus
+    #[cfg(unix)]
+    pub fn get_status(&self) -> Option<crate::WaitStatus> {
+        self.status
+    }
+}
+
+#[cfg(not(any(feature = "async", feature = "polling")))]
+impl<S, I, O> InteractSession<&mut Session<Proc, S>, I, O>
+where
+    I: Read,
+    O: Write,
+    S: NonBlocking + Write + Read,
+{
+    /// Runs the session.
+    ///
+    /// See [`Session::interact`].
+    ///
+    /// [`Session::interact`]: crate::session::Session::interact
+    pub fn spawn<C, IF, OF, IA, OA, WA, OPS>(&mut self, mut ops: OPS) -> Result<bool, Error>
+    where
+        OPS: BorrowMut<InteractOptions<C, IF, OF, IA, OA, WA>>,
+        IF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+        OF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+        IA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+        OA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+        WA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+    {
+        #[cfg(unix)]
+        {
+            let is_echo = self
+                .session
+                .get_echo()
+                .map_err(|e| Error::unknown("failed to get echo", e.to_string()))?;
+            if !is_echo {
+                let _ = self.session.set_echo(true, None);
+            }
+
+            self.status = None;
+            let is_alive = interact_buzy_loop(self, ops.borrow_mut())?;
+
+            if !is_echo {
+                let _ = self.session.set_echo(false, None);
+            }
+
+            Ok(is_alive)
+        }
+
+        #[cfg(windows)]
+        {
+            interact_buzy_loop(self, ops.borrow_mut())
+        }
+    }
+}
+
+#[cfg(all(unix, feature = "polling", not(feature = "async")))]
+impl<S, I, O> InteractSession<&mut Session<Proc, S>, I, O>
+where
+    I: Read + std::os::unix::io::AsRawFd,
+    O: Write,
+    S: Write + Read + std::os::unix::io::AsRawFd,
+{
+    /// Runs the session.
+    ///
+    /// See [`Session::interact`].
+    ///
+    /// [`Session::interact`]: crate::session::Session::interact
+    pub fn spawn<C, IF, OF, IA, OA, WA, OPS>(&mut self, mut ops: OPS) -> Result<bool, Error>
+    where
+        OPS: BorrowMut<InteractOptions<C, IF, OF, IA, OA, WA>>,
+        IF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+        OF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+        IA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+        OA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+        WA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+    {
+        let is_echo = self
+            .session
+            .get_echo()
+            .map_err(|e| Error::unknown("failed to get echo", e.to_string()))?;
+        if !is_echo {
+            let _ = self.session.set_echo(true, None);
+        }
+
+        self.status = None;
+        let is_alive = interact_polling(self, ops.borrow_mut())?;
+
+        if !is_echo {
+            let _ = self.session.set_echo(false, None);
+        }
+
+        Ok(is_alive)
+    }
+}
+
+#[cfg(feature = "async")]
+impl<S, I, O> InteractSession<&mut Session<Proc, S>, I, O>
+where
+    I: futures_lite::AsyncRead + Unpin,
+    O: Write,
+    S: futures_lite::AsyncRead + futures_lite::AsyncWrite + Unpin,
+{
+    /// Runs the session.
+    ///
+    /// See [`Session::interact`].
+    ///
+    /// [`Session::interact`]: crate::session::Session::interact
+    pub async fn spawn<C, IF, OF, IA, OA, WA, OPS>(&mut self, mut opts: OPS) -> Result<bool, Error>
+    where
+        OPS: BorrowMut<InteractOptions<C, IF, OF, IA, OA, WA>>,
+        IF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+        OF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+        IA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+        OA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+        WA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+    {
+        #[cfg(unix)]
+        {
+            let is_echo = self
+                .session
+                .get_echo()
+                .map_err(|e| Error::unknown("failed to get echo", e.to_string()))?;
+            if !is_echo {
+                let _ = self.session.set_echo(true, None);
+            }
+
+            let is_alive = interact_async(self, opts.borrow_mut()).await?;
+
+            if !is_echo {
+                let _ = self.session.set_echo(false, None);
+            }
+
+            Ok(is_alive)
+        }
+
+        #[cfg(windows)]
+        {
+            interact_async(self, opts.borrow_mut()).await
+        }
+    }
+}
+
+#[cfg(all(windows, feature = "polling", not(feature = "async")))]
+impl<S, I, O, C> InteractSession<Session<Proc, S>, I, O, C>
+where
+    I: Read + Send + 'static,
+    O: Write,
+    S: Write + Read + std::os::unix::io::AsRawFd,
+{
+    /// Runs the session.
+    ///
+    /// See [`Session::interact`].
+    ///
+    /// [`Session::interact`]: crate::session::Session::interact
+    pub fn spawn(mut self) -> Result<bool, Error> {
+        interact_polling_on_thread(
+            self.session,
+            self.output,
+            self.input,
+            &mut self.state,
+            self.escape_character,
+            self.input_filter,
+            self.output_filter,
+            self.input_action,
+            self.output_action,
+            self.idle_action,
+        )?;
+
+        Ok(self.state)
+    }
+}
+
+#[cfg(all(not(feature = "async"), not(feature = "polling")))]
+fn interact_buzy_loop<S, O, I, C, IF, OF, IA, OA, WA>(
+    interact: &mut InteractSession<&mut Session<Proc, S>, I, O>,
+    opts: &mut InteractOptions<C, IF, OF, IA, OA, WA>,
+) -> Result<bool, Error>
+where
+    S: NonBlocking + Write + Read,
+    I: Read,
+    O: Write,
+    IF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+    OF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+    IA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+    OA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+    WA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+{
+    let mut buf = [0; 512];
+    loop {
+        #[cfg(unix)]
+        {
+            let status = get_status(interact.session)?;
+            if !matches!(status, Some(crate::WaitStatus::StillAlive)) {
+                interact.status = status;
+                return Ok(false);
+            }
+        }
+
+        #[cfg(windows)]
+        {
+            if !interact.session.is_alive() {
+                return Ok(false);
+            }
+        }
+
+        match interact.session.try_read(&mut buf) {
+            Ok(n) => {
+                let eof = n == 0;
+                let buf = &buf[..n];
+                let buf = call_filter(opts.output_filter.as_mut(), buf)?;
+
+                call_action(
+                    opts.output_action.as_mut(),
+                    interact.session,
+                    &mut interact.input,
+                    &mut interact.output,
+                    &mut opts.state,
+                    &buf,
+                    eof,
+                )?;
+
+                if eof {
+                    return Ok(true);
+                }
+
+                spin_write(&mut interact.output, &buf)?;
+                spin_flush(&mut interact.output)?;
+            }
+            Err(err) if err.kind() == ErrorKind::WouldBlock => {}
+            Err(err) => return Err(err.into()),
+        }
+
+        // We dont't print user input back to the screen.
+        // In terminal mode it will be ECHOed back automatically.
+        // This way we preserve terminal seetings for example when user inputs password.
+        // The terminal must have been prepared before.
+        match interact.input.read(&mut buf) {
+            Ok(n) => {
+                let eof = n == 0;
+                let buf = &buf[..n];
+                let buf = call_filter(opts.input_filter.as_mut(), buf)?;
+
+                call_action(
+                    opts.input_action.as_mut(),
+                    interact.session,
+                    &mut interact.input,
+                    &mut interact.output,
+                    &mut opts.state,
+                    &buf,
+                    eof,
+                )?;
+
+                if eof {
+                    return Ok(true);
+                }
+
+                let escape_char_position = buf.iter().position(|c| *c == interact.escape_character);
+                match escape_char_position {
+                    Some(pos) => {
+                        interact.session.write_all(&buf[..pos])?;
+                        return Ok(true);
+                    }
+                    None => {
+                        interact.session.write_all(&buf[..])?;
+                    }
+                }
+            }
+            Err(err) if err.kind() == ErrorKind::WouldBlock => {}
+            Err(err) => return Err(err.into()),
+        }
+
+        call_action(
+            opts.idle_action.as_mut(),
+            interact.session,
+            &mut interact.input,
+            &mut interact.output,
+            &mut opts.state,
+            &[],
+            false,
+        )?;
+    }
+}
+
+#[cfg(all(unix, not(feature = "async"), feature = "polling"))]
+fn interact_polling<S, O, I, C, IF, OF, IA, OA, WA>(
+    interact: &mut InteractSession<&mut Session<Proc, S>, I, O>,
+    opts: &mut InteractOptions<C, IF, OF, IA, OA, WA>,
+) -> Result<bool, Error>
+where
+    S: Write + Read + std::os::unix::io::AsRawFd,
+    I: Read + std::os::unix::io::AsRawFd,
+    O: Write,
+    IF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+    OF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+    IA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+    OA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+    WA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+{
+    use polling::{Event, Poller};
+
+    // Create a poller and register interest in readability on the socket.
+    let poller = Poller::new()?;
+    poller.add(interact.input.as_raw_fd(), Event::readable(0))?;
+    poller.add(
+        interact.session.get_stream().as_raw_fd(),
+        Event::readable(1),
+    )?;
+
+    let mut buf = [0; 512];
+
+    // The event loop.
+    let mut events = Vec::new();
+    loop {
+        let status = get_status(interact.session)?;
+        if !matches!(status, Some(crate::WaitStatus::StillAlive)) {
+            interact.status = status;
+            return Ok(false);
+        }
+
+        // Wait for at least one I/O event.
+        events.clear();
+        let _ = poller.wait(&mut events, Some(std::time::Duration::from_secs(5)))?;
+
+        for ev in &events {
+            if ev.key == 0 {
+                // We dont't print user input back to the screen.
+                // In terminal mode it will be ECHOed back automatically.
+                // This way we preserve terminal seetings for example when user inputs password.
+                // The terminal must have been prepared before.
+                match interact.input.read(&mut buf) {
+                    Ok(n) => {
+                        let eof = n == 0;
+                        let buf = &buf[..n];
+                        let buf = call_filter(opts.input_filter.as_mut(), buf)?;
+
+                        call_action(
+                            opts.input_action.as_mut(),
+                            interact.session,
+                            &mut interact.input,
+                            &mut interact.output,
+                            &mut opts.state,
+                            &buf,
+                            eof,
+                        )?;
+
+                        if eof {
+                            return Ok(true);
+                        }
+
+                        let escape_char_pos =
+                            buf.iter().position(|c| *c == interact.escape_character);
+                        match escape_char_pos {
+                            Some(pos) => {
+                                interact.session.write_all(&buf[..pos]).map_err(Error::IO)?;
+                                return Ok(true);
+                            }
+                            None => interact.session.write_all(&buf[..])?,
+                        }
+                    }
+                    Err(err) if err.kind() == ErrorKind::WouldBlock => {}
+                    Err(err) => return Err(err.into()),
+                }
+
+                // Set interest in the next readability event.
+                poller.modify(interact.input.as_raw_fd(), Event::readable(0))?;
+            }
+
+            if ev.key == 1 {
+                match interact.session.read(&mut buf) {
+                    Ok(n) => {
+                        let eof = n == 0;
+                        let buf = &buf[..n];
+                        let buf = call_filter(opts.output_filter.as_mut(), buf)?;
+
+                        call_action(
+                            opts.output_action.as_mut(),
+                            interact.session,
+                            &mut interact.input,
+                            &mut interact.output,
+                            &mut opts.state,
+                            &buf,
+                            eof,
+                        )?;
+
+                        if eof {
+                            return Ok(true);
+                        }
+
+                        spin_write(&mut interact.output, &buf)?;
+                        spin_flush(&mut interact.output)?;
+                    }
+                    Err(err) if err.kind() == ErrorKind::WouldBlock => {}
+                    Err(err) => return Err(err.into()),
+                }
+
+                // Set interest in the next readability event.
+                poller.modify(
+                    interact.session.get_stream().as_raw_fd(),
+                    Event::readable(1),
+                )?;
+            }
+        }
+
+        call_action(
+            opts.idle_action.as_mut(),
+            interact.session,
+            &mut interact.input,
+            &mut interact.output,
+            &mut opts.state,
+            &[],
+            false,
+        )?;
+    }
+}
+
+#[cfg(all(windows, not(feature = "async"), feature = "polling"))]
+fn interact_polling_on_thread<
+    State,
+    Output,
+    Input,
+    InputFilter,
+    OutputFilter,
+    InputAction,
+    OutputAction,
+    IdleAction,
+>(
+    session: &mut Session,
+    mut output: Output,
+    input: Input,
+    state: &mut State,
+    escape_character: u8,
+    mut input_filter: Option<InputFilter>,
+    mut output_filter: Option<OutputFilter>,
+    mut input_action: Option<InputAction>,
+    mut output_action: Option<OutputAction>,
+    mut idle_action: Option<IdleAction>,
+) -> Result<bool, Error>
+where
+    Input: Read + Send + 'static,
+    Output: Write,
+    InputFilter: FnMut(&[u8]) -> Result<Cow<[u8]>, Error>,
+    OutputFilter: FnMut(&[u8]) -> Result<Cow<[u8]>, Error>,
+    InputAction: FnMut(Context<'_, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
+    OutputAction: FnMut(Context<'_, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
+    IdleAction: FnMut(Context<'_, &mut Session, &mut Output, &mut State>) -> Result<(), Error>,
+{
+    use crate::{
+        error::to_io_error,
+        waiter::{Recv, Wait2},
+    };
+
+    // Create a poller and register interest in readability on the socket.
+    let stream = session.get_stream().try_clone().map_err(to_io_error(""))?;
+    let mut poller = Wait2::new(input, stream);
+
+    loop {
+        // In case where proceses exits we are trying to
+        // fill buffer to run callbacks if there was something in.
+        //
+        // We ignore errors because there might be errors like EOCHILD etc.
+        let status = session.is_alive();
+        if matches!(status, Ok(false)) {
+            return Ok(false);
+        }
+
+        // Wait for at least one I/O event.
+        let event = poller.recv().map_err(to_io_error(""))?;
+        match event {
+            Recv::R1(b) => match b {
+                Ok(b) => {
+                    let eof = b.is_none();
+                    let n = if eof { 0 } else { 1 };
+                    let buf = b.map_or([0], |b| [b]);
+                    let buf = &buf[..n];
+
+                    let buf = if let Some(filter) = input_filter.as_mut() {
+                        (filter)(buf)?
+                    } else {
+                        Cow::Borrowed(buf)
+                    };
+
+                    if let Some(action) = input_action.as_mut() {
+                        let ctx = Context::new(&mut *session, &mut output, &buf, eof, &mut *state);
+                        (action)(ctx)?;
+                    }
+
+                    if eof {
+                        return Ok(true);
+                    }
+
+                    let escape_char_pos = buf.iter().position(|c| *c == escape_character);
+                    match escape_char_pos {
+                        Some(pos) => {
+                            session.write_all(&buf[..pos])?;
+                            return Ok(true);
+                        }
+                        None => session.write_all(&buf[..])?,
+                    }
+                }
+                Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
+                Err(err) => return Err(err.into()),
+            },
+            Recv::R2(b) => match b {
+                Ok(b) => {
+                    let eof = b.is_none();
+                    let n = if eof { 0 } else { 1 };
+                    let buf = b.map_or([0], |b| [b]);
+                    let buf = &buf[..n];
+
+                    let buf = if let Some(filter) = output_filter.as_mut() {
+                        (filter)(buf)?
+                    } else {
+                        Cow::Borrowed(buf)
+                    };
+
+                    if let Some(action) = output_action.as_mut() {
+                        let ctx = Context::new(&mut *session, &mut output, &buf, eof, &mut *state);
+                        (action)(ctx)?;
+                    }
+
+                    if eof {
+                        return Ok(true);
+                    }
+
+                    output.write_all(&buf)?;
+                    output.flush()?;
+                }
+                Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
+                Err(err) => return Err(err.into()),
+            },
+            Recv::Timeout => {
+                if let Some(action) = idle_action.as_mut() {
+                    let ctx = Context::new(&mut *session, &mut output, &[], false, &mut *state);
+                    (action)(ctx)?;
+                }
+            }
+        }
+    }
+}
+
+#[cfg(feature = "async")]
+async fn interact_async<S, O, I, C, IF, OF, IA, OA, WA>(
+    interact: &mut InteractSession<&mut Session<Proc, S>, I, O>,
+    opts: &mut InteractOptions<C, IF, OF, IA, OA, WA>,
+) -> Result<bool, Error>
+where
+    S: futures_lite::AsyncRead + futures_lite::AsyncWrite + Unpin,
+    I: futures_lite::AsyncRead + Unpin,
+    O: Write,
+    IF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+    OF: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+    IA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+    OA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+    WA: FnMut(Context<'_, Session<Proc, S>, I, O, C>) -> Result<(), Error>,
+{
+    use std::io;
+
+    use futures_lite::{AsyncReadExt, AsyncWriteExt};
+
+    let mut stdin_buf = [0; 512];
+    let mut proc_buf = [0; 512];
+    loop {
+        #[cfg(unix)]
+        {
+            let status = get_status(interact.session)?;
+            if !matches!(status, Some(crate::WaitStatus::StillAlive)) {
+                interact.status = status;
+                return Ok(false);
+            }
+        }
+
+        #[cfg(windows)]
+        {
+            if !interact.session.is_alive() {
+                return Ok(false);
+            }
+        }
+
+        #[derive(Debug)]
+        enum ReadFrom {
+            Stdin,
+            Process,
+            Timeout,
+        }
+
+        let read_process = async {
+            (
+                ReadFrom::Process,
+                interact.session.read(&mut proc_buf).await,
+            )
+        };
+        let read_stdin = async { (ReadFrom::Stdin, interact.input.read(&mut stdin_buf).await) };
+        let timeout = async {
+            (
+                ReadFrom::Timeout,
+                async {
+                    futures_timer::Delay::new(std::time::Duration::from_secs(5)).await;
+                    io::Result::Ok(0)
+                }
+                .await,
+            )
+        };
+
+        let read_fut = futures_lite::future::or(read_process, read_stdin);
+        let (read_from, result) = futures_lite::future::or(read_fut, timeout).await;
+
+        match read_from {
+            ReadFrom::Process => {
+                let n = result?;
+                let eof = n == 0;
+                let buf = &proc_buf[..n];
+                let buf = call_filter(opts.output_filter.as_mut(), buf)?;
+
+                call_action(
+                    opts.output_action.as_mut(),
+                    interact.session,
+                    &mut interact.input,
+                    &mut interact.output,
+                    &mut opts.state,
+                    &buf,
+                    eof,
+                )?;
+
+                if eof {
+                    return Ok(true);
+                }
+
+                spin_write(&mut interact.output, &buf)?;
+                spin_flush(&mut interact.output)?;
+            }
+            ReadFrom::Stdin => {
+                // We dont't print user input back to the screen.
+                // In terminal mode it will be ECHOed back automatically.
+                // This way we preserve terminal seetings for example when user inputs password.
+                // The terminal must have been prepared before.
+                match result {
+                    Ok(n) => {
+                        let eof = n == 0;
+                        let buf = &stdin_buf[..n];
+                        let buf = call_filter(opts.output_filter.as_mut(), buf)?;
+
+                        call_action(
+                            opts.input_action.as_mut(),
+                            interact.session,
+                            &mut interact.input,
+                            &mut interact.output,
+                            &mut opts.state,
+                            &buf,
+                            eof,
+                        )?;
+
+                        if eof {
+                            return Ok(true);
+                        }
+
+                        let escape_char_pos =
+                            buf.iter().position(|c| *c == interact.escape_character);
+                        match escape_char_pos {
+                            Some(pos) => {
+                                interact.session.write_all(&buf[..pos]).await?;
+                                return Ok(true);
+                            }
+                            None => interact.session.write_all(&buf[..]).await?,
+                        }
+                    }
+                    Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
+                    Err(err) => return Err(err.into()),
+                }
+            }
+            ReadFrom::Timeout => {
+                call_action(
+                    opts.idle_action.as_mut(),
+                    interact.session,
+                    &mut interact.input,
+                    &mut interact.output,
+                    &mut opts.state,
+                    &[],
+                    false,
+                )?;
+
+                // We need to check whether a process is alive;
+                continue;
+            }
+        }
+    }
+}
+
+fn spin_write<W>(mut writer: W, buf: &[u8]) -> std::io::Result<()>
+where
+    W: Write,
+{
+    loop {
+        match writer.write_all(buf) {
+            Ok(_) => return Ok(()),
+            Err(err) if err.kind() != std::io::ErrorKind::WouldBlock => return Err(err),
+            Err(_) => (),
+        }
+    }
+}
+
+fn spin_flush<W>(mut writer: W) -> std::io::Result<()>
+where
+    W: Write,
+{
+    loop {
+        match writer.flush() {
+            Ok(_) => return Ok(()),
+            Err(err) if err.kind() != std::io::ErrorKind::WouldBlock => return Err(err),
+            Err(_) => (),
+        }
+    }
+}
+
+fn call_action<F, S, I, O, C>(
+    action: Option<F>,
+    s: &mut S,
+    r: &mut I,
+    w: &mut O,
+    state: &mut C,
+    buf: &[u8],
+    eof: bool,
+) -> Result<(), Error>
+where
+    F: FnMut(Context<'_, S, I, O, C>) -> Result<(), Error>,
+{
+    match action {
+        Some(mut action) => (action)(Context::new(s, r, w, state, buf, eof)),
+        None => Ok(()),
+    }
+}
+
+fn call_filter<F>(filter: Option<F>, buf: &[u8]) -> Result<Cow<'_, [u8]>, Error>
+where
+    F: FnMut(&[u8]) -> Result<Cow<'_, [u8]>, Error>,
+{
+    match filter {
+        Some(mut action) => (action)(buf),
+        None => Ok(Cow::Borrowed(buf)),
+    }
+}
+
+#[cfg(unix)]
+fn get_status<S>(session: &Session<Proc, S>) -> Result<Option<crate::WaitStatus>, Error> {
+    match session.status() {
+        Ok(status) => Ok(Some(status)),
+        Err(ptyprocess::errno::Errno::ECHILD | ptyprocess::errno::Errno::ESRCH) => Ok(None),
+        Err(err) => Err(Error::IO(std::io::Error::new(ErrorKind::Other, err))),
+    }
+}
diff --git a/src/needle.rs b/src/needle.rs
index 21b1fad..df29bc6 100644
--- a/src/needle.rs
+++ b/src/needle.rs
@@ -311,6 +311,7 @@ mod tests {
 
     #[allow(clippy::needless_borrow)]
     #[test]
+    #[allow(clippy::needless_borrow)]
     fn test_bytes_ref() {
         assert_eq!(
             (&[b'q', b'w', b'e']).check(b"qwerty", false).unwrap(),
diff --git a/src/process/unix.rs b/src/process/unix.rs
index 735416c..9fa03f4 100644
--- a/src/process/unix.rs
+++ b/src/process/unix.rs
@@ -206,7 +206,7 @@ pub(crate) fn make_non_blocking(fd: RawFd, blocking: bool) -> Result<()> {
 }
 
 fn nix_error_to_io(err: nix::Error) -> io::Error {
-    io::Error::from_raw_os_error(err as _)
+    io::Error::new(io::ErrorKind::Other, err)
 }
 
 /// Turn e.g. "prog arg1 arg2" into ["prog", "arg1", "arg2"]
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 32e867f..a87bcbc 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -24,12 +24,7 @@ pub mod sync_session;
 
 use std::io::Write;
 
-use crate::{
-    interact::{InteractSession, NoAction, NoFilter},
-    process::Process,
-    stream::log::LoggedStream,
-    Error,
-};
+use crate::{interact::InteractSession, process::Process, stream::log::LoggedStream, Error};
 
 #[cfg(not(feature = "async"))]
 use std::io::Read;
@@ -174,30 +169,16 @@ impl<P, S> Session<P, S> {
         not(all(unix, not(feature = "async"), not(feature = "polling"))),
         doc = "```ignore"
     )]
-    /// use std::io;
+    /// use std::io::{stdout, Cursor};
+    /// use expectrl::{self, interact::InteractOptions};
     ///
     /// let mut p = expectrl::spawn("cat").unwrap();
     ///
-    /// let input = io::Cursor::new(String::from("Some text right here"));
+    /// let input = Cursor::new(String::from("Some text right here"));
     ///
-    /// p.interact(input, io::stdout()).spawn().unwrap();
+    /// p.interact(input, stdout()).spawn(InteractOptions::default()).unwrap();
     /// ```
-    pub fn interact<I, O>(
-        &mut self,
-        input: I,
-        output: O,
-    ) -> InteractSession<
-        '_,
-        (),
-        Self,
-        O,
-        I,
-        NoFilter,
-        NoFilter,
-        NoAction<Self, O, ()>,
-        NoAction<Self, O, ()>,
-        NoAction<Self, O, ()>,
-    > {
-        InteractSession::new(self, output, input, ())
+    pub fn interact<I, O>(&mut self, input: I, output: O) -> InteractSession<&mut Self, I, O> {
+        InteractSession::new(self, input, output)
     }
 }
diff --git a/src/stream/stdin.rs b/src/stream/stdin.rs
index 2d71b05..03c6be6 100644
--- a/src/stream/stdin.rs
+++ b/src/stream/stdin.rs
@@ -218,12 +218,23 @@ mod inner {
 
     impl std::fmt::Debug for StdinInner {
         fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-            f.debug_struct("StdinInner")
-                .field("terminal", &"...")
-                .field("is_blocking", &self.is_blocking)
-                .field("stdin", &self.stdin)
-                .field("stdin", &self.stdin)
-                .finish()
+            #[cfg(not(feature = "async"))]
+            {
+                f.debug_struct("StdinInner")
+                    .field("terminal", &"...")
+                    .field("is_blocking", &self.is_blocking)
+                    .field("stdin", &self.stdin)
+                    .field("stdin", &self.stdin)
+                    .finish()
+            }
+            #[cfg(feature = "async")]
+            {
+                f.debug_struct("StdinInner")
+                    .field("terminal", &"...")
+                    .field("stdin", &self.stdin)
+                    .field("stdin", &self.stdin)
+                    .finish()
+            }
         }
     }
 


Test patch:
diff --git a/tests/expect.rs b/tests/expect.rs
index 2f78e80..9a08457 100644
--- a/tests/expect.rs
+++ b/tests/expect.rs
@@ -111,10 +111,7 @@ fn expect_regex() {
     #[cfg(not(feature = "async"))]
     {
         let m = session.expect(Regex("lo.*")).unwrap();
-        assert_eq!(
-            m.before(),
-            [27, 91, 50, 74, 27, 91, 109, 27, 91, 72, 72, 101, 108]
-        );
+        assert_eq!(m.matches().count(), 1);
         assert_eq!(m.get(0).unwrap(), b"lo World\r");
     }
 
@@ -122,10 +119,7 @@ fn expect_regex() {
     {
         futures_lite::future::block_on(async {
             let m = session.expect(Regex("lo.*")).await.unwrap();
-            assert_eq!(
-                m.before(),
-                [27, 91, 50, 74, 27, 91, 109, 27, 91, 72, 72, 101, 108]
-            );
+            assert_eq!(m.matches().count(), 1);
             assert_eq!(m.get(0).unwrap(), b"lo World\r");
         })
     }
@@ -170,10 +164,8 @@ fn expect_n_bytes() {
     {
         // ignore spawned command
         let m = session.expect(NBytes(14)).unwrap();
-        assert_eq!(
-            m.get(0).unwrap(),
-            "\u{1b}[2J\u{1b}[m\u{1b}[HHell".as_bytes()
-        );
+        assert_eq!(m.matches().count(), 1);
+        assert_eq!(m.get(0).unwrap().len(), 14);
         assert_eq!(m.before(), b"");
     }
 
@@ -182,10 +174,8 @@ fn expect_n_bytes() {
         futures_lite::future::block_on(async {
             // ignore spawned command
             let m = session.expect(NBytes(14)).await.unwrap();
-            assert_eq!(
-                m.get(0).unwrap(),
-                "\u{1b}[2J\u{1b}[m\u{1b}[HHell".as_bytes()
-            );
+            assert_eq!(m.matches().count(), 1);
+            assert_eq!(m.get(0).unwrap().len(), 14);
             assert_eq!(m.before(), b"");
         })
     }
diff --git a/tests/interact.rs b/tests/interact.rs
index 46048c3..b2ac145 100644
--- a/tests/interact.rs
+++ b/tests/interact.rs
@@ -1,4 +1,4 @@
-#![cfg(not(windows))]
+#![cfg(unix)]
 
 use std::{
     io::{self, Cursor, Read, Write},
@@ -19,6 +19,8 @@ use expectrl::WaitStatus;
 #[ignore = "It requires manual interaction; Or it's necessary to redirect an stdin of current process"]
 #[test]
 fn interact_callback() {
+    use expectrl::interact::InteractOptions;
+
     let mut input_handle = Lookup::new();
     let mut output_handle = Lookup::new();
 
@@ -26,15 +28,7 @@ fn interact_callback() {
 
     let mut stdin = Stdin::open().unwrap();
 
-    session
-        .interact(&mut stdin, sink())
-        .on_input(|ctx| {
-            if input_handle.on(ctx.buf, ctx.eof, "213")?.is_some() {
-                ctx.session.send_line("Hello World")?;
-            }
-
-            Ok(())
-        })
+    let opts = InteractOptions::default()
         .on_output(|ctx| {
             if let Some(m) = output_handle.on(ctx.buf, ctx.eof, b'\n')? {
                 let line = m.before();
@@ -43,8 +37,15 @@ fn interact_callback() {
 
             Ok(())
         })
-        .spawn()
-        .unwrap();
+        .on_input(|ctx| {
+            if input_handle.on(ctx.buf, ctx.eof, "213")?.is_some() {
+                ctx.session.send_line("Hello World")?;
+            }
+
+            Ok(())
+        });
+
+    session.interact(&mut stdin, sink()).spawn(opts).unwrap();
 
     stdin.close().unwrap();
 }
@@ -53,6 +54,8 @@ fn interact_callback() {
 #[cfg(not(feature = "async"))]
 #[test]
 fn interact_output_callback() {
+    use expectrl::interact::{InteractOptions, InteractSession};
+
     let mut session = expectrl::spawn("sleep 1 && echo 'Hello World'").unwrap();
 
     let mut stdin = Stdin::open().unwrap();
@@ -61,18 +64,15 @@ fn interact_output_callback() {
     let mut state = 0;
 
     let mut lookup = Lookup::new();
-    session
-        .interact(&mut stdin, stdout)
-        .set_state(&mut state)
-        .on_output(|ctx| {
-            if lookup.on(ctx.buf, ctx.eof, "World")?.is_some() {
-                **ctx.state += 1;
-            }
+    let interact_opts = InteractOptions::new(&mut state).on_output(|ctx| {
+        if lookup.on(ctx.buf, ctx.eof, "World")?.is_some() {
+            **ctx.state += 1;
+        }
 
-            Ok(())
-        })
-        .spawn()
-        .unwrap();
+        Ok(())
+    });
+    let mut interact = InteractSession::new(&mut session, &mut stdin, stdout);
+    interact.spawn(interact_opts).unwrap();
 
     stdin.close().unwrap();
 
@@ -86,6 +86,8 @@ fn interact_output_callback() {
 #[cfg(not(feature = "async"))]
 #[test]
 fn interact_callbacks_called_after_exit() {
+    use expectrl::interact::InteractOptions;
+
     let mut session = expectrl::spawn("echo 'Hello World'").unwrap();
 
     assert_eq!(
@@ -101,15 +103,13 @@ fn interact_callbacks_called_after_exit() {
     let mut lookup = Lookup::new();
     session
         .interact(&mut stdin, stdout)
-        .set_state(&mut state)
-        .on_output(|ctx| {
+        .spawn(&mut InteractOptions::new(&mut state).on_output(|ctx| {
             if lookup.on(ctx.buf, ctx.eof, "World")?.is_some() {
                 **ctx.state += 1;
             }
 
             Ok(())
-        })
-        .spawn()
+        }))
         .unwrap();
 
     stdin.close().unwrap();
@@ -121,6 +121,8 @@ fn interact_callbacks_called_after_exit() {
 #[cfg(not(any(feature = "async", feature = "polling")))]
 #[test]
 fn interact_callbacks_with_stream_redirection() {
+    use expectrl::interact::InteractOptions;
+
     let output_lines = vec![
         "NO_MATCHED\n".to_string(),
         "QWE\n".to_string(),
@@ -136,14 +138,13 @@ fn interact_callbacks_with_stream_redirection() {
     let mut input_handle = Lookup::new();
     session
         .interact(reader, &mut writer)
-        .on_input(|ctx| {
+        .spawn(InteractOptions::default().on_input(|ctx| {
             if input_handle.on(ctx.buf, ctx.eof, "QWE")?.is_some() {
                 ctx.session.send_line("Hello World")?;
             };
 
             Ok(())
-        })
-        .spawn()
+        }))
         .unwrap();
 
     let buffer = String::from_utf8_lossy(writer.get_ref());
@@ -154,30 +155,34 @@ fn interact_callbacks_with_stream_redirection() {
 #[cfg(not(any(feature = "async", feature = "polling")))]
 #[test]
 fn interact_filters() {
+    use expectrl::interact::InteractOptions;
+
     let reader = ReaderWithDelayEof::new("1009\nNO\n", Duration::from_secs(4));
     let mut writer = io::Cursor::new(vec![0; 2048]);
 
     let mut session = spawn("cat").unwrap();
     session
         .interact(reader, &mut writer)
-        .input_filter(|buf| {
-            // ignore 0 chars
-            let v = buf.iter().filter(|&&b| b != b'0').copied().collect();
-            Ok(v)
-        })
-        .output_filter(|buf| {
-            // Make NO -> YES
-            let v = buf
-                .chunks(2)
-                .flat_map(|s| match s {
-                    &[b'N', b'O'] => &[b'Y', b'E', b'S'],
-                    other => other,
+        .spawn(
+            InteractOptions::default()
+                .input_filter(|buf| {
+                    // ignore 0 chars
+                    let v = buf.iter().filter(|&&b| b != b'0').copied().collect();
+                    Ok(v)
                 })
-                .copied()
-                .collect();
-            Ok(v)
-        })
-        .spawn()
+                .output_filter(|buf| {
+                    // Make NO -> YES
+                    let v = buf
+                        .chunks(2)
+                        .flat_map(|s| match s {
+                            &[b'N', b'O'] => &[b'Y', b'E', b'S'],
+                            other => other,
+                        })
+                        .copied()
+                        .collect();
+                    Ok(v)
+                }),
+        )
         .unwrap();
 
     let buffer = String::from_utf8_lossy(writer.get_ref());
@@ -190,6 +195,8 @@ fn interact_filters() {
 #[cfg(all(unix, not(any(feature = "async", feature = "polling"))))]
 #[test]
 fn interact_context() {
+    use expectrl::interact::InteractOptions;
+
     let mut session = spawn("cat").unwrap();
 
     let reader = ListReaderWithDelayedEof::new(
@@ -206,9 +213,7 @@ fn interact_context() {
     let mut input_data = Lookup::new();
     let mut output_data = Lookup::new();
 
-    let state = session
-        .interact(reader, &mut writer)
-        .set_state((0, 0))
+    let mut opts = InteractOptions::new((0, 0))
         .on_input(|ctx| {
             if input_data.on(ctx.buf, ctx.eof, "QWE\n")?.is_some() {
                 ctx.state.0 += 1;
@@ -224,10 +229,17 @@ fn interact_context() {
             }
 
             Ok(())
-        })
-        .spawn()
+        });
+
+    let is_alive = session
+        .interact(reader, &mut writer)
+        .spawn(&mut opts)
         .unwrap();
 
+    let state = opts.into_inner();
+
+    assert!(is_alive);
+
     assert_eq!(state.0, 4);
     assert!(state.1 > 0, "{:?}", state.1);
 
@@ -242,6 +254,8 @@ fn interact_on_output_not_matched() {
     // Which may cause it to stay buffered in session.
     // Verify this buffer was cleaned and 123 won't be accessed then.
 
+    use expectrl::interact::InteractOptions;
+
     let reader = ListReaderWithDelayedEof::new(
         vec![
             "QWE\n".to_string(),
@@ -256,9 +270,7 @@ fn interact_on_output_not_matched() {
     let mut input = Lookup::new();
 
     let mut session = spawn("cat").unwrap();
-    let state = session
-        .interact(reader, &mut writer)
-        .set_state((0, 0))
+    let mut opts = InteractOptions::new((0, 0))
         .on_input(|ctx| {
             if input.on(ctx.buf, ctx.eof, "QWE\n")?.is_some() {
                 ctx.state.0 += 1;
@@ -274,10 +286,17 @@ fn interact_on_output_not_matched() {
         .on_idle(|_ctx| {
             std::thread::sleep(Duration::from_millis(500));
             Ok(())
-        })
-        .spawn()
+        });
+
+    let is_alive = session
+        .interact(reader, &mut writer)
+        .spawn(&mut opts)
         .unwrap();
 
+    let state = opts.into_inner();
+
+    assert!(is_alive);
+
     assert_eq!(state.0, 2);
     assert_eq!(state.1, 0);
 
@@ -320,6 +339,8 @@ fn interact_on_output_not_matched() {
 #[cfg(feature = "async")]
 #[test]
 fn interact_stream_redirection() {
+    use expectrl::interact::InteractOptions;
+
     futures_lite::future::block_on(async {
         let commands = "Hello World\nIt works :)\n";
 
@@ -328,7 +349,11 @@ fn interact_stream_redirection() {
 
         let mut session = expectrl::spawn("cat").unwrap();
 
-        session.interact(reader, &mut writer).spawn().await.unwrap();
+        session
+            .interact(reader, &mut writer)
+            .spawn(InteractOptions::default())
+            .await
+            .unwrap();
 
         let buffer = String::from_utf8_lossy(writer.get_ref());
         let buffer = buffer.trim_end_matches(char::from(0));
@@ -340,6 +365,39 @@ fn interact_stream_redirection() {
     });
 }
 
+#[cfg(feature = "async")]
+#[test]
+fn interact_output_callback() {
+    use expectrl::{
+        interact::{actions::lookup::Lookup, InteractOptions, InteractSession},
+        stream::stdin::Stdin,
+    };
+
+    let mut session = expectrl::spawn("sleep 1 && echo 'Hello World'").unwrap();
+
+    let mut stdin = Stdin::open().unwrap();
+    let stdout = std::io::sink();
+
+    let mut otps = InteractOptions::new((0, Lookup::new())).on_output(|ctx| {
+        if ctx.state.1.on(ctx.buf, ctx.eof, "World")?.is_some() {
+            ctx.state.0 += 1;
+        }
+
+        Ok(())
+    });
+    let mut interact = InteractSession::new(&mut session, &mut stdin, stdout);
+    futures_lite::future::block_on(interact.spawn(&mut otps)).unwrap();
+
+    let (state, _) = otps.into_inner();
+
+    stdin.close().unwrap();
+
+    // fixme: sometimes it's 0
+    //        I guess because the process gets down to fast.
+
+    assert!(matches!(state, 1 | 0), "{state:?}");
+}
+
 struct ListReaderWithDelayedEof {
     lines: Vec<String>,
     eof_timeout: Duration,
diff --git a/tests/io.rs b/tests/io.rs
index c2c44ca..8296540 100644
--- a/tests/io.rs
+++ b/tests/io.rs
@@ -379,7 +379,7 @@ fn try_read_after_process_exit() {
 
         match _p_try_read(&mut proc, &mut [0; 128]) {
             Ok(n) => {
-                assert_eq!(n, 59);
+                assert!(n > 0);
                 assert!(_p_try_read(&mut proc, &mut [0; 128]).is_err());
                 assert!(_p_try_read(&mut proc, &mut [0; 128]).is_err());
                 assert!(_p_is_empty(&mut proc).unwrap());
@@ -466,13 +466,10 @@ fn continues_try_reads() {
 #[test]
 #[cfg(not(target_os = "macos"))]
 #[cfg(not(windows))]
-fn automatic_stop_of_interact() {
+fn automatic_stop_of_interact_on_eof() {
     let mut p = Session::spawn(Command::new("ls")).unwrap();
     _p_interact(&mut p).unwrap();
 
-    // It may be finished not only because process is done but
-    // also because it reached EOF.
-
     // check that second spawn works
     let mut p = Session::spawn(Command::new("ls")).unwrap();
     _p_interact(&mut p).unwrap();
@@ -666,7 +663,7 @@ fn _p_try_read(proc: &mut Session, buf: &mut [u8]) -> std::io::Result<usize> {
 
 #[cfg(unix)]
 fn _p_interact(proc: &mut Session) -> Result<(), expectrl::Error> {
-    use expectrl::stream::stdin::Stdin;
+    use expectrl::{interact::InteractOptions, stream::stdin::Stdin};
     use std::io::stdout;
 
     let mut stdin = Stdin::open()?;
@@ -674,11 +671,15 @@ fn _p_interact(proc: &mut Session) -> Result<(), expectrl::Error> {
 
     #[cfg(not(feature = "async"))]
     {
-        proc.interact(&mut stdin, stdout).spawn()?;
+        proc.interact(&mut stdin, stdout)
+            .spawn(InteractOptions::default())?;
     }
     #[cfg(feature = "async")]
     {
-        block_on(proc.interact(&mut stdin, stdout).spawn())?;
+        block_on(
+            proc.interact(&mut stdin, stdout)
+                .spawn(InteractOptions::default()),
+        )?;
     }
 
     stdin.close()
diff --git a/tests/session.rs b/tests/session.rs
index c557361..6147889 100644
--- a/tests/session.rs
+++ b/tests/session.rs
@@ -52,6 +52,8 @@ fn send() {
 #[cfg(windows)]
 #[test]
 fn send() {
+    use std::io::Write;
+
     let mut session = spawn("powershell -C type").unwrap();
     #[cfg(not(feature = "async"))]
     {