#![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;
#[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};
}
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;
#[cfg(feature = "direct")]
pub use plushie_widget_sdk as widget_sdk;
pub use plushie_core_macros::{PlushieEnum, WidgetCommand, WidgetEvent, WidgetProps};
#[cfg(feature = "direct")]
pub const RENDERER_VERSION: &str = plushie_renderer_lib::RENDERER_VERSION;
#[cfg(all(feature = "wire", not(feature = "direct")))]
pub const RENDERER_VERSION: &str = env!("CARGO_PKG_VERSION");
use command::Command;
use event::Event;
use settings::{ExitReason, RestartPolicy, Settings, WindowConfig};
use subscription::Subscription;
#[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![],
)
}
pub fn id(&self) -> &str {
&self.id
}
pub fn type_name(&self) -> &str {
&self.type_name
}
pub fn props(&self) -> &plushie_core::protocol::Props {
&self.props
}
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(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ViewList(Vec<View>);
impl ViewList {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn windows(&self) -> &[View] {
&self.0
}
pub fn into_windows(self) -> Vec<View> {
self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
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(),
},
}
}
}
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()
}
}
pub trait App: Send + 'static {
type Model: Send + 'static;
fn init() -> (Self::Model, Command);
fn update(model: &mut Self::Model, event: Event) -> Command;
fn view(model: &Self::Model, widgets: &mut widget::WidgetRegistrar) -> ViewList;
fn subscribe(_model: &Self::Model) -> Vec<Subscription> {
vec![]
}
fn settings() -> Settings {
Settings::default()
}
fn window_config(_model: &Self::Model) -> WindowConfig {
WindowConfig::default()
}
fn handle_renderer_exit(_model: &mut Self::Model, _reason: ExitReason) {}
fn restart_policy() -> RestartPolicy {
RestartPolicy::default()
}
}
pub type Result = std::result::Result<(), Error>;
pub fn run<A: App>() -> Result {
#[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>(),
},
}
}
#[cfg(feature = "wire")]
mod dispatch {
pub enum ModeDecision {
Connect(super::ConnectOpts),
Spawn(Option<String>),
}
pub fn detect_mode() -> Option<ModeDecision> {
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,
}));
}
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)));
}
}
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" => {
return None;
}
other => {
log::warn!("unknown PLUSHIE_MODE `{other}`; falling back to default");
}
}
}
None
}
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
}
}
#[cfg(feature = "wire")]
#[derive(Debug, Clone, Default)]
pub struct ConnectOpts {
pub socket: Option<String>,
pub token: Option<String>,
}
#[cfg(feature = "wire")]
pub fn run_with_renderer<A: App>(binary_path: &str) -> Result {
runner::wire::run_wire::<A>(binary_path)
}
#[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)
}
#[cfg(feature = "wire")]
pub fn run_spawn<A: App>() -> Result {
let binary = runner::wire_discovery::discover_renderer()?;
runner::wire::run_wire::<A>(&binary)
}
#[cfg(feature = "wire")]
pub fn run_connect<A: App>(opts: ConnectOpts) -> 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::<A>(resolved)
}
#[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)
}
#[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();
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()
}