use std::sync::Mutex;
use iced::{Subscription, Task};
use plushie_ext::codec::Codec;
use plushie_ext::message::{Message, StdinEvent};
use plushie_ext::protocol::IncomingMessage;
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_ext::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();
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").unwrap();
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, writer, _transport_guard, _token) = transport.into_parts();
let is_headless = has_flag("--headless") || has_flag("--mock");
if is_headless {
plushie_renderer_lib::emitters::init_output(writer);
} else {
let channel_writer = crate::output::spawn_writer_thread(writer);
plushie_renderer_lib::emitters::init_output(Box::new(channel_writer));
}
let ext_keys = builder
.extension_keys()
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
if has_flag("--mock") {
crate::headless::run(
forced_codec,
builder.build_dispatcher(),
crate::headless::Mode::Mock,
max_sessions,
&ext_keys,
transport_name,
reader,
expected_token.as_deref(),
);
return Ok(());
}
if has_flag("--headless") {
crate::headless::run(
forced_codec,
builder.build_dispatcher(),
crate::headless::Mode::Headless,
max_sessions,
&ext_keys,
transport_name,
reader,
expected_token.as_deref(),
);
return Ok(());
}
let mut reader = reader;
let codec = crate::startup::detect_codec(forced_codec, &mut reader);
Codec::set_global(codec);
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, transport_name) {
log_hello_error(&e);
return Ok(());
}
let initial = crate::startup::read_required_settings(&codec, &mut reader);
crate::startup::validate_settings(&initial.settings, expected_token.as_deref(), &codec);
let iced_settings = plushie_renderer_lib::settings::parse_iced_settings(&initial.settings);
let font_bytes = crate::startup::collect_font_bytes(&initial.settings);
let (tx, rx) = tokio::sync::mpsc::channel::<StdinEvent>(64);
spawn_stdin_reader(tx, reader);
*STDIN_RX.lock().expect("STDIN_RX lock poisoned") = 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_ext::app::PlushieAppBuilder>> =
Mutex::new(Some(builder));
iced::daemon(
move || {
let (settings, fonts) = settings_slot
.lock()
.expect("settings_slot lock poisoned")
.take()
.unwrap_or_default();
let dispatcher = builder_slot
.lock()
.expect("builder_slot lock poisoned")
.take()
.expect("daemon init closure called more than once")
.build_dispatcher();
let effect_handler = Box::new(crate::effects::NativeEffectHandler);
let mut app = App::new(dispatcher, effect_handler);
app.scale_factor = plushie_renderer_lib::app::validate_scale_factor(
settings
.get("scale_factor")
.and_then(|v| v.as_f64())
.map(plushie_ext::prop_helpers::f64_to_f32)
.unwrap_or(1.0),
);
let effects = app.core.apply(IncomingMessage::Settings { settings });
for effect in effects {
match effect {
plushie_ext::engine::CoreEffect::ExtensionConfig(config) => {
app.dispatcher.init_all(
&config,
&app.theme,
app.core.default_text_size,
app.core.default_font,
);
}
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);
}
}