Files
ai-app/app-rust/examples/phone.rs
T
irisandClaude Opus 5 4ccfda6b8e Delete the decisions and design logs; scripts, rigs and xtask off the root
Iris: "remove both decisions and iris.md. I've decided to instead make
decisions when planning with agents rather than after they do things, and
they're both too long for me to wanna read, + don't cover all the
decisions I'll wanna make about the code anyways. I'll just naturally run
into things for now. Todo is important though."

So docs/DECISIONS.md (850 lines) and docs/IRIS.md (1,986) are gone, and
AGENTS.md now says not to start another: raise a choice while planning it
with her, otherwise decide it and put the reasoning at the code it
governs. The TODO lists stay. docs/SUBAGENTS_DECISIONS.md went with them
-- same artefact, same reasoning, and she did not name it, so its six
decisions were folded into docs/SUBAGENTS.md rather than deleted.

Deleting the logs left ~30 citations dangling in code comments and docs.
Each states its reason inline and cited the file only for provenance, so
they now read "decided 2026-09-07" or name the module doc that carries
the reasoning.

The root had six things that were not a program or a document. Moved,
per "I only meant top level sh files":

  run-tests.sh, test-wg-tunnel.sh, wg-setup-host.sh  -> scripts/
  rigs/                                              -> scripts/rigs/
  xtask/                                             -> scripts/xtask/

A project's own scripts stayed with the project: app/*.sh, app-rust/*.sh,
iris/*.sh and server/enroll-link.sh did not move.

`target/` at the root is deleted and cannot come back: there was never a
workspace there, and the 29 MB was only xtask's scratch space, now in
scripts/xtask/target/. `cargo xtask apk` still runs from the repo root
and now publishes to scripts/build/outputs/apk/<mode>/ -- one directory
deep, because that is what Dev Updater's `*/build/outputs/apk/*/*.apk`
discovery pattern needs, and scripts/xtask/build would have been two.

Verified: ./scripts/run-tests.sh and `cd iris && cargo test` green, clippy
and fmt clean everywhere, `cargo xtask apk debug --abi x86_64` builds and
signs an APK carrying lib/x86_64/libai_app.so at the new publish path, and
the repo root is now eleven entries with no build output among them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:16:24 -04:00

147 lines
5.7 KiB
Rust

//! Layer 2 of docs/RUST.md's "Three test layers": the fixture-backed
//! transcript screen in a phone-shaped window, for looking at.
//!
//! iris/run-headless.sh phone --phone --shot /tmp/phone.png -- -p transcript-fixture
//!
//! `--phone` sets the headless sway output to the phone's own 1080x2424
//! and exports `IRIS_SCALE=2.55`, so this draws at the density Iris's
//! phone reports (`ai_app::ui::fixture::PHONE_SCALE`) rather than the
//! desktop's 1.0 -- same screen, same fixture and the same folding as
//! the Android bench and the headless tests, so what differs between a
//! screenshot here and one from the phone is the renderer, never the
//! data.
//!
//! `--message TEXT` (through `RUN_HEADLESS_ARGS`) starts with that text
//! already in the composer, `\n` for a newline -- the composer's grown
//! and overflowing states are otherwise unreachable here, since this
//! window has no keyboard to type into (UI_RULES.md's "check the states
//! you can't see by default"). `--typed TEXT` *enters* the same text
//! instead, one character per 100ms: laying the composer out from
//! scratch and growing one already on screen are different cases, and
//! only the second reproduced the caret landing in the bar's padding
//! (decided 2026-09-08).
//!
//! No server: `transcript-fixture` embeds the transcript. Colour,
//! spacing, type and anything a person has to *see* is answered here;
//! anything with an assertion behind it belongs in `tests/
//! phone_screen.rs` one layer down.
use iris::prelude::*;
use winit::{dpi::PhysicalSize, window::WindowAttributes};
/// The `--ime PX` argument: the bottom inset a keyboard would report,
/// applied after the first frame the way Android's `on_insets_changed`
/// does. The composer's keyboard-open layout is otherwise unreachable
/// here, and it is where its mask went wrong before (see
/// `ActiveData::own_mask`).
fn ime_argv() -> Option<f32> {
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
if arg == "--ime" {
return args.next()?.parse().ok();
}
}
None
}
/// The `--message TEXT` argument, with `\n` taken as a newline so a
/// multi-line message survives one shell word.
fn message_argv() -> Option<String> {
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
if arg == "--message" {
return Some(args.next()?.replace("\\n", "\n"));
}
}
None
}
/// The `--typed TEXT` argument: the same text as `--message`, but
/// *entered* rather than preloaded -- one insertion per 100ms, into a
/// focused field, the way a person types. The two are different cases
/// for layout: `--message` is laid out from scratch on the first frame,
/// while this grows an already-drawn composer, which is the path
/// Iris's 2026-09-08 phone report is about.
fn typed_argv() -> Option<String> {
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
if arg == "--typed" {
return Some(args.next()?.replace("\\n", "\n"));
}
}
None
}
fn main() {
DefaultApp::<Client>::run();
}
#[derive(DefaultUiState)]
pub struct Client {
ui_state: DefaultUiState,
#[allow(dead_code)]
screen: Option<ai_app::ui::TranscriptScreen>,
}
impl DefaultAppState for Client {
fn window_attributes() -> WindowAttributes {
WindowAttributes::default()
.with_title("iris transcript (bench fixture)")
.with_inner_size(PhysicalSize::new(
ai_app::ui::fixture::PHONE_WIDTH,
ai_app::ui::fixture::PHONE_HEIGHT,
))
}
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) {
Ok(opened) => {
if let Some(message) = message_argv() {
opened.screen.composer.field.edit(rsc).set(&message);
}
if let Some(text) = typed_argv() {
let field = opened.screen.composer.field;
let redraw = rsc.tasks.redraw_handle();
rsc.spawn_task(async move |mut ctx| {
for ch in text.chars() {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
ctx.update(move |state: &mut Client, rsc| {
state.set_focus(Some(field));
let end = rsc[field].text().len();
let mut edit = field.edit(rsc);
if edit.text.caret().is_none() {
edit.set_cursor_byte(end);
}
edit.insert(&ch.to_string());
});
redraw.request_redraw();
}
});
}
if let Some(inset) = ime_argv() {
opened.screen.composer.set_bottom_inset(rsc, inset);
}
Some(opened.screen)
}
// On screen rather than a panic: this window exists to be
// looked at, and "the fixture stopped folding" is something
// to read, not a process that vanished (UI_RULES.md).
Err(message) => {
let text = wtext(format!("Couldn't fold the bench fixture: {message}"))
.color(Color::WHITE)
.wrap(true)
.pad(dp(16))
.add_strong(rsc)
.any();
ui_state.set_root(text);
None
}
};
Self { ui_state, screen }
}
}