plushie 0.7.0

Desktop GUI framework for Rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
//! Plushie: a desktop GUI framework for Rust.
//!
//! Build native desktop applications with the Elm architecture:
//! define your model, handle events in `update`, and describe
//! your UI in `view`.
//!
//! # Quick start
//!
//! ```no_run
//! use plushie::prelude::*;
//!
//! struct Counter { count: i32 }
//!
//! impl App for Counter {
//!     type Model = Self;
//!
//!     fn init() -> (Self, Command) {
//!         (Counter { count: 0 }, Command::none())
//!     }
//!
//!     fn update(model: &mut Self, event: Event) -> Command {
//!         match event.widget_match() {
//!             Some(Click("inc")) => model.count += 1,
//!             Some(Click("dec")) => model.count -= 1,
//!             _ => {}
//!         }
//!         Command::none()
//!     }
//!
//!     fn view(model: &Self, _widgets: &mut WidgetRegistrar) -> ViewList {
//!         window("main").title("Counter").child(
//!             column().spacing(8.0).padding(16)
//!                 .child(text(&format!("Count: {}", model.count)))
//!                 .child(
//!                     row().spacing(8.0)
//!                         .child(button("inc", "+"))
//!                         .child(button("dec", "-"))
//!                 )
//!         ).into()
//!     }
//! }
//!
//! fn main() -> plushie::Result {
//!     plushie::run::<Counter>()
//! }
//! ```
//!
//! # Two modes
//!
//! `plushie::run::<A>()` is feature-agnostic. It dispatches to
//! whichever runner is compiled in:
//!
//! - **Direct mode** (`direct` feature, default): In-process iced
//!   rendering. No subprocess, no serialization.
//! - **Wire mode** (`wire` feature): Spawns a renderer binary and
//!   communicates over stdin/stdout. Auto-discovers the binary via
//!   `PLUSHIE_BINARY_PATH`, then a custom build output under
//!   `target/plushie-renderer/`, then a downloaded stock binary under
//!   `target/plushie/bin/`, then `PATH`.
//!
//! When both features are enabled, direct wins. Pass an explicit
//! renderer path via `run_with_renderer` (wire feature only) to
//! force a specific wire binary.
//!
//! Builds with neither runner feature are supported for library,
//! query, and test-session APIs, but cannot launch an app. In that
//! configuration, [`run`] returns [`Error::NoRunnerFeature`] instead
//! of trying to pick a renderer.
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![deny(missing_docs)]

pub mod animation;
pub mod automation;
pub mod cli;
pub mod command;
pub mod derive_support;
#[cfg(feature = "dev")]
pub mod dev;
mod error;
pub mod event;
pub mod prelude;
pub mod query;
pub mod route;
pub mod runner;
pub(crate) mod runtime;

/// Re-exports of runtime internals for in-repo integration tests.
///
/// - `SubOp` and `SubscriptionManager` back
///   `test::TestSession::last_subscription_ops`.
/// - `diff_tree` plus `apply_patch` and `try_apply_patch` let
///   tree-diff proptests round-trip patches against arbitrary
///   `TreeNode` pairs.
///
/// This module is public because Rust integration tests compile as
/// external crates. The exported names are intentionally unstable and
/// are not part of the SDK contract for application code.
#[doc(hidden)]
pub mod runtime_internals {
    pub use crate::runtime::subscriptions::{SubOp, SubscriptionManager};
    pub use crate::runtime::tree_diff::{PatchOp, apply_patch, diff_tree, try_apply_patch};
}

/// Runtime tuning constants exposed to tests and advanced callers.
pub mod runtime_config {
    pub use crate::runtime::DISPATCH_DEPTH_LIMIT;
}
pub mod selection;
pub mod settings;
pub mod state;
pub mod subscription;
pub mod test;
pub mod types;
pub mod ui;
pub mod undo;
pub mod widget;

pub use error::Error;

// Re-export the widget SDK for widget authors who also use the app SDK.
//
// Widget authorship requires direct-mode rendering (or a custom
// renderer binary built against plushie-widget-sdk). Wire-only builds
// omit the alias; the renderer subprocess provides widget impls.
#[cfg(feature = "direct")]
pub use plushie_widget_sdk as widget_sdk;

// Re-export the derive macros for widget authoring.
pub use plushie_core_macros::{PlushieEnum, WidgetCommand, WidgetEvent, WidgetProps};

/// Version string of the renderer this SDK was built against.
///
/// Matches `plushie-renderer-lib`'s `CARGO_PKG_VERSION` at build
/// time. Wire mode compares the string against the renderer's
/// advertised version in the `hello` message; a mismatch does not
/// abort the handshake (the wire-protocol version is separate), but
/// it does get logged so version skew surfaces early.
///
/// Host SDKs in other languages keep their own synced per-SDK
/// `BINARY_VERSION` files. The Rust SDK uses this constant instead.
#[cfg(feature = "direct")]
pub const RENDERER_VERSION: &str = plushie_renderer_lib::RENDERER_VERSION;

/// Version string of the renderer this SDK was built against.
///
/// Wire-only builds don't depend on `plushie-renderer-lib`, so the
/// value comes straight from `CARGO_PKG_VERSION`, which the workspace
/// keeps in lock-step with the renderer crate at release time.
#[cfg(all(feature = "wire", not(feature = "direct")))]
pub const RENDERER_VERSION: &str = env!("CARGO_PKG_VERSION");

// ---------------------------------------------------------------------------
// App trait
// ---------------------------------------------------------------------------

use command::Command;
use event::Event;
use settings::{ExitReason, RestartPolicy, Settings, WindowConfig};
use subscription::Subscription;

/// A view tree returned from [`App::view`].
///
/// Built using UI builder functions (`window`, `column`, `button`,
/// `text`, etc.). The Rust SDK keeps `View` as its own authoring
/// type and converts it to the wire-level
/// [`plushie_core::protocol::TreeNode`] at the runtime boundary.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct View {
    pub(crate) id: String,
    pub(crate) type_name: String,
    pub(crate) props: plushie_core::protocol::Props,
    pub(crate) children: Vec<View>,
}

impl View {
    pub(crate) fn new(
        id: String,
        type_name: impl Into<String>,
        props: plushie_core::protocol::Props,
        children: Vec<View>,
    ) -> Self {
        Self {
            id,
            type_name: type_name.into(),
            props,
            children,
        }
    }

    pub(crate) fn empty() -> Self {
        Self::new(
            String::new(),
            "container",
            plushie_core::protocol::Props::default(),
            vec![],
        )
    }

    /// Stable widget ID for this node.
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Widget type name for this node.
    pub fn type_name(&self) -> &str {
        &self.type_name
    }

    /// Raw props carried by this node.
    pub fn props(&self) -> &plushie_core::protocol::Props {
        &self.props
    }

    /// Child views nested under this node.
    pub fn children(&self) -> &[View] {
        &self.children
    }

    pub(crate) fn into_tree_node(self) -> plushie_core::protocol::TreeNode {
        plushie_core::protocol::TreeNode {
            id: self.id,
            type_name: self.type_name,
            props: self.props,
            children: self
                .children
                .into_iter()
                .map(View::into_tree_node)
                .collect(),
        }
    }
}

/// The list of top-level windows returned from [`App::view`].
///
/// `view` returns `impl Into<ViewList>`, so implementors can yield a
/// bare [`View`] (single window), a `Vec<View>` (peer windows on
/// native targets), an array `[w1, w2]`, or `()` for "no windows"
/// (loading, transition, or error screens where there is nothing to
/// display yet).
///
/// The runtime collapses the list into a single tree root at the
/// boundary: a single entry is promoted to the root, multiple entries
/// wrap under a synthetic `"root"` container, and an empty list
/// renders an empty container. Apps never construct the synthetic
/// root by hand.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ViewList(Vec<View>);

impl ViewList {
    /// Create a new empty list (renders an empty tree).
    pub fn new() -> Self {
        Self(Vec::new())
    }

    /// Returns the windows as a slice.
    pub fn windows(&self) -> &[View] {
        &self.0
    }

    /// Consumes the list and returns the underlying `Vec<View>`.
    pub fn into_windows(self) -> Vec<View> {
        self.0
    }

    /// Returns `true` if the list contains no windows.
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Collapse the list into a single wire-level tree root.
    ///
    /// `[]` becomes an empty container, `[single]` promotes the view
    /// to the root directly, and multiple entries wrap under a
    /// synthetic `"root"` container so the diff pipeline can treat
    /// the tree uniformly.
    pub(crate) fn into_tree_node(self) -> plushie_core::protocol::TreeNode {
        match self.0.len() {
            0 => View::empty().into_tree_node(),
            1 => {
                let mut windows = self.0;
                windows.remove(0).into_tree_node()
            }
            _ => plushie_core::protocol::TreeNode {
                id: "root".to_string(),
                type_name: "container".to_string(),
                props: plushie_core::protocol::Props::default(),
                children: self.0.into_iter().map(View::into_tree_node).collect(),
            },
        }
    }
}

// Any builder or node that converts into a `View` (the `ui::*`
// builders, as well as `View` itself) can return directly from
// `App::view` for single-window apps - the runtime wraps the single
// entry for them. For multi-window, return `Vec<View>` or `[View; N]`
// instead; those have their own explicit impls below.
impl<T: Into<View>> From<T> for ViewList {
    fn from(view: T) -> Self {
        Self(vec![view.into()])
    }
}

impl From<Vec<View>> for ViewList {
    fn from(views: Vec<View>) -> Self {
        Self(views)
    }
}

impl From<Option<View>> for ViewList {
    fn from(view: Option<View>) -> Self {
        Self(view.into_iter().collect())
    }
}

impl<const N: usize> From<[View; N]> for ViewList {
    fn from(views: [View; N]) -> Self {
        Self(views.into())
    }
}

impl From<()> for ViewList {
    fn from(_: ()) -> Self {
        Self::new()
    }
}

/// The core trait for plushie applications.
///
/// Implement `init`, `update`, and `view` to create an app.
/// The runtime calls these in a loop: events flow in through
/// `update`, state changes flow out through `view`.
///
/// # Required methods
///
/// - [`init`](App::init): Create the initial model and startup commands.
/// - [`update`](App::update): Handle events and produce side effects.
/// - [`view`](App::view): Build the view tree from the current model.
///
/// # Optional methods
///
/// - [`subscribe`](App::subscribe): Declare active event subscriptions.
/// - [`settings`](App::settings): Application-level configuration.
/// - [`window_config`](App::window_config): Per-window defaults.
/// - [`handle_renderer_exit`](App::handle_renderer_exit): React to
///   renderer crashes (wire mode only).
///
/// # `Event` is non-exhaustive
///
/// The [`Event`] enum is `#[non_exhaustive]` so new variants can land
/// without a major version bump. Match expressions in `update` need
/// a wildcard arm (`_ => {}`) even when every current variant is
/// covered, otherwise the compiler refuses to build. The widget
/// match accessors ([`Event::widget_match`], `as_widget`, `as_key_press`,
/// etc.) sidestep the issue when only widget-style events matter.
pub trait App: Send + 'static {
    /// Application state. Owned by the runtime, passed to all callbacks.
    type Model: Send + 'static;

    /// Initialize the app. Returns the initial model and any
    /// startup commands (e.g., fetch data, start timers).
    fn init() -> (Self::Model, Command);

    /// Handle an event. Mutate the model and return commands
    /// for side effects. Called once per event.
    fn update(model: &mut Self::Model, event: Event) -> Command;

    /// Build the view tree from the current model. Called after
    /// every update. Returns the list of top-level windows to
    /// render, built from UI builder functions (`window`, `column`,
    /// `button`, `text`, etc.).
    ///
    /// [`ViewList`] has `From` impls that cover the common shapes,
    /// so a trailing `.into()` is enough in each case:
    ///
    /// - Single window: return a window builder or [`View`].
    ///   ```ignore
    ///   fn view(model: &Self, _widgets: &mut WidgetRegistrar) -> ViewList {
    ///       window("main").child(main_content(model)).into()
    ///   }
    ///   ```
    /// - Multiple peer windows: return a `Vec<View>`.
    ///   ```ignore
    ///   fn view(model: &Self, _widgets: &mut WidgetRegistrar) -> ViewList {
    ///       vec![
    ///           window("main").child(main_content(model)).into(),
    ///           window("detail").child(detail_content(model)).into(),
    ///       ].into()
    ///   }
    ///   ```
    /// - Nothing to show (loading, transition, error state): return an empty list.
    ///   ```ignore
    ///   fn view(_model: &Self, _widgets: &mut WidgetRegistrar) -> ViewList {
    ///       ViewList::new()
    ///   }
    ///   ```
    ///
    /// Use `widgets` to register composite widgets:
    /// ```ignore
    /// fn view(model: &Self, widgets: &mut WidgetRegistrar) -> ViewList {
    ///     window("main").child(
    ///         WidgetView::<MyWidget>::new("w1").register(widgets)
    ///     ).into()
    /// }
    /// ```
    fn view(model: &Self::Model, widgets: &mut widget::WidgetRegistrar) -> ViewList;

    /// Active subscriptions. Called after every update. The runtime
    /// diffs the returned list and starts/stops subscriptions as
    /// needed. Default: no subscriptions.
    fn subscribe(_model: &Self::Model) -> Vec<Subscription> {
        vec![]
    }

    /// Application-level settings (theme, fonts, text defaults).
    /// Called once at startup. Default: renderer defaults.
    fn settings() -> Settings {
        Settings::default()
    }

    /// Per-window defaults (title, size, position).
    /// Called once at startup. Default: renderer defaults.
    fn window_config(_model: &Self::Model) -> WindowConfig {
        WindowConfig::default()
    }

    /// Called synchronously before [`run`] (or `run_with_renderer`)
    /// returns [`Error::RendererExit`] when the renderer subprocess
    /// exits. Wire mode only; direct mode never calls this.
    ///
    /// Use this hook to save state, log diagnostics, or clean up
    /// model-side resources. The typed error coordinates
    /// process-level action (retry, exit, surface to user) after the
    /// hook returns.
    ///
    /// When auto-restart is active (see [`App::restart_policy`]), the
    /// hook fires on every restart attempt with the matching
    /// [`ExitReason`], then once more with
    /// [`ExitReason::MaxRestartsReached`] when the limit is hit.
    fn handle_renderer_exit(_model: &mut Self::Model, _reason: ExitReason) {}

    /// Restart policy for wire mode.
    ///
    /// The default policy restarts up to five times with exponential
    /// backoff and a thirty-second heartbeat. Return a custom policy
    /// to adjust limits or disable auto-restart entirely
    /// (`max_restarts: 0`).
    fn restart_policy() -> RestartPolicy {
        RestartPolicy::default()
    }
}

// ---------------------------------------------------------------------------
// Result type
// ---------------------------------------------------------------------------

/// Result type for plushie entry points.
///
/// The error type is the [`plushie::Error`](crate::Error) enum. Match
/// on specific variants to handle failure modes (spawn failure,
/// protocol mismatch, renderer exit) distinctly.
pub type Result = std::result::Result<(), Error>;

// ---------------------------------------------------------------------------
// Entry points
// ---------------------------------------------------------------------------

/// Run the app.
///
/// Feature-agnostic entry point. The runner is selected at compile
/// time from the enabled features:
///
/// - `direct` (default): in-process iced rendering. No subprocess.
/// - `wire`: spawns a renderer binary and talks stdin/stdout.
///
/// When both features are enabled, `direct` wins. To force wire mode
/// against a specific binary, use `run_with_renderer` (available
/// when the `wire` feature is enabled).
///
/// # Wire binary discovery
///
/// Wire mode locates the renderer in this order (first hit wins):
///
/// 1. `PLUSHIE_BINARY_PATH` environment variable (explicit; fails
///    fast if the file is missing).
/// 2. Custom build output at
///    `target/plushie-renderer/target/{release,debug}/<bin>`, where
///    `cargo plushie build` deposits widget-aware binaries.
/// 3. Downloaded stock binary at
///    `target/plushie/bin/plushie-renderer-<os>-<arch>[.exe]` from
///    `cargo plushie download`.
/// 4. `plushie-renderer` on `PATH` (on Windows, `plushie-renderer.exe`).
///
/// If none resolve to an executable, returns
/// [`Error::BinaryNotFound`] with guidance naming each install path.
///
/// # Errors
///
/// - [`Error::NoRunnerFeature`] if neither `direct` nor `wire` is
///   enabled at compile time.
/// - In direct mode: iced event-loop init failure, `init` panic,
///   or unrecoverable window-system failure.
/// - In wire mode: binary discovery failure, spawn failure, handshake
///   failure, or I/O error during the session.
pub fn run<A: App>() -> Result {
    // Mode precedence (highest to lowest):
    //   1. PLUSHIE_SOCKET env or --plushie-socket CLI -> wire-connect.
    //   2. PLUSHIE_BINARY_PATH env -> wire-spawn with explicit binary.
    //   3. PLUSHIE_MODE / --plushie-mode -> force mode explicitly.
    //   4. Feature default: direct if compiled, else wire-spawn via
    //      four-step discovery.
    #[cfg(feature = "wire")]
    {
        let mode = dispatch::detect_mode();
        if let Some(decision) = mode {
            return dispatch_wire_mode::<A>(decision);
        }
    }
    #[cfg(feature = "direct")]
    {
        runner::direct::run::<A>()
    }
    #[cfg(all(feature = "wire", not(feature = "direct")))]
    {
        let binary = runner::wire_discovery::discover_renderer()?;
        runner::wire::run_wire::<A>(&binary)
    }
    #[cfg(not(any(feature = "direct", feature = "wire")))]
    {
        Err(Error::NoRunnerFeature)
    }
}

#[cfg(feature = "wire")]
fn dispatch_wire_mode<A: App>(decision: dispatch::ModeDecision) -> Result {
    match decision {
        dispatch::ModeDecision::Connect(opts) => run_connect::<A>(opts),
        dispatch::ModeDecision::Spawn(opt_path) => match opt_path {
            Some(path) => run_with_renderer::<A>(&path),
            None => run_spawn::<A>(),
        },
    }
}

/// Mode detection helpers.
///
/// Split into its own module so the precedence logic is testable in
/// isolation. All fields are wire-gated because the mode decisions
/// they produce are wire-specific.
#[cfg(feature = "wire")]
mod dispatch {
    /// Outcome of mode detection.
    pub enum ModeDecision {
        /// Connect to an existing socket rather than spawning a binary.
        Connect(super::ConnectOpts),
        /// Spawn a renderer binary. `None` triggers auto-discovery;
        /// `Some(path)` uses the explicit binary.
        Spawn(Option<String>),
    }

    /// Inspect env + argv and return a decision, if any of the
    /// precedence-1-3 conditions fire. `None` means fall through to
    /// the feature-default branch.
    pub fn detect_mode() -> Option<ModeDecision> {
        // Step 1: socket (env + CLI).
        let cli_socket = cli_value("--plushie-socket");
        let env_socket = std::env::var("PLUSHIE_SOCKET").ok();
        if let Some(sock) = cli_socket.or(env_socket)
            && !sock.trim().is_empty()
        {
            let token =
                cli_value("--plushie-token").or_else(|| std::env::var("PLUSHIE_TOKEN").ok());
            return Some(ModeDecision::Connect(super::ConnectOpts {
                socket: Some(sock),
                token,
            }));
        }

        // Step 2: explicit binary path.
        if let Ok(path) = std::env::var("PLUSHIE_BINARY_PATH") {
            let trimmed = path.trim().to_string();
            if !trimmed.is_empty() {
                return Some(ModeDecision::Spawn(Some(trimmed)));
            }
        }

        // Step 3: PLUSHIE_MODE or --plushie-mode forcing.
        let forced = cli_value("--plushie-mode").or_else(|| std::env::var("PLUSHIE_MODE").ok());
        if let Some(mode) = forced {
            match mode.as_str() {
                "wire" => return Some(ModeDecision::Spawn(None)),
                "direct" => {
                    // Signal fall-through by returning None; the
                    // feature default branch will pick direct if it's
                    // compiled in.
                    return None;
                }
                other => {
                    log::warn!("unknown PLUSHIE_MODE `{other}`; falling back to default");
                }
            }
        }

        None
    }

    /// Extract the value for `--flag <value>` or `--flag=value` from
    /// `std::env::args()`. Returns `None` if the flag isn't present.
    fn cli_value(flag: &str) -> Option<String> {
        let prefix_eq = format!("{flag}=");
        let mut args = std::env::args().skip(1);
        while let Some(arg) = args.next() {
            if arg == flag {
                return args.next();
            }
            if let Some(rest) = arg.strip_prefix(&prefix_eq) {
                return Some(rest.to_string());
            }
        }
        None
    }
}

/// Options for [`run_connect`] that select which renderer socket to
/// connect to and what token (if any) to present on handshake.
///
/// Socket resolution: explicit `socket` > `PLUSHIE_SOCKET` env > error.
/// Token resolution: explicit `token` > `PLUSHIE_TOKEN` env > a JSON
/// negotiation line read from stdin with a 1-second timeout (mirrors
/// Elixir's `plushie.connect.ex:113`).
#[cfg(feature = "wire")]
#[derive(Debug, Clone, Default)]
pub struct ConnectOpts {
    /// Socket address (Unix path, `:port`, or `host:port`).
    pub socket: Option<String>,
    /// Auth token presented during handshake.
    pub token: Option<String>,
}

/// Run the app in wire mode against a specific renderer binary.
///
/// Escape hatch for apps that ship a custom renderer (for example, a
/// build with additional `PlushieWidget` implementations). The caller
/// supplies the path explicitly; no discovery is attempted.
///
/// Under the default feature set, consider pointing this at the stock
/// `plushie-renderer` binary via `env!("CARGO_BIN_EXE_plushie-renderer")`
/// from a build that depends on `plushie-renderer` as a dev-dep.
///
/// # Errors
///
/// Returns an error if the renderer binary cannot be spawned, the
/// protocol handshake fails (version mismatch or malformed hello),
/// or stdin/stdout I/O fails during the session.
#[cfg(feature = "wire")]
pub fn run_with_renderer<A: App>(binary_path: &str) -> Result {
    runner::wire::run_wire::<A>(binary_path)
}

/// Run the app in wire mode on a caller-provided tokio runtime.
///
/// Identical to [`run_with_renderer`] except SDK-local async tasks
/// ([`Command::task`](crate::command::Command::task),
/// streams, delayed events, and effect-timeout deadlines) are
/// spawned on the supplied [`tokio::runtime::Handle`] instead of
/// a privately owned runtime. Use this when the host app already
/// drives its own tokio runtime and wants to avoid a second one
/// being created.
///
/// # Errors
///
/// Same as [`run_with_renderer`].
#[cfg(feature = "wire")]
pub fn run_wire_with_runtime<A: App>(binary_path: &str, runtime: tokio::runtime::Handle) -> Result {
    runner::wire::run_wire_with_runtime::<A>(binary_path, runtime)
}

/// Run the app in wire mode by spawning a renderer binary discovered
/// via the four-step chain (env, custom build, downloaded, PATH).
///
/// This is the explicit building block behind the feature-default
/// branch of [`run`]. Use it when the auto-dispatch layers in [`run`]
/// would otherwise pick direct mode and you want to force a subprocess
/// renderer without providing a fixed binary path.
///
/// # Errors
///
/// See [`run_with_renderer`] for the wire-mode failure modes, plus
/// [`Error::BinaryNotFound`] when discovery fails.
#[cfg(feature = "wire")]
pub fn run_spawn<A: App>() -> Result {
    let binary = runner::wire_discovery::discover_renderer()?;
    runner::wire::run_wire::<A>(&binary)
}

/// Run the app by connecting to a renderer listening on an existing
/// socket.
///
/// Resolves the socket from `opts.socket` then `PLUSHIE_SOCKET`, and
/// the token from `opts.token` then `PLUSHIE_TOKEN` then a JSON
/// negotiation line read from stdin with a one-second timeout. The
/// resolved token is merged into the Settings message so the
/// renderer's listen-mode verification accepts the connection.
///
/// This is the curated wrapper that adds the stdin-negotiation
/// token fallback on top of the runner-layer
/// [`runner::wire::run_connect`] and builds the final [`ConnectOpts`]
/// for it.
///
/// # Errors
///
/// Returns [`Error::InvalidSettings`] when no socket can be resolved,
/// [`Error::Io`] on connect failures, and any of the normal wire
/// mode failure modes (protocol mismatch, encode / decode errors,
/// renderer disconnect).
#[cfg(feature = "wire")]
pub fn run_connect<A: App>(opts: ConnectOpts) -> Result {
    // Fill in the token from env + stdin if the caller didn't supply
    // one; keep the runner layer deterministic (it only looks at
    // opts.token + PLUSHIE_SOCKET) by resolving everything here.
    let token = opts
        .token
        .clone()
        .or_else(|| std::env::var("PLUSHIE_TOKEN").ok())
        .or_else(read_token_from_stdin);
    let resolved = ConnectOpts {
        socket: opts.socket,
        token,
    };
    runner::wire::run_connect::<A>(resolved)
}

/// Run the app by connecting to a renderer on a caller-provided
/// tokio runtime.
///
/// Mirrors [`run_connect`] for callers that already own a runtime;
/// SDK-local async tasks are spawned on the supplied
/// [`tokio::runtime::Handle`] instead of a privately owned one.
///
/// # Errors
///
/// Same as [`run_connect`].
#[cfg(feature = "wire")]
pub fn run_connect_with_runtime<A: App>(
    opts: ConnectOpts,
    runtime: tokio::runtime::Handle,
) -> Result {
    let token = opts
        .token
        .clone()
        .or_else(|| std::env::var("PLUSHIE_TOKEN").ok())
        .or_else(read_token_from_stdin);
    let resolved = ConnectOpts {
        socket: opts.socket,
        token,
    };
    runner::wire::run_connect_with_runtime::<A>(resolved, runtime)
}

/// Best-effort read of a newline-terminated JSON token line from stdin.
///
/// One-second timeout mirrors Elixir's `plushie.connect.ex:113` read.
/// Returns `None` on timeout, EOF, or parse failure so the caller can
/// proceed without a token when negotiation isn't in play.
#[cfg(feature = "wire")]
fn read_token_from_stdin() -> Option<String> {
    use std::io::BufRead;
    use std::sync::mpsc;
    use std::thread;
    use std::time::Duration;

    let (tx, rx) = mpsc::channel::<Option<String>>();
    thread::spawn(move || {
        let mut line = String::new();
        let n = std::io::stdin().lock().read_line(&mut line).unwrap_or(0);
        if n == 0 {
            let _ = tx.send(None);
            return;
        }
        let trimmed = line.trim();
        // Accept either a bare token string or a `{"token":"..."}`
        // JSON object.
        let parsed = serde_json::from_str::<serde_json::Value>(trimmed)
            .ok()
            .and_then(|v| {
                v.get("token")
                    .and_then(|t| t.as_str())
                    .map(str::to_string)
                    .or_else(|| v.as_str().map(str::to_string))
            });
        let _ = tx.send(parsed);
    });
    rx.recv_timeout(Duration::from_secs(1)).ok().flatten()
}