From c4c3242679283f5520ab30780f4d23c80248dbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Franc=CC=A7ois=20Pe=CC=81russe?= Date: Mon, 21 Sep 2026 22:32:45 -0400 Subject: [PATCH 1/6] Add screen-size command Adds `codea screen-size [preset]` over the setScreenSize/getScreenSize MCP tools, so a project can be screenshotted at different sizes and orientations without editing it. Modelled on `idle-timer`/`paused` rather than `runtime`: the presets apply to the viewer, not to a project, so there is no project argument. The five preset ids are validated client-side, which rejects a typo without a round trip, and the display name is mapped locally since the device returns the bare id. The device half is not implemented yet, so this errors against current Codea builds until it ships. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014kJYH24sqV9xNvhVb86bMZ --- README.md | 2 +- SKILL.md | 3 ++ skill/SKILL.md | 3 ++ src/main.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++++++- src/mcp.rs | 10 ++++++ 5 files changed, 102 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9b721e8..e5438c4 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ codea logs --follow - `ls`, `new`, `rename`, `move`, `delete` - `pull`, `push` - `run`, `stop`, `restart`, `pause`, `resume`, `paused`, `exec` -- `screenshot`, `idle-timer`, `logs`, `clear-logs` +- `screenshot`, `screen-size`, `idle-timer`, `logs`, `clear-logs` - `collections ls|new|delete` - `templates ls|add|remove` - `deps ls|available|add|remove` diff --git a/SKILL.md b/SKILL.md index a79d815..16dd995 100644 --- a/SKILL.md +++ b/SKILL.md @@ -259,6 +259,8 @@ Always prefer `--wait` over asking the user to manually switch to Codea first. | `codea paused [on\|off]` | Get or set paused state | | `codea screenshot [--output ]` | Capture a screenshot | | `codea idle-timer ` | Get or set idle timer | +| `codea screen-size` | Show the viewer's screen size preset | +| `codea screen-size ` | Set the viewer's screen size: `match-display`, `iphone-portrait`, `iphone-landscape`, `tv`, `square` | | `codea logs` | Get log output | | `codea logs --head N` | Get first N lines | | `codea logs --tail N` | Get last N lines | @@ -459,6 +461,7 @@ light.pop() - Always `pull` before editing to get the latest files from device - Use `sleep 2` or similar between `run` and `screenshot` to let the project render a frame - `exec` requires a project to already be running +- `screen-size` resizes the running viewer, so use it to screenshot a project at several sizes or orientations without touching the project's code. It needs a project running, and it is a Codea Pro feature — expect an error rather than a paywall if either is missing - Screenshot returns a PNG — save it and use vision to inspect results; do not open it in an external app unless the user explicitly asks - `codea logs` accumulates all output since last `clear-logs`; use `--head 20` when Codea is spamming a repeated error to find the original cause - File paths on device use `codea://` URIs internally; you don't need to deal with these directly diff --git a/skill/SKILL.md b/skill/SKILL.md index a79d815..16dd995 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -259,6 +259,8 @@ Always prefer `--wait` over asking the user to manually switch to Codea first. | `codea paused [on\|off]` | Get or set paused state | | `codea screenshot [--output ]` | Capture a screenshot | | `codea idle-timer ` | Get or set idle timer | +| `codea screen-size` | Show the viewer's screen size preset | +| `codea screen-size ` | Set the viewer's screen size: `match-display`, `iphone-portrait`, `iphone-landscape`, `tv`, `square` | | `codea logs` | Get log output | | `codea logs --head N` | Get first N lines | | `codea logs --tail N` | Get last N lines | @@ -459,6 +461,7 @@ light.pop() - Always `pull` before editing to get the latest files from device - Use `sleep 2` or similar between `run` and `screenshot` to let the project render a frame - `exec` requires a project to already be running +- `screen-size` resizes the running viewer, so use it to screenshot a project at several sizes or orientations without touching the project's code. It needs a project running, and it is a Codea Pro feature — expect an error rather than a paywall if either is missing - Screenshot returns a PNG — save it and use vision to inspect results; do not open it in an external app unless the user explicitly asks - `codea logs` accumulates all output since last `clear-logs`; use `--head 20` when Codea is spamming a repeated error to find the original cause - File paths on device use `codea://` URIs internally; you don't need to deal with these directly diff --git a/src/main.rs b/src/main.rs index 1af111c..d70a111 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,14 @@ use crate::discover::{DiscoverEvent, discover_devices_with_progress}; use crate::local::create_local_project; use crate::mcp::{MCPClient, maybe_base64_text}; +const SCREEN_SIZE_PRESETS: [(&str, &str); 5] = [ + ("match-display", "Match Display"), + ("iphone-portrait", "iPhone (Portrait)"), + ("iphone-landscape", "iPhone (Landscape)"), + ("tv", "TV (16:9)"), + ("square", "Square (1:1)"), +]; + #[derive(Parser, Debug)] #[command(name = "codea")] #[command(about = "Codea CLI — connect to Codea on your device.")] @@ -53,6 +61,8 @@ enum Commands { Screenshot(ScreenshotArgs), #[command(name = "idle-timer")] IdleTimer(IdleTimerArgs), + #[command(name = "screen-size")] + ScreenSize(ScreenSizeArgs), Logs(LogsArgs), #[command(name = "clear-logs")] ClearLogs(ProfileArg), @@ -156,6 +166,17 @@ struct IdleTimerArgs { profile: String, } +#[derive(Args, Debug)] +struct ScreenSizeArgs { + #[arg( + value_name = "preset", + help = "match-display, iphone-portrait, iphone-landscape, tv, or square" + )] + preset: Option, + #[arg(long, default_value = "default")] + profile: String, +} + #[derive(Args, Debug)] struct LogsArgs { #[arg(long)] @@ -358,6 +379,7 @@ fn run() -> Result<()> { Commands::Exec(args) => exec_command(args, cli.wait), Commands::Screenshot(args) => screenshot_command(args, cli.wait), Commands::IdleTimer(args) => idle_timer_command(args, cli.wait), + Commands::ScreenSize(args) => screen_size_command(args, cli.wait), Commands::Logs(args) => logs_command(args, cli.wait), Commands::ClearLogs(args) => clear_logs_command(&args.profile, cli.wait), Commands::New(args) => new_command(args, cli.wait), @@ -807,6 +829,56 @@ fn idle_timer_command(args: IdleTimerArgs, wait: bool) -> Result<()> { Ok(()) } +fn screen_size_command(args: ScreenSizeArgs, wait: bool) -> Result<()> { + if let Some(preset) = args.preset.as_deref() + && !SCREEN_SIZE_PRESETS.iter().any(|(id, _)| *id == preset) + { + bail!( + "Invalid preset '{}'. Use one of: {}.", + preset, + screen_size_preset_ids() + ); + } + + let mut client = client_for_profile(&args.profile, wait)?; + match args.preset.as_deref() { + None => { + let reported = client.get_screen_size()?; + let preset = reported.trim(); + if preset.is_empty() { + bail!("Codea did not report a screen size."); + } + println!("Screen size: {}", describe_screen_size(preset)); + } + Some(preset) => { + let message = client.set_screen_size(preset)?; + if message.trim().is_empty() { + println!("Screen size: {}", describe_screen_size(preset)); + } else { + println!("{message}"); + } + } + } + Ok(()) +} + +fn screen_size_preset_ids() -> String { + SCREEN_SIZE_PRESETS + .iter() + .map(|(id, _)| *id) + .collect::>() + .join(", ") +} + +/// Codea reports the current screen size as the bare preset id, matching the +/// plain-text shape of `getRuntime`; the display name is mapped here. +fn describe_screen_size(preset: &str) -> String { + match SCREEN_SIZE_PRESETS.iter().find(|(id, _)| *id == preset) { + Some((id, label)) => format!("{id} \u{2014} {label}"), + None => preset.to_string(), + } +} + fn logs_command(args: LogsArgs, wait: bool) -> Result<()> { let mut client = client_for_profile(&args.profile, wait)?; if args.follow { @@ -1652,7 +1724,10 @@ fn project_name(path: &str) -> String { #[cfg(test)] mod tests { - use super::{completion_kind_name, parse_collection_project, resolve_runtime_filter}; + use super::{ + completion_kind_name, describe_screen_size, parse_collection_project, + resolve_runtime_filter, + }; #[test] fn parse_collection_project_supports_icloud_prefix() { @@ -1694,4 +1769,13 @@ mod tests { assert_eq!(completion_kind_name(3), Some("function")); assert_eq!(completion_kind_name(999), None); } + + #[test] + fn describe_screen_size_falls_back_to_unknown_id() { + assert_eq!( + describe_screen_size("match-display"), + "match-display \u{2014} Match Display" + ); + assert_eq!(describe_screen_size("holodeck"), "holodeck"); + } } diff --git a/src/mcp.rs b/src/mcp.rs index 3faadf6..e2fbaca 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -282,6 +282,16 @@ impl MCPClient { )?)) } + pub fn get_screen_size(&mut self) -> Result { + Ok(Self::text(&self.call_tool("getScreenSize", json!({}))?)) + } + + pub fn set_screen_size(&mut self, preset: &str) -> Result { + Ok(Self::text( + &self.call_tool("setScreenSize", json!({"preset": preset}))?, + )) + } + pub fn get_function_help(&mut self, function_name: &str) -> Result { Self::json_result( &self.call_tool("getFunctionHelp", json!({"functionName": function_name}))?, From 65b594bebd414fdc21a3eb75ef43790fd9fbcaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Franc=CC=A7ois=20Pe=CC=81russe?= Date: Mon, 21 Sep 2026 23:20:43 -0400 Subject: [PATCH 2/6] Correct the screen-size Pro note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Air Code server only runs for Pro subscribers, so every `codea` command already requires Pro and a non-Pro user cannot reach `screen-size` to be told about it. Singling this command out as Pro-gated, and promising an error instead of a paywall, described a path that cannot happen. Replaces it with the two failures that are reachable — no project running, and a non-viewer host such as the standalone Runner — and notes that WIDTH/HEIGHT change with the preset, confirmed on device across all five presets. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014kJYH24sqV9xNvhVb86bMZ --- SKILL.md | 2 +- skill/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SKILL.md b/SKILL.md index 16dd995..ad1ea6e 100644 --- a/SKILL.md +++ b/SKILL.md @@ -461,7 +461,7 @@ light.pop() - Always `pull` before editing to get the latest files from device - Use `sleep 2` or similar between `run` and `screenshot` to let the project render a frame - `exec` requires a project to already be running -- `screen-size` resizes the running viewer, so use it to screenshot a project at several sizes or orientations without touching the project's code. It needs a project running, and it is a Codea Pro feature — expect an error rather than a paywall if either is missing +- `screen-size` resizes the running viewer, so use it to screenshot a project at several sizes or orientations without touching the project's code. `WIDTH`/`HEIGHT` inside Lua change with the preset, so this is a real resize, not a crop. It needs a project running and only the app's viewer supports it, so it reports an error rather than failing silently when no project is running or the target is a standalone Runner - Screenshot returns a PNG — save it and use vision to inspect results; do not open it in an external app unless the user explicitly asks - `codea logs` accumulates all output since last `clear-logs`; use `--head 20` when Codea is spamming a repeated error to find the original cause - File paths on device use `codea://` URIs internally; you don't need to deal with these directly diff --git a/skill/SKILL.md b/skill/SKILL.md index 16dd995..ad1ea6e 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -461,7 +461,7 @@ light.pop() - Always `pull` before editing to get the latest files from device - Use `sleep 2` or similar between `run` and `screenshot` to let the project render a frame - `exec` requires a project to already be running -- `screen-size` resizes the running viewer, so use it to screenshot a project at several sizes or orientations without touching the project's code. It needs a project running, and it is a Codea Pro feature — expect an error rather than a paywall if either is missing +- `screen-size` resizes the running viewer, so use it to screenshot a project at several sizes or orientations without touching the project's code. `WIDTH`/`HEIGHT` inside Lua change with the preset, so this is a real resize, not a crop. It needs a project running and only the app's viewer supports it, so it reports an error rather than failing silently when no project is running or the target is a standalone Runner - Screenshot returns a PNG — save it and use vision to inspect results; do not open it in an external app unless the user explicitly asks - `codea logs` accumulates all output since last `clear-logs`; use `--head 20` when Codea is spamming a repeated error to find the original cause - File paths on device use `codea://` URIs internally; you don't need to deal with these directly From 1b7d6a0680185cd742d7f0acc39bde36ec211e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Franc=CC=A7ois=20Pe=CC=81russe?= Date: Mon, 21 Sep 2026 23:51:47 -0400 Subject: [PATCH 3/6] Add a changelog and cargo-release configuration Records the history back to 0.1.0 from the tags, and configures cargo-release to move the Unreleased section and its compare links to the new version on bump. Distribution is by cargo-dist from the pushed tag, so publishing to crates.io stays off. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TqPb55Kii6dZHot2PPGMFq --- CHANGELOG.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++ release.toml | 12 ++++++++ 2 files changed, 90 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 release.toml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..96b43a4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,78 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- `codea screen-size [preset]` reads or sets the screen size of the running + project's viewer, so a project can be checked at another aspect ratio or + orientation. Presets are `match-display`, `iphone-portrait`, + `iphone-landscape`, `tv` and `square`. Needs a Codea build that provides the + `setScreenSize` and `getScreenSize` Air Code tools. + +## [0.1.6] - 2026-04-02 + +### Added + +- MCP usage examples in the README. + +### Fixed + +- `codea status` reports the paused and idle timer states correctly. + +### Changed + +- Reworked the release mechanism. + +## [0.1.5] - 2026-03-30 + +### Added + +- WiX template for the Windows MSI installer. + +## [0.1.4] - 2026-03-29 + +### Added + +- Windows installer packaging. + +## [0.1.3] - 2026-03-29 + +### Added + +- Update notifications, cached between runs. + +## [0.1.2] - 2026-03-29 + +### Added + +- A progress spinner while discovering devices. + +## [0.1.1] - 2026-03-29 + +### Changed + +- More reliable fallback when creating a local project. + +## [0.1.0] - 2026-03-29 + +First release. Connects to a Codea or Carbide runtime over Air Code to discover +hosts, save connection profiles, manage projects, collections, templates and +dependencies, run and stop projects, execute Lua, inspect and change a project's +runtime type, query the API docs and autocomplete data, capture screenshots, +stream logs, push and pull project files, and create local projects. Ships +Homebrew, PowerShell and MSI installers. + +[Unreleased]: https://github.com/twolivesleft/codea-cli/compare/v0.1.6...HEAD +[0.1.6]: https://github.com/twolivesleft/codea-cli/compare/v0.1.5...v0.1.6 +[0.1.5]: https://github.com/twolivesleft/codea-cli/compare/v0.1.4...v0.1.5 +[0.1.4]: https://github.com/twolivesleft/codea-cli/compare/v0.1.3...v0.1.4 +[0.1.3]: https://github.com/twolivesleft/codea-cli/compare/v0.1.2...v0.1.3 +[0.1.2]: https://github.com/twolivesleft/codea-cli/compare/v0.1.1...v0.1.2 +[0.1.1]: https://github.com/twolivesleft/codea-cli/compare/v0.1.0...v0.1.1 +[0.1.0]: https://github.com/twolivesleft/codea-cli/releases/tag/v0.1.0 diff --git a/release.toml b/release.toml new file mode 100644 index 0000000..6f2fbc0 --- /dev/null +++ b/release.toml @@ -0,0 +1,12 @@ +# Distribution is handled by cargo-dist from the pushed tag, not by crates.io. +publish = false + +allow-branch = ["main"] +tag-name = "v{{version}}" +tag-message = "chore: Release {{crate_name}} version {{version}}" +pre-release-commit-message = "chore: release {{version}}" + +pre-release-replacements = [ + { file = "CHANGELOG.md", search = "## \\[Unreleased\\]", replace = "## [Unreleased]\n\n## [{{version}}] - {{date}}", exactly = 1 }, + { file = "CHANGELOG.md", search = "\\[Unreleased\\]: (.*)/compare/v(.*)\\.\\.\\.HEAD", replace = "[Unreleased]: ${1}/compare/v{{version}}...HEAD\n[{{version}}]: ${1}/compare/v${2}...v{{version}}", exactly = 1 }, +] From 5140c82ab83fc9c96bb2d48901149380dccb5923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Franc=CC=A7ois=20Pe=CC=81russe?= Date: Mon, 21 Sep 2026 23:55:59 -0400 Subject: [PATCH 4/6] Keep cargo-release config in one file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release.toml duplicated publish, allow-branch and pre-release-commit-message from [package.metadata.release] in Cargo.toml, and cargo-release 1.1.6 gives the Cargo.toml section precedence: with tag-name set in both, a dry run tags from Cargo.toml. The values matched, so nothing behaved differently, but editing release.toml — the obvious place to look — would have silently done nothing for those three keys. Removes the Cargo.toml section so release.toml is the only source, and notes in the file why that section must stay gone. Verified by dry run: the tag is v0.1.7 from release.toml, publish = false is honoured (no packaging or upload step, unlike publish = true), and allow-branch still refuses a topic branch. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014kJYH24sqV9xNvhVb86bMZ --- Cargo.toml | 5 ----- release.toml | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1181a9b..e926106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,11 +29,6 @@ serde_json = "1" inherits = "release" lto = "thin" -[package.metadata.release] -publish = false -allow-branch = ["main"] -pre-release-commit-message = "chore: release {{version}}" - [package.metadata.wix] upgrade-guid = "088E8154-4DE5-4E0E-A1C0-5CC9EBC0027E" path-guid = "12910971-CFB5-405D-BCE1-383C7724B3D9" diff --git a/release.toml b/release.toml index 6f2fbc0..b9e9f27 100644 --- a/release.toml +++ b/release.toml @@ -1,3 +1,5 @@ +# The only home for cargo-release config: keys here are overridden by +# [package.metadata.release] in Cargo.toml, so that section must stay absent. # Distribution is handled by cargo-dist from the pushed tag, not by crates.io. publish = false From 980a3a33ddd9b95aa40cfab0c6cfddb724fc7324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Franc=CC=A7ois=20Pe=CC=81russe?= Date: Tue, 22 Sep 2026 00:15:34 -0400 Subject: [PATCH 5/6] Read the screen size as JSON with live dimensions getScreenSize now returns {"preset": id} plus width and height once the viewer has laid out, rather than the bare preset id, so a future custom size with an arbitrary width and height needs no second change to the wire format. Changing it now costs nothing because neither half has shipped. The dimensions are the viewer's live bounds, not the preset's nominal size, so under match-display they follow the window and reading the size is a query rather than a stored setting; the docs say so. They are omitted rather than null before first layout, so a lone width or height, or neither, prints the preset alone. setScreenSize is unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014kJYH24sqV9xNvhVb86bMZ --- CHANGELOG.md | 6 ++-- SKILL.md | 3 +- skill/SKILL.md | 3 +- src/main.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96b43a4..99cf12d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `codea screen-size [preset]` reads or sets the screen size of the running project's viewer, so a project can be checked at another aspect ratio or orientation. Presets are `match-display`, `iphone-portrait`, - `iphone-landscape`, `tv` and `square`. Needs a Codea build that provides the - `setScreenSize` and `getScreenSize` Air Code tools. + `iphone-landscape`, `tv` and `square`. Reading it also reports the viewer's + current pixel size, which is its live bounds rather than the preset's nominal + size, so under `match-display` it follows the window. Needs a Codea build that + provides the `setScreenSize` and `getScreenSize` Air Code tools. ## [0.1.6] - 2026-04-02 diff --git a/SKILL.md b/SKILL.md index ad1ea6e..311ecba 100644 --- a/SKILL.md +++ b/SKILL.md @@ -259,7 +259,7 @@ Always prefer `--wait` over asking the user to manually switch to Codea first. | `codea paused [on\|off]` | Get or set paused state | | `codea screenshot [--output ]` | Capture a screenshot | | `codea idle-timer ` | Get or set idle timer | -| `codea screen-size` | Show the viewer's screen size preset | +| `codea screen-size` | Show the viewer's screen size preset, and its current pixel size once the viewer has laid out | | `codea screen-size ` | Set the viewer's screen size: `match-display`, `iphone-portrait`, `iphone-landscape`, `tv`, `square` | | `codea logs` | Get log output | | `codea logs --head N` | Get first N lines | @@ -462,6 +462,7 @@ light.pop() - Use `sleep 2` or similar between `run` and `screenshot` to let the project render a frame - `exec` requires a project to already be running - `screen-size` resizes the running viewer, so use it to screenshot a project at several sizes or orientations without touching the project's code. `WIDTH`/`HEIGHT` inside Lua change with the preset, so this is a real resize, not a crop. It needs a project running and only the app's viewer supports it, so it reports an error rather than failing silently when no project is running or the target is a standalone Runner +- Reading `screen-size` is a query, not a stored setting: the size it reports is the viewer's live bounds, so under `match-display` it follows the window and can change with no `screen-size` call at all. A project that has only just started may report a preset with no size yet - Screenshot returns a PNG — save it and use vision to inspect results; do not open it in an external app unless the user explicitly asks - `codea logs` accumulates all output since last `clear-logs`; use `--head 20` when Codea is spamming a repeated error to find the original cause - File paths on device use `codea://` URIs internally; you don't need to deal with these directly diff --git a/skill/SKILL.md b/skill/SKILL.md index ad1ea6e..311ecba 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -259,7 +259,7 @@ Always prefer `--wait` over asking the user to manually switch to Codea first. | `codea paused [on\|off]` | Get or set paused state | | `codea screenshot [--output ]` | Capture a screenshot | | `codea idle-timer ` | Get or set idle timer | -| `codea screen-size` | Show the viewer's screen size preset | +| `codea screen-size` | Show the viewer's screen size preset, and its current pixel size once the viewer has laid out | | `codea screen-size ` | Set the viewer's screen size: `match-display`, `iphone-portrait`, `iphone-landscape`, `tv`, `square` | | `codea logs` | Get log output | | `codea logs --head N` | Get first N lines | @@ -462,6 +462,7 @@ light.pop() - Use `sleep 2` or similar between `run` and `screenshot` to let the project render a frame - `exec` requires a project to already be running - `screen-size` resizes the running viewer, so use it to screenshot a project at several sizes or orientations without touching the project's code. `WIDTH`/`HEIGHT` inside Lua change with the preset, so this is a real resize, not a crop. It needs a project running and only the app's viewer supports it, so it reports an error rather than failing silently when no project is running or the target is a standalone Runner +- Reading `screen-size` is a query, not a stored setting: the size it reports is the viewer's live bounds, so under `match-display` it follows the window and can change with no `screen-size` call at all. A project that has only just started may report a preset with no size yet - Screenshot returns a PNG — save it and use vision to inspect results; do not open it in an external app unless the user explicitly asks - `codea logs` accumulates all output since last `clear-logs`; use `--head 20` when Codea is spamming a repeated error to find the original cause - File paths on device use `codea://` URIs internally; you don't need to deal with these directly diff --git a/src/main.rs b/src/main.rs index d70a111..71e92d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -843,12 +843,11 @@ fn screen_size_command(args: ScreenSizeArgs, wait: bool) -> Result<()> { let mut client = client_for_profile(&args.profile, wait)?; match args.preset.as_deref() { None => { - let reported = client.get_screen_size()?; - let preset = reported.trim(); - if preset.is_empty() { - bail!("Codea did not report a screen size."); + let (preset, size) = parse_screen_size(&client.get_screen_size()?)?; + match size { + Some(size) => println!("Screen size: {}, {size}", describe_screen_size(&preset)), + None => println!("Screen size: {}", describe_screen_size(&preset)), } - println!("Screen size: {}", describe_screen_size(preset)); } Some(preset) => { let message = client.set_screen_size(preset)?; @@ -870,8 +869,8 @@ fn screen_size_preset_ids() -> String { .join(", ") } -/// Codea reports the current screen size as the bare preset id, matching the -/// plain-text shape of `getRuntime`; the display name is mapped here. +/// Maps a preset id to its display name, which lives here rather than on the +/// device so the reported id stays the contract. fn describe_screen_size(preset: &str) -> String { match SCREEN_SIZE_PRESETS.iter().find(|(id, _)| *id == preset) { Some((id, label)) => format!("{id} \u{2014} {label}"), @@ -879,6 +878,46 @@ fn describe_screen_size(preset: &str) -> String { } } +/// Codea reports the current screen size as `{"preset": id}`, plus `width` and +/// `height` once the viewer has laid out. Those two are omitted rather than +/// null before first layout, and they are the viewer's live bounds rather than +/// the preset's nominal size, so under `match-display` they follow the window. +fn parse_screen_size(text: &str) -> Result<(String, Option)> { + let trimmed = text.trim(); + if trimmed.is_empty() { + bail!("Codea did not report a screen size."); + } + let Ok(value) = serde_json::from_str::(trimmed) else { + bail!("Codea reported an unreadable screen size: {trimmed}"); + }; + + let preset = value + .get("preset") + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("Codea reported a screen size without a preset: {trimmed}"))?; + let size = match ( + value.get("width").and_then(Value::as_f64), + value.get("height").and_then(Value::as_f64), + ) { + (Some(width), Some(height)) => Some(format!( + "{} \u{00d7} {}", + format_dimension(width), + format_dimension(height) + )), + _ => None, + }; + Ok((preset.to_string(), size)) +} + +/// The viewer's bounds are fractional in principle, so avoid printing "1920.0". +fn format_dimension(value: f64) -> String { + if value.fract() == 0.0 { + format!("{}", value as i64) + } else { + format!("{value}") + } +} + fn logs_command(args: LogsArgs, wait: bool) -> Result<()> { let mut client = client_for_profile(&args.profile, wait)?; if args.follow { @@ -1725,7 +1764,7 @@ fn project_name(path: &str) -> String { #[cfg(test)] mod tests { use super::{ - completion_kind_name, describe_screen_size, parse_collection_project, + completion_kind_name, describe_screen_size, parse_collection_project, parse_screen_size, resolve_runtime_filter, }; @@ -1770,6 +1809,43 @@ mod tests { assert_eq!(completion_kind_name(999), None); } + #[test] + fn parse_screen_size_reads_preset_and_live_dimensions() { + let (preset, size) = + parse_screen_size(r#"{"height":1080,"preset":"tv","width":1920}"#).unwrap(); + assert_eq!(preset, "tv"); + assert_eq!(size.as_deref(), Some("1920 \u{00d7} 1080")); + } + + #[test] + fn parse_screen_size_accepts_a_viewer_that_has_not_laid_out() { + let (preset, size) = parse_screen_size(r#"{"preset":"tv"}"#).unwrap(); + assert_eq!(preset, "tv"); + assert_eq!(size, None); + } + + #[test] + fn parse_screen_size_ignores_a_lone_dimension() { + let (preset, size) = parse_screen_size(r#"{"preset":"square","width":1112}"#).unwrap(); + assert_eq!(preset, "square"); + assert_eq!(size, None); + } + + #[test] + fn parse_screen_size_keeps_fractional_bounds_but_not_trailing_zeros() { + let (_, size) = + parse_screen_size(r#"{"preset":"match-display","width":1592.5,"height":1192}"#) + .unwrap(); + assert_eq!(size.as_deref(), Some("1592.5 \u{00d7} 1192")); + } + + #[test] + fn parse_screen_size_rejects_unusable_answers() { + assert!(parse_screen_size(" ").is_err()); + assert!(parse_screen_size("tv").is_err()); + assert!(parse_screen_size(r#"{"width":1920,"height":1080}"#).is_err()); + } + #[test] fn describe_screen_size_falls_back_to_unknown_id() { assert_eq!( From 8c1cb5b085e369ab35f4a1739bd312eeb178e82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Franc=CC=A7ois=20Pe=CC=81russe?= Date: Tue, 22 Sep 2026 07:42:28 -0400 Subject: [PATCH 6/6] Claim only what the device run showed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reported size demonstrably comes from laid-out bounds rather than a table of preset sizes: ResizableRenderer.Preset.width is 0 for tv, square and matchWindow, yet tv reports 1920 x 1080, and the five presets cross-check to the pixel against WIDTH/HEIGHT read inside Lua. That part stands. Following the window under match-display does not: it is a sound inference from the renderer tracking view bounds, but resizing the window needs Stage Manager or split view and hands on the device, so it has not been observed. The absent-dimensions branch has never fired either — polling after a run returns "no project running" until the size is already there. Both now read as expected rather than tested, and the changelog states only what shipped. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014kJYH24sqV9xNvhVb86bMZ --- CHANGELOG.md | 4 ++-- SKILL.md | 2 +- skill/SKILL.md | 2 +- src/main.rs | 5 +++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99cf12d..adace80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 project's viewer, so a project can be checked at another aspect ratio or orientation. Presets are `match-display`, `iphone-portrait`, `iphone-landscape`, `tv` and `square`. Reading it also reports the viewer's - current pixel size, which is its live bounds rather than the preset's nominal - size, so under `match-display` it follows the window. Needs a Codea build that + current pixel size, taken from its laid-out bounds rather than the preset's + nominal size. Needs a Codea build that provides the `setScreenSize` and `getScreenSize` Air Code tools. ## [0.1.6] - 2026-04-02 diff --git a/SKILL.md b/SKILL.md index 311ecba..e09d449 100644 --- a/SKILL.md +++ b/SKILL.md @@ -462,7 +462,7 @@ light.pop() - Use `sleep 2` or similar between `run` and `screenshot` to let the project render a frame - `exec` requires a project to already be running - `screen-size` resizes the running viewer, so use it to screenshot a project at several sizes or orientations without touching the project's code. `WIDTH`/`HEIGHT` inside Lua change with the preset, so this is a real resize, not a crop. It needs a project running and only the app's viewer supports it, so it reports an error rather than failing silently when no project is running or the target is a standalone Runner -- Reading `screen-size` is a query, not a stored setting: the size it reports is the viewer's live bounds, so under `match-display` it follows the window and can change with no `screen-size` call at all. A project that has only just started may report a preset with no size yet +- Reading `screen-size` is a live query, not a stored setting: the size it reports is the viewer's laid-out bounds rather than the preset's nominal size. Under `match-display` it is expected to follow the window, though that has not been observed — resizing the window on an iPad needs Stage Manager or split view. It can in principle report a preset with no size before the viewer lays out, but that has never been seen in practice, so don't build a workflow around it - Screenshot returns a PNG — save it and use vision to inspect results; do not open it in an external app unless the user explicitly asks - `codea logs` accumulates all output since last `clear-logs`; use `--head 20` when Codea is spamming a repeated error to find the original cause - File paths on device use `codea://` URIs internally; you don't need to deal with these directly diff --git a/skill/SKILL.md b/skill/SKILL.md index 311ecba..e09d449 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -462,7 +462,7 @@ light.pop() - Use `sleep 2` or similar between `run` and `screenshot` to let the project render a frame - `exec` requires a project to already be running - `screen-size` resizes the running viewer, so use it to screenshot a project at several sizes or orientations without touching the project's code. `WIDTH`/`HEIGHT` inside Lua change with the preset, so this is a real resize, not a crop. It needs a project running and only the app's viewer supports it, so it reports an error rather than failing silently when no project is running or the target is a standalone Runner -- Reading `screen-size` is a query, not a stored setting: the size it reports is the viewer's live bounds, so under `match-display` it follows the window and can change with no `screen-size` call at all. A project that has only just started may report a preset with no size yet +- Reading `screen-size` is a live query, not a stored setting: the size it reports is the viewer's laid-out bounds rather than the preset's nominal size. Under `match-display` it is expected to follow the window, though that has not been observed — resizing the window on an iPad needs Stage Manager or split view. It can in principle report a preset with no size before the viewer lays out, but that has never been seen in practice, so don't build a workflow around it - Screenshot returns a PNG — save it and use vision to inspect results; do not open it in an external app unless the user explicitly asks - `codea logs` accumulates all output since last `clear-logs`; use `--head 20` when Codea is spamming a repeated error to find the original cause - File paths on device use `codea://` URIs internally; you don't need to deal with these directly diff --git a/src/main.rs b/src/main.rs index 71e92d5..3c6ba3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -880,8 +880,9 @@ fn describe_screen_size(preset: &str) -> String { /// Codea reports the current screen size as `{"preset": id}`, plus `width` and /// `height` once the viewer has laid out. Those two are omitted rather than -/// null before first layout, and they are the viewer's live bounds rather than -/// the preset's nominal size, so under `match-display` they follow the window. +/// null before first layout, and they are the viewer's laid-out bounds rather +/// than the preset's nominal size, so under `match-display` they are expected +/// to follow the window. fn parse_screen_size(text: &str) -> Result<(String, Option)> { let trimmed = text.trim(); if trimmed.is_empty() {