use iced::{Subscription, Task};
use parking_lot::Mutex;
use plushie_renderer_engine::Codec;
use plushie_widget_sdk::protocol::IncomingMessage;
use plushie_widget_sdk::runtime::{Message, StdinEvent};
use plushie_renderer_lib::App;
use plushie_renderer_lib::emitters::emit_hello;
use super::stdin::{STDIN_RX, spawn_stdin_reader};
fn log_hello_error(err: &std::io::Error) {
if err.kind() != std::io::ErrorKind::BrokenPipe {
log::error!("failed to emit hello: {err}");
}
}
pub(crate) fn run(builder: plushie_widget_sdk::app::PlushieAppBuilder) -> iced::Result {
let args: Vec<String> = std::env::args().collect();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
run_inner(builder, args)
}
fn run_inner(
mut builder: plushie_widget_sdk::app::PlushieAppBuilder,
args: Vec<String>,
) -> iced::Result {
let has_flag = |flag: &str| args.iter().any(|a| a == flag);
let forced_codec = if has_flag("--msgpack") {
Some(Codec::MsgPack)
} else if has_flag("--json") {
Some(Codec::Json)
} else {
None
};
let max_sessions = args
.windows(2)
.find(|w| w[0] == "--max-sessions")
.and_then(|w| w[1].parse::<usize>().ok())
.unwrap_or(1)
.max(1);
let exec_command = args
.windows(2)
.find(|w| w[0] == "--exec")
.map(|w| w[1].clone());
let listen_arg = if has_flag("--listen") {
let idx = args
.iter()
.position(|a| a == "--listen")
.expect("has_flag(--listen) gates this branch");
let next = args.get(idx + 1);
match next {
Some(s) if !s.starts_with("--") => Some(Some(s.as_str())),
_ => Some(None), }
} else {
None
};
let transport = if let Some(addr_arg) = listen_arg {
let addr = match crate::transport::ListenAddr::parse(addr_arg) {
Ok(a) => a,
Err(e) => {
log::error!("invalid --listen address: {e}");
return Ok(());
}
};
match crate::transport::Transport::listen(&addr, exec_command.as_deref()) {
Ok(t) => t,
Err(e) => {
log::error!("failed to start listen transport: {e}");
return Ok(());
}
}
} else if let Some(cmd) = &exec_command {
match crate::transport::Transport::exec(cmd) {
Ok(t) => t,
Err(e) => {
log::error!("failed to start exec transport: {e}");
return Ok(());
}
}
} else {
#[cfg(windows)]
set_binary_mode();
crate::transport::Transport::stdio()
};
let transport_name = transport.name();
let expected_token = transport.expected_token.clone();
let (reader, mut writer_opt, _transport_guard, _token) = {
let (r, w, g, t) = transport.into_parts();
(r, Some(w), g, t)
};
let ext_keys = builder
.custom_type_names()
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let session_factory = builder.take_session_factory();
if has_flag("--mock") {
let writer = writer_opt.take().expect("writer available on mock path");
crate::headless::run(
forced_codec,
crate::headless::Mode::Mock,
max_sessions,
&ext_keys,
transport_name,
reader,
writer,
expected_token.as_deref(),
session_factory,
);
return Ok(());
}
if has_flag("--headless") {
let writer = writer_opt
.take()
.expect("writer available on headless path");
crate::headless::run(
forced_codec,
crate::headless::Mode::Headless,
max_sessions,
&ext_keys,
transport_name,
reader,
writer,
expected_token.as_deref(),
session_factory,
);
return Ok(());
}
let mut reader = reader;
let codec = match crate::startup::detect_codec(forced_codec, &mut reader) {
Ok(c) => c,
Err(e) => {
log::error!("{e}");
return Ok(());
}
};
let writer = writer_opt.take().expect("writer consumed by headless path");
let channel_writer = crate::output::spawn_writer_thread(writer);
let sink = plushie_renderer_lib::WriterSink::new(Box::new(channel_writer), codec);
plushie_renderer_lib::emitters::init_sink(Box::new(sink));
plushie_renderer_lib::emitters::install_panic_hook();
let ext_key_refs: Vec<&str> = ext_keys.iter().map(|s| s.as_str()).collect();
if let Err(e) = emit_hello("windowed", "wgpu", &ext_key_refs, &["iced"], transport_name) {
log_hello_error(&e);
return Ok(());
}
let initial = match crate::startup::read_required_settings(&codec, &mut reader) {
Ok(v) => v,
Err(e) => {
crate::startup::emit_startup_error(&codec, &e);
return Ok(());
}
};
if let Err(e) = crate::startup::validate_settings(
&initial.settings,
expected_token.as_deref(),
&ext_key_refs,
) {
crate::startup::emit_startup_error(&codec, &e);
return Ok(());
}
let iced_settings = plushie_renderer_lib::settings::parse_iced_settings(&initial.settings);
plushie_renderer_lib::settings::apply_validate_props(&initial.settings);
let font_bytes = crate::startup::collect_font_bytes(&initial.settings);
let (tx, rx) = tokio::sync::mpsc::channel::<StdinEvent>(64);
spawn_stdin_reader(codec, tx, reader);
*STDIN_RX.lock() = Some(rx);
let settings_slot: Mutex<Option<(serde_json::Value, Vec<Vec<u8>>)>> =
Mutex::new(Some((initial.settings, font_bytes)));
let builder_slot: Mutex<Option<plushie_widget_sdk::app::PlushieAppBuilder>> =
Mutex::new(Some(builder));
iced::daemon(
move || {
let (settings, fonts) = settings_slot.lock().take().unwrap_or_default();
let builder = builder_slot
.lock()
.take()
.expect("daemon init closure called more than once")
.widget_set(&plushie_widget_sdk::runtime::iced_widget_set());
let registry = builder.build();
let effect_handler = Box::new(plushie_renderer_lib::NativeEffectHandler);
let sink = plushie_renderer_lib::emitters::sink_arc();
let mut app = App::new(registry, effect_handler, sink);
app.set_codec(codec);
app.scale_factor = plushie_renderer_lib::app::validate_scale_factor(
settings
.get("scale_factor")
.and_then(|v| v.as_f64())
.map(plushie_widget_sdk::prop_helpers::f64_to_f32)
.unwrap_or(1.0),
);
let effects = app.core.apply(IncomingMessage::Settings { settings });
for effect in effects {
use plushie_renderer_engine::{CoreEffect, StateChange};
match effect {
CoreEffect::StateChange(StateChange::WidgetConfig(config)) => {
let ctx = plushie_widget_sdk::registry::InitCtx {
config: &config,
theme: &app.theme,
default_text_size: app.core.default_text_size,
default_font: app.core.default_font,
};
app.registry.init_all(&ctx);
}
other => {
log::warn!("unexpected effect from initial Settings: {other:?}");
}
}
}
let font_tasks: Vec<Task<Message>> = fonts
.into_iter()
.map(|bytes| {
iced::font::load(bytes).map(|result| {
if let Err(e) = result {
log::error!("font load error: {e:?}");
}
Message::NoOp
})
})
.collect();
let task = if font_tasks.is_empty() {
Task::none()
} else {
Task::batch(font_tasks)
};
(app, task)
},
App::update,
App::view_window,
)
.title(App::title_for_window)
.subscription(|app: &App| {
Subscription::batch([
app.renderer_subscriptions(),
Subscription::run(super::stdin::stdin_subscription).map(Message::Stdin),
])
})
.theme(App::theme_for_window)
.scale_factor(App::scale_factor_for_window)
.settings(iced_settings)
.run()
}
#[cfg(windows)]
#[allow(unsafe_code)]
fn set_binary_mode() {
unsafe extern "C" {
fn _setmode(fd: i32, mode: i32) -> i32;
}
const O_BINARY: i32 = 0x8000;
unsafe {
_setmode(0, O_BINARY);
_setmode(1, O_BINARY);
}
}