iris is the framework alone; the app is one crate in app-rust/

Iris: "the organization of the rust rewrite is a mess right now... there
shouldn't be anything related to the app inside of iris. Iris is supposed
to be the UI framework alone." And, on the crate count: "I'm confused why
the app only code needs more than one crate though."

Nine cargo workspaces become three, and the port's project code -- which
sat in five places, four of them inside the framework -- becomes one crate,
`ai-app`, in `app-rust/`:

  client-core                -> app-rust/src/client
  iris/transcript-ui         -> app-rust/src/ui
  iris/transcript-fixture    -> app-rust/src/ui/fixture.rs + tests/ + touch/
  iris/desktop-app           -> app-rust/src/desktop + src/bin_desktop.rs
  iris/android-app           -> app-rust/src/android + android-project/
  android-shell              -> app-rust/src/shell

iris/ keeps core, macro, the iris crate, tabs-ui and rig-input, and now
mentions no session, transcript, setup or server anywhere.

Only two of the old splits had a reason that survived reading. event-model
stays a crate at the repo root because server/ depends on it too, so a
crate is what makes the backend and the app agree by construction. The two
Android .so names looked like a hard constraint -- a package produces one
library artifact -- until P2 turned out to already plan merging those two
Android apps into one; both faces now come out of libai_app.so, picked
apart by features so `--no-default-features --features shell` keeps wgpu,
parley and iris out of the Compose app's APK. docs/RUST.md's "One app
crate" has the rest, including what each remaining feature is for.

DECISIONS.md and SUBAGENTS.md move into docs/ with everything else.

Verified: ./run-tests.sh and `cd iris && cargo test` green, clippy and fmt
clean in all five workspaces, `cargo ndk -t x86_64` links libai_app.so,
build-apk.sh produces an APK that installs and launches on this checkout's
emulator (Gl ... virgl, as expected), and the phone-sized headless
screenshot renders the transcript unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-08 23:36:38 -04:00
1 parent e9a6562dc6
commit 6d5a231f5c
100 files changed
+924 -3295

No files matched your search

+581
View File
@@ -0,0 +1,581 @@
//! A language the highlighter can colour, and the data-driven [`Rules`] each
//! one scans by. Ported from `app/.../Languages.kt`; see that file's doc for
//! why nearly every language is a row of data read by one shared scanner,
//! with Markdown the one exception (`super::markdown`).
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
C,
Coffeescript,
Cpp,
Csharp,
Dart,
Fish,
Go,
Java,
Javascript,
Json,
Kotlin,
Markdown,
Perl,
Php,
Python,
Ron,
Ruby,
Rust,
Shell,
Swift,
Toml,
Typescript,
}
impl Language {
/// Every value, for the same exhaustiveness check the Kotlin test runs
/// (`Language.entries`).
pub const ALL: [Language; 22] = [
Language::C,
Language::Coffeescript,
Language::Cpp,
Language::Csharp,
Language::Dart,
Language::Fish,
Language::Go,
Language::Java,
Language::Javascript,
Language::Json,
Language::Kotlin,
Language::Markdown,
Language::Perl,
Language::Php,
Language::Python,
Language::Ron,
Language::Ruby,
Language::Rust,
Language::Shell,
Language::Swift,
Language::Toml,
Language::Typescript,
];
}
/// What [`super::scan`] needs to know about one language -- data, not code,
/// so that adding a language is a row here rather than a branch anywhere.
#[derive(Debug, Clone, Default)]
pub struct Rules {
/// Words drawn as keywords. Only plain words; the scanner cannot reach
/// anything else.
pub keywords: HashSet<&'static str>,
/// Tokens that open a comment running to the end of the line.
pub line_comments: Vec<&'static str>,
/// Whether `line_comments` count only at the start of a word. The shells
/// need it: `$#`, `${#x}` and `a#b` are not comments.
pub line_comments_at_word_start: bool,
pub block_comment: Option<BlockComment>,
/// The string forms. The longest opener that matches wins, so `"""` is
/// tried before `"`.
pub quotes: Vec<Quote>,
pub attributes: Attributes,
/// Rust and RON: an optional `b`, `r`, n hashes, `"`, closing at `"` and n hashes.
pub raw_strings: bool,
/// Rust: `'` opens a character literal only when a backslash or one
/// character and a `'` follow. Otherwise it is a lifetime or a label.
pub lifetimes: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct BlockComment {
pub open: &'static str,
pub close: &'static str,
pub nests: bool,
}
/// One string form. `escapes` is whether a backslash escapes the closer
/// (and itself).
#[derive(Debug, Clone, Copy)]
pub struct Quote {
pub open: &'static str,
pub close: &'static str,
pub escapes: bool,
}
/// What opens a metadata span, of the shapes that exist across these languages.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Attributes {
#[default]
None,
/// `@` and a word: Kotlin and Java annotations, Python decorators.
AtWord,
/// `#[` or `#![` through the matching `]`: Rust and RON attributes.
HashBracket,
/// `#` at the start of a line, to the end of it: the C preprocessor.
HashLine,
/// `[` at the start of a line through the matching `]`: a TOML table header.
LineBracket,
}
const C_STYLE: BlockComment = BlockComment {
open: "/*",
close: "*/",
nests: false,
};
const NESTING: BlockComment = BlockComment {
open: "/*",
close: "*/",
nests: true,
};
const DOUBLE: Quote = Quote {
open: "\"",
close: "\"",
escapes: true,
};
const SINGLE: Quote = Quote {
open: "'",
close: "'",
escapes: true,
};
const TRIPLE_DOUBLE: Quote = Quote {
open: "\"\"\"",
close: "\"\"\"",
escapes: true,
};
const TRIPLE_SINGLE: Quote = Quote {
open: "'''",
close: "'''",
escapes: true,
};
fn words(list: &'static str) -> HashSet<&'static str> {
list.split_whitespace().collect()
}
/// The rules for one language. A `match` rather than a lazily-built map --
/// there is no once-per-process cost worth paying for in a language table
/// this small, and it sidesteps the Kotlin version's own workaround for
/// property initialization order.
pub fn rules_for(language: Language) -> Rules {
match language {
Language::C => Rules {
keywords: words(KEYWORDS_C),
line_comments: vec!["//"],
block_comment: Some(C_STYLE),
quotes: vec![DOUBLE, SINGLE],
attributes: Attributes::HashLine,
..Default::default()
},
Language::Cpp => Rules {
keywords: words(KEYWORDS_CPP),
line_comments: vec!["//"],
block_comment: Some(C_STYLE),
quotes: vec![DOUBLE, SINGLE],
attributes: Attributes::HashLine,
..Default::default()
},
Language::Csharp => Rules {
keywords: words(KEYWORDS_CSHARP),
line_comments: vec!["//"],
block_comment: Some(C_STYLE),
quotes: vec![DOUBLE, SINGLE],
..Default::default()
},
// `###` opens and closes a block comment and `#` opens a line one,
// which is why the scanner tries the block opener first.
Language::Coffeescript => Rules {
keywords: words(KEYWORDS_COFFEESCRIPT),
line_comments: vec!["#"],
block_comment: Some(BlockComment {
open: "###",
close: "###",
nests: false,
}),
quotes: vec![TRIPLE_DOUBLE, TRIPLE_SINGLE, DOUBLE, SINGLE],
..Default::default()
},
Language::Dart => Rules {
keywords: words(KEYWORDS_DART),
line_comments: vec!["//"],
block_comment: Some(NESTING),
quotes: vec![TRIPLE_DOUBLE, TRIPLE_SINGLE, DOUBLE, SINGLE],
attributes: Attributes::AtWord,
..Default::default()
},
Language::Fish => Rules {
keywords: words(KEYWORDS_FISH),
line_comments: vec!["#"],
line_comments_at_word_start: true,
quotes: vec![DOUBLE, SINGLE],
..Default::default()
},
Language::Go => Rules {
keywords: words(KEYWORDS_GO),
line_comments: vec!["//"],
block_comment: Some(C_STYLE),
quotes: vec![
DOUBLE,
SINGLE,
Quote {
open: "`",
close: "`",
escapes: false,
},
],
..Default::default()
},
Language::Java => Rules {
keywords: words(KEYWORDS_JAVA),
line_comments: vec!["//"],
block_comment: Some(C_STYLE),
quotes: vec![DOUBLE, SINGLE],
attributes: Attributes::AtWord,
..Default::default()
},
Language::Javascript => Rules {
keywords: words(KEYWORDS_JAVASCRIPT),
line_comments: vec!["//"],
block_comment: Some(C_STYLE),
quotes: vec![
DOUBLE,
SINGLE,
Quote {
open: "`",
close: "`",
escapes: true,
},
],
..Default::default()
},
Language::Json => Rules {
keywords: words(KEYWORDS_JSON),
quotes: vec![DOUBLE],
..Default::default()
},
Language::Kotlin => Rules {
keywords: words(KEYWORDS_KOTLIN),
line_comments: vec!["//"],
block_comment: Some(NESTING),
quotes: vec![
Quote {
open: "\"\"\"",
close: "\"\"\"",
escapes: false,
},
DOUBLE,
SINGLE,
],
attributes: Attributes::AtWord,
..Default::default()
},
Language::Perl => Rules {
keywords: words(KEYWORDS_PERL),
line_comments: vec!["#"],
quotes: vec![DOUBLE, SINGLE],
..Default::default()
},
Language::Php => Rules {
keywords: words(KEYWORDS_PHP),
line_comments: vec!["//", "#"],
block_comment: Some(C_STYLE),
quotes: vec![DOUBLE, SINGLE],
attributes: Attributes::AtWord,
..Default::default()
},
Language::Python => Rules {
keywords: words(KEYWORDS_PYTHON),
line_comments: vec!["#"],
quotes: vec![TRIPLE_DOUBLE, TRIPLE_SINGLE, DOUBLE, SINGLE],
attributes: Attributes::AtWord,
..Default::default()
},
Language::Ron => Rules {
keywords: words(KEYWORDS_RON),
line_comments: vec!["//"],
block_comment: Some(NESTING),
quotes: vec![DOUBLE, SINGLE],
attributes: Attributes::HashBracket,
raw_strings: true,
..Default::default()
},
Language::Ruby => Rules {
keywords: words(KEYWORDS_RUBY),
line_comments: vec!["#"],
quotes: vec![DOUBLE, SINGLE],
..Default::default()
},
Language::Rust => Rules {
keywords: words(KEYWORDS_RUST),
line_comments: vec!["//"],
block_comment: Some(NESTING),
// No `'` here: `lifetimes` decides when one opens a character literal.
quotes: vec![DOUBLE],
attributes: Attributes::HashBracket,
raw_strings: true,
lifetimes: true,
..Default::default()
},
Language::Shell => Rules {
keywords: words(KEYWORDS_SHELL),
line_comments: vec!["#"],
line_comments_at_word_start: true,
// A shell's single quotes are literal: `'a\'` is not one string.
quotes: vec![
DOUBLE,
Quote {
open: "'",
close: "'",
escapes: false,
},
],
..Default::default()
},
Language::Swift => Rules {
keywords: words(KEYWORDS_SWIFT),
line_comments: vec!["//"],
block_comment: Some(NESTING),
quotes: vec![TRIPLE_DOUBLE, DOUBLE],
attributes: Attributes::AtWord,
..Default::default()
},
Language::Toml => Rules {
keywords: words(KEYWORDS_TOML),
line_comments: vec!["#"],
quotes: vec![
TRIPLE_DOUBLE,
Quote {
open: "'''",
close: "'''",
escapes: false,
},
DOUBLE,
Quote {
open: "'",
close: "'",
escapes: false,
},
],
attributes: Attributes::LineBracket,
..Default::default()
},
Language::Typescript => Rules {
keywords: words(KEYWORDS_TYPESCRIPT),
line_comments: vec!["//"],
block_comment: Some(C_STYLE),
quotes: vec![
DOUBLE,
SINGLE,
Quote {
open: "`",
close: "`",
escapes: true,
},
],
attributes: Attributes::AtWord,
..Default::default()
},
// Markdown has no token rules; see `super::markdown::scan_markdown`.
Language::Markdown => Rules::default(),
}
}
// The keyword sets. Every list below other than RON, TOML, fish and JSON
// came from dev.snipme:highlights 1.1.0 (Apache-2.0), the library the
// Kotlin scanner replaced, so that no fence which was coloured there turns
// plain here either.
const KEYWORDS_C: &str =
"auto break case char const continue default do double else enum extern float for goto if
int long register return short signed sizeof static struct switch typedef union unsigned
void volatile while";
const KEYWORDS_CPP: &str =
"asm auto bool break case catch char class const const_cast continue default delete do
double dynamic_cast else enum explicit export extern false float for friend goto if inline
int long mutable namespace new operator private protected public register reinterpret_cast
return short signed sizeof static static_cast struct switch template this throw true try
typedef typeid typename union unsigned using virtual void volatile wchar_t while";
const KEYWORDS_CSHARP: &str =
"abstract as base bool break byte case catch char checked class const continue decimal
default delegate do double else enum event explicit extern false finally fixed float for
foreach goto if implicit in int interface internal is lock long namespace new null object
operator out override params private protected public readonly ref return sbyte sealed short
sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked
unsafe ushort using virtual void volatile while";
const KEYWORDS_COFFEESCRIPT: &str =
"Infinity NaN and arguments await break by case catch class continue debugger delete defer
default do else export extends false finally for function if import in instanceof is isnt
let loop new no not null of on or package return super switch this throw true try typeof
unless undefined var wait when with yield";
const KEYWORDS_DART: &str =
"abstract as assert async await base break case catch class const continue covariant
default deferred do dynamic else enum export extends external factory false final finally
for get if implements import in interface is late library mixin new null on operator part
required rethrow return sealed set show static super switch this throw true try var void
when with while yield";
/// fish is not in the library at all, so its fences are drawn plain today.
/// The list is the shell's own words, which is what a fish fence is mostly
/// made of.
const KEYWORDS_FISH: &str =
"and begin break builtin case command continue else end exec for function if in not or
return switch while set echo test string math read source";
const KEYWORDS_GO: &str =
"break case chan const continue default defer else fallthrough false for func go goto if
import interface map package range return select struct switch true type var";
const KEYWORDS_JAVA: &str =
"abstract assert boolean break byte case catch char class const continue default do double
else enum extends final finally float for goto if implements import instanceof int interface
long native new null package private protected public return short static strictfp super
switch synchronized this throw throws transient try void volatile while";
const KEYWORDS_JAVASCRIPT: &str =
"async await boolean break case catch class const continue debugger default delete do else
enum export extends false finally for function if implements import in instanceof interface
let new null package private protected public return super switch this throw true try typeof
var void while with yield";
const KEYWORDS_JSON: &str = "true false null";
const KEYWORDS_KOTLIN: &str =
"actual abstract annotation as break by catch class companion const constructor continue
coroutine crossinline data delegate dynamic do else enum expect external false final finally
for fun get if import in infix inline interface internal is lazy lateinit native null object
open operator out override package private protected public reified return sealed set super
suspend tailrec this throw true try typealias typeof val var vararg when while yield";
const KEYWORDS_PERL: &str =
"__DATA__ __END__ __FILE__ __LINE__ __PACKAGE__ and cmp continue do else elsif eq eval for
foreach goto gt if last le lt my ne next no not or package redo ref return sub unless until
use while xor";
const KEYWORDS_PHP: &str =
"__halt_compiler abstract and array as break callable case catch class clone const continue
declare default die do echo else elseif empty enddeclare endfor endforeach endif endswitch
endwhile eval exit extends final finally fn for foreach function global goto if implements
include include_once instanceof insteadof interface isset list match new or print private
protected public require require_once return static switch throw trait try unset use var
while xor yield";
const KEYWORDS_PYTHON: &str =
"False True and as assert async await break class continue def del elif else except finally
for from global if import in is lambda nonlocal not or pass raise return try while with
yield";
/// RON is not in the library either; these are the words a RON file can hold.
const KEYWORDS_RON: &str = "true false Some None inf NaN";
const KEYWORDS_RUBY: &str =
"__ENCODING__ __END__ __FILE__ __LINE__ BEGIN END alias and begin break case class def do
else elsif end ensure false for if in module next nil not or redo rescue retry return self
super then true undef unless until when while yield";
const KEYWORDS_RUST: &str =
"as async await break const continue crate dyn else enum extern false fn for if impl in
let loop match mod move mut pub ref return Self self static struct super trait true type
union unsafe use where while abstract become box do final macro override priv try typeof
unsized virtual yield";
const KEYWORDS_SHELL: &str =
"alias bg bind break builtin caller cd command compgen complete compopt continue declare
dirs disown echo enable eval exec exit export fc fg getopts hash help history jobs kill let
local logout popd printf pushd pwd read readonly return set shift shopt source suspend
test";
const KEYWORDS_SWIFT: &str =
"_ associatedtype class deinit enum extension fileprivate func import init inout internal
let open operator private precedencegroup protocol public rethrows static struct subscript
typealias var break case catch continue default defer do else fallthrough for guard if in
repeat return throw switch where while Any as await false is nil self Self super throws true
try associativity convenience didSet dynamic final get indirect infix lazy left mutating none
nonmutating optional override postfix precedence prefix Protocol required right set some Type
unowned weak willSet";
/// TOML is not in the library; `inf` and `nan` are values rather than
/// names, like the booleans.
const KEYWORDS_TOML: &str = "true false inf nan";
const KEYWORDS_TYPESCRIPT: &str =
"abstract as asserts await break case catch class const constructor continue debugger
default delete do else enum export extends false finally for from function get if implements
import in infer instanceof interface is keyof let module namespace new null number object
package private protected public readonly require global return set static string super
switch this throw true try type typeof undefined unique unknown var void while with yield";
/// The highlighter's language for a fence's info word, or `None` for one it
/// has no rules for. Also what `super::file_language` reads for a file's
/// extension -- one table, so a language added for fences is a language
/// added for files.
pub fn fence_language(name: Option<&str>) -> Option<Language> {
let name = name?.trim().to_lowercase();
FENCE_LANGUAGES
.iter()
.find(|(alias, _)| *alias == name)
.map(|(_, language)| *language)
}
/// The highlighter's language for a *file*, from its name.
///
/// The extension is the part after the *last* dot, which is what makes
/// `build.gradle.kts` Kotlin. A leading dot is not one: `.bashrc` has no
/// extension, it has a name that starts with a dot. A name with no dot at
/// all -- `Makefile` -- is likewise `None`.
pub fn file_language(name: &str) -> Option<Language> {
let dot = name.rfind('.')?;
if dot < 1 {
return None;
}
fence_language(Some(&name[dot + 1..]))
}
const FENCE_LANGUAGES: &[(&str, Language)] = &[
("kotlin", Language::Kotlin),
("kt", Language::Kotlin),
("kts", Language::Kotlin),
("rust", Language::Rust),
("rs", Language::Rust),
("sh", Language::Shell),
("bash", Language::Shell),
("shell", Language::Shell),
("zsh", Language::Shell),
("console", Language::Shell),
("python", Language::Python),
("py", Language::Python),
("javascript", Language::Javascript),
("js", Language::Javascript),
("jsx", Language::Javascript),
("typescript", Language::Typescript),
("ts", Language::Typescript),
("tsx", Language::Typescript),
("java", Language::Java),
("c", Language::C),
("h", Language::C),
("cpp", Language::Cpp),
("c++", Language::Cpp),
("cc", Language::Cpp),
("hpp", Language::Cpp),
("csharp", Language::Csharp),
("cs", Language::Csharp),
("c#", Language::Csharp),
("go", Language::Go),
("golang", Language::Go),
("swift", Language::Swift),
("dart", Language::Dart),
("ruby", Language::Ruby),
("rb", Language::Ruby),
("php", Language::Php),
("perl", Language::Perl),
("pl", Language::Perl),
("coffeescript", Language::Coffeescript),
("coffee", Language::Coffeescript),
("ron", Language::Ron),
("toml", Language::Toml),
("fish", Language::Fish),
("json", Language::Json),
("markdown", Language::Markdown),
("md", Language::Markdown),
];