diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 3174ce5ecd..81eb76bfd9 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -702,6 +702,7 @@ "bitflags_1.3.2": "{\"dependencies\":[{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.2\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3\"}],\"features\":{\"default\":[],\"example_generated\":[],\"rustc-dep-of-std\":[\"core\",\"compiler_builtins\"]}}", "bitflags_2.10.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"arbitrary\",\"req\":\"^1.0\"},{\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.12\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.12.2\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.228\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde_lib\",\"package\":\"serde\",\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.19\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.18\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"zerocopy\",\"req\":\"^0.8\"}],\"features\":{\"example_generated\":[],\"serde\":[\"serde_core\"],\"std\":[]}}", "bitflags_2.11.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"arbitrary\",\"req\":\"^1.0\"},{\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.12\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.12.2\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.228\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde_lib\",\"package\":\"serde\",\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.19\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.18\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"zerocopy\",\"req\":\"^0.8\"}],\"features\":{\"example_generated\":[],\"serde\":[\"serde_core\"],\"std\":[]}}", + "bitvec_1.0.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"funty\",\"req\":\"^2.0\"},{\"name\":\"radium\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"name\":\"tap\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"wyz\",\"req\":\"^0.5\"}],\"features\":{\"alloc\":[],\"atomic\":[],\"default\":[\"atomic\",\"std\"],\"std\":[\"alloc\"],\"testing\":[]}}", "blake2_0.10.6": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.10.3\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"}],\"features\":{\"default\":[\"std\"],\"reset\":[],\"simd\":[],\"simd_asm\":[\"simd_opt\"],\"simd_opt\":[\"simd\"],\"size_opt\":[],\"std\":[\"digest/std\"]}}", "block-buffer_0.10.4": "{\"dependencies\":[{\"name\":\"generic-array\",\"req\":\"^0.14\"}],\"features\":{}}", "block-padding_0.3.3": "{\"dependencies\":[{\"name\":\"generic-array\",\"req\":\"^0.14\"}],\"features\":{\"std\":[]}}", @@ -711,7 +712,9 @@ "borsh_1.6.0": "{\"dependencies\":[{\"name\":\"ascii\",\"optional\":true,\"req\":\"^1.1\"},{\"name\":\"borsh-derive\",\"optional\":true,\"req\":\"~1.6.0\"},{\"name\":\"bson\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2.1\"},{\"name\":\"hashbrown\",\"optional\":true,\"req\":\">=0.11, <0.16.0\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.29.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"}],\"features\":{\"de_strict_order\":[],\"default\":[\"std\"],\"derive\":[\"borsh-derive\"],\"rc\":[],\"std\":[],\"unstable__schema\":[\"derive\",\"borsh-derive/schema\"]}}", "bstr_1.12.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.7.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"dfa-search\"],\"name\":\"regex-automata\",\"optional\":true,\"req\":\"^0.4.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.85\"},{\"kind\":\"dev\",\"name\":\"ucd-parse\",\"req\":\"^0.1.3\"},{\"kind\":\"dev\",\"name\":\"unicode-segmentation\",\"req\":\"^1.2.1\"}],\"features\":{\"alloc\":[\"memchr/alloc\",\"serde?/alloc\"],\"default\":[\"std\",\"unicode\"],\"serde\":[\"dep:serde\"],\"std\":[\"alloc\",\"memchr/std\",\"serde?/std\"],\"unicode\":[\"dep:regex-automata\"]}}", "bumpalo_3.19.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.8\"},{\"kind\":\"dev\",\"name\":\"blink-alloc\",\"req\":\"=0.4.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"=1.10.0\"},{\"kind\":\"dev\",\"name\":\"rayon-core\",\"req\":\"=1.12.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.171\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.197\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.115\"}],\"features\":{\"allocator_api\":[],\"bench_allocator_api\":[\"allocator_api\",\"blink-alloc/nightly\"],\"boxed\":[],\"collections\":[],\"default\":[],\"serde\":[\"dep:serde\"],\"std\":[]}}", + "by_address_1.2.1": "{\"dependencies\":[],\"features\":{}}", "bytemuck_1.25.0": "{\"dependencies\":[{\"name\":\"bytemuck_derive\",\"optional\":true,\"req\":\"^1.10.2\"},{\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.22\"}],\"features\":{\"aarch64_simd\":[],\"align_offset\":[],\"alloc_uninit\":[],\"avx512_simd\":[],\"const_zeroed\":[],\"derive\":[\"bytemuck_derive\"],\"extern_crate_alloc\":[],\"extern_crate_std\":[\"extern_crate_alloc\"],\"impl_core_error\":[],\"latest_stable_rust\":[\"aarch64_simd\",\"avx512_simd\",\"align_offset\",\"alloc_uninit\",\"const_zeroed\",\"derive\",\"impl_core_error\",\"min_const_generics\",\"must_cast\",\"must_cast_extra\",\"pod_saturating\",\"track_caller\",\"transparentwrapper_extra\",\"wasm_simd\",\"zeroable_atomics\",\"zeroable_maybe_uninit\",\"zeroable_unwind_fn\"],\"min_const_generics\":[],\"must_cast\":[],\"must_cast_extra\":[\"must_cast\"],\"nightly_docs\":[],\"nightly_float\":[],\"nightly_portable_simd\":[\"rustversion\"],\"nightly_stdsimd\":[],\"pod_saturating\":[],\"track_caller\":[],\"transparentwrapper_extra\":[],\"unsound_ptr_pod_impl\":[],\"wasm_simd\":[],\"zeroable_atomics\":[],\"zeroable_maybe_uninit\":[],\"zeroable_unwind_fn\":[]}}", + "bytemuck_derive_1.10.2": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"syn\",\"req\":\"^2.0.1\"}],\"features\":{}}", "byteorder-lite_0.1.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "byteorder_1.5.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"std\"],\"i128\":[],\"std\":[]}}", "bytes-utils_0.1.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.144\"}],\"features\":{\"default\":[\"std\"],\"serde\":[\"dep:serde\",\"bytes/serde\"],\"std\":[\"bytes/default\"]}}", @@ -902,6 +905,7 @@ "event-listener_5.4.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"concurrent-queue\",\"req\":\"^2.4.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"critical-section\",\"optional\":true,\"req\":\"^1.2.0\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"critical-section\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"name\":\"parking\",\"optional\":true,\"req\":\"^2.0.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.12\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"portable-atomic-util\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"portable_atomic_crate\",\"optional\":true,\"package\":\"portable-atomic\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"try-lock\",\"req\":\"^0.2.5\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"default\":[\"std\"],\"loom\":[\"concurrent-queue/loom\",\"parking?/loom\",\"dep:loom\"],\"portable-atomic\":[\"portable-atomic-util\",\"portable_atomic_crate\",\"concurrent-queue/portable-atomic\"],\"std\":[\"concurrent-queue/std\",\"parking\"]}}", "eventsource-stream_0.2.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"nom\",\"req\":\"^7.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.8\"},{\"features\":[\"stream\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.11\"},{\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.2\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"nom/std\"]}}", "eyre_0.6.12": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.28\"},{\"kind\":\"dev\",\"name\":\"backtrace\",\"req\":\"^0.3.46\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"indenter\",\"req\":\"^0.3.0\"},{\"name\":\"once_cell\",\"req\":\"^1.18.0\"},{\"default_features\":false,\"name\":\"pyo3\",\"optional\":true,\"req\":\"^0.20\"},{\"default_features\":false,\"features\":[\"auto-initialize\"],\"kind\":\"dev\",\"name\":\"pyo3\",\"req\":\"^0.20\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.19\"}],\"features\":{\"auto-install\":[],\"default\":[\"auto-install\",\"track-caller\"],\"track-caller\":[]}}", + "fast-srgb8_1.0.0": "{\"dependencies\":[],\"features\":{}}", "faster-hex_0.10.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1.4.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"heapless\",\"req\":\"^0.8\",\"target\":\"cfg(not(feature = \\\"alloc\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.3.2\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustc-hex\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\",\"serde\"],\"serde\":[\"dep:serde\",\"alloc\"],\"std\":[\"alloc\",\"serde?/std\"]}}", "fastrand_2.3.0": "{\"dependencies\":[{\"features\":[\"js\"],\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"wyhash\",\"req\":\"^0.5\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"js\":[\"std\",\"getrandom\"],\"std\":[\"alloc\"]}}", "fax_0.2.6": "{\"dependencies\":[{\"name\":\"fax_derive\",\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"tiff\",\"req\":\"^0.9\"}],\"features\":{\"debug\":[]}}", @@ -936,6 +940,7 @@ "fs_extra_1.3.0": "{\"dependencies\":[],\"features\":{}}", "fsevent-sys_4.1.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.68\"}],\"features\":{}}", "fslock_0.2.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.66\",\"target\":\"cfg(unix)\"},{\"features\":[\"minwindef\",\"minwinbase\",\"winbase\",\"errhandlingapi\",\"winerror\",\"winnt\",\"synchapi\",\"handleapi\",\"fileapi\",\"processthreadsapi\"],\"name\":\"winapi\",\"req\":\"^0.3.8\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", + "funty_2.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "futures-channel_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.31\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", "futures-channel_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.32\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", "futures-core_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", @@ -1093,6 +1098,7 @@ "icu_properties_2.1.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"icu_collections\",\"req\":\"~2.1.1\"},{\"default_features\":false,\"features\":[\"zerovec\"],\"name\":\"icu_locale_core\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"icu_properties_data\",\"optional\":true,\"req\":\"~2.1.2\"},{\"default_features\":false,\"name\":\"icu_provider\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"name\":\"unicode-bidi\",\"optional\":true,\"req\":\"^0.3.11\"},{\"default_features\":false,\"features\":[\"yoke\",\"zerofrom\"],\"name\":\"zerotrie\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"derive\",\"yoke\"],\"name\":\"zerovec\",\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"zerovec/alloc\",\"icu_collections/alloc\",\"serde?/alloc\"],\"compiled_data\":[\"dep:icu_properties_data\",\"icu_provider/baked\"],\"datagen\":[\"serde\",\"dep:databake\",\"zerovec/databake\",\"icu_collections/databake\",\"icu_locale_core/databake\",\"zerotrie/databake\",\"icu_provider/export\"],\"default\":[\"compiled_data\"],\"serde\":[\"dep:serde\",\"icu_locale_core/serde\",\"zerovec/serde\",\"icu_collections/serde\",\"icu_provider/serde\",\"zerotrie/serde\"],\"unicode_bidi\":[\"dep:unicode-bidi\"]}}", "icu_properties_data_2.1.2": "{\"dependencies\":[],\"features\":{}}", "icu_provider_2.1.1": "{\"dependencies\":[{\"name\":\"bincode\",\"optional\":true,\"req\":\"^1.3.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"name\":\"erased-serde\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"icu_locale_core\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"default_features\":false,\"name\":\"postcard\",\"optional\":true,\"req\":\"^1.0.3\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.45\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"name\":\"stable_deref_trait\",\"optional\":true,\"req\":\"^1.2.0\"},{\"default_features\":false,\"name\":\"writeable\",\"optional\":true,\"req\":\"^0.6.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"yoke\",\"req\":\"^0.8.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"zerofrom\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"zerotrie\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"zerovec\",\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"icu_locale_core/alloc\",\"serde?/alloc\",\"yoke/alloc\",\"zerofrom/alloc\",\"zerovec/alloc\",\"zerotrie?/alloc\",\"dep:stable_deref_trait\",\"dep:writeable\"],\"baked\":[\"dep:zerotrie\",\"dep:writeable\"],\"deserialize_bincode_1\":[\"serde\",\"dep:bincode\",\"std\"],\"deserialize_json\":[\"serde\",\"dep:serde_json\"],\"deserialize_postcard_1\":[\"serde\",\"dep:postcard\"],\"export\":[\"serde\",\"dep:erased-serde\",\"dep:databake\",\"std\",\"sync\",\"dep:postcard\",\"zerovec/databake\"],\"logging\":[\"dep:log\"],\"serde\":[\"dep:serde\",\"yoke/serde\"],\"std\":[\"alloc\"],\"sync\":[],\"zerotrie\":[]}}", + "icy_sixel_0.5.0": "{\"dependencies\":[{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8.1\"},{\"default_features\":false,\"features\":[\"png\"],\"kind\":\"dev\",\"name\":\"image\",\"req\":\"^0.25.9\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4.1\"},{\"name\":\"quantette\",\"req\":\"^0.5.1\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"}],\"features\":{}}", "id-arena_2.3.0": "{\"dependencies\":[{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.0.3\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "ident_case_1.0.1": "{\"dependencies\":[],\"features\":{}}", "idna_1.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1\"},{\"name\":\"idna_adapter\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"const_generics\"],\"name\":\"smallvec\",\"req\":\"^1.13.1\"},{\"kind\":\"dev\",\"name\":\"tester\",\"req\":\"^0.9\"},{\"name\":\"utf8_iter\",\"req\":\"^1.0.4\"}],\"features\":{\"alloc\":[],\"compiled_data\":[\"idna_adapter/compiled_data\"],\"default\":[\"std\",\"compiled_data\"],\"std\":[\"alloc\"]}}", @@ -1278,12 +1284,15 @@ "opentelemetry_0.31.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"js-sys\",\"req\":\"^0.3.63\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\")))\"},{\"name\":\"pin-project-lite\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"os_rng\",\"thread_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"thiserror\",\"optional\":true,\"req\":\"^2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\">=0.1.40\"}],\"features\":{\"default\":[\"trace\",\"metrics\",\"logs\",\"internal-logs\",\"futures\"],\"futures\":[\"futures-core\",\"futures-sink\",\"pin-project-lite\"],\"internal-logs\":[\"tracing\"],\"logs\":[],\"metrics\":[],\"spec_unstable_logs_enabled\":[\"logs\"],\"testing\":[\"trace\"],\"trace\":[\"futures\",\"thiserror\"]}}", "opentelemetry_sdk_0.31.0": "{\"dependencies\":[{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"futures-channel\",\"req\":\"^0.3\"},{\"name\":\"futures-executor\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\",\"sink\",\"async-await-macro\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"http\",\"optional\":true,\"req\":\"^1.1\"},{\"default_features\":false,\"name\":\"opentelemetry\",\"req\":\"^0.31\"},{\"default_features\":false,\"name\":\"opentelemetry-http\",\"optional\":true,\"req\":\"^0.31\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.0\"},{\"features\":[\"flamegraph\",\"criterion\"],\"kind\":\"dev\",\"name\":\"pprof\",\"req\":\"^0.14\",\"target\":\"cfg(not(target_os = \\\"windows\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"std_rng\",\"small_rng\",\"os_rng\",\"thread_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.23.0\"},{\"default_features\":false,\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"temp-env\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"url\",\"optional\":true,\"req\":\"^2.5\"}],\"features\":{\"default\":[\"trace\",\"metrics\",\"logs\",\"internal-logs\"],\"experimental_async_runtime\":[],\"experimental_logs_batch_log_processor_with_async_runtime\":[\"logs\",\"experimental_async_runtime\"],\"experimental_logs_concurrent_log_processor\":[\"logs\"],\"experimental_metrics_custom_reader\":[\"metrics\"],\"experimental_metrics_disable_name_validation\":[\"metrics\"],\"experimental_metrics_periodicreader_with_async_runtime\":[\"metrics\",\"experimental_async_runtime\"],\"experimental_trace_batch_span_processor_with_async_runtime\":[\"tokio/sync\",\"trace\",\"experimental_async_runtime\"],\"internal-logs\":[\"opentelemetry/internal-logs\"],\"jaeger_remote_sampler\":[\"trace\",\"opentelemetry-http\",\"http\",\"serde\",\"serde_json\",\"url\",\"experimental_async_runtime\"],\"logs\":[\"opentelemetry/logs\"],\"metrics\":[\"opentelemetry/metrics\"],\"rt-tokio\":[\"tokio/rt\",\"tokio/time\",\"tokio-stream\",\"experimental_async_runtime\"],\"rt-tokio-current-thread\":[\"tokio/rt\",\"tokio/time\",\"tokio-stream\",\"experimental_async_runtime\"],\"spec_unstable_logs_enabled\":[\"logs\",\"opentelemetry/spec_unstable_logs_enabled\"],\"spec_unstable_metrics_views\":[\"metrics\"],\"testing\":[\"opentelemetry/testing\",\"trace\",\"metrics\",\"logs\",\"rt-tokio\",\"rt-tokio-current-thread\",\"tokio/macros\",\"tokio/rt-multi-thread\"],\"trace\":[\"opentelemetry/trace\",\"rand\",\"percent-encoding\"]}}", "option-ext_0.2.0": "{\"dependencies\":[],\"features\":{}}", + "ordered-float_5.3.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"default_features\":false,\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.12.2\"},{\"name\":\"derive-visitor\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"num-cmp\",\"optional\":true,\"req\":\"^0.1.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.9\"},{\"name\":\"proptest\",\"optional\":true,\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8.3\"},{\"default_features\":false,\"features\":[\"rend\"],\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.7.41\"},{\"default_features\":false,\"name\":\"rkyv_08\",\"optional\":true,\"package\":\"rkyv\",\"req\":\"^0.8\"},{\"name\":\"schemars\",\"optional\":true,\"req\":\"^0.8.8\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"speedy\",\"optional\":true,\"req\":\"^0.8.3\"}],\"features\":{\"default\":[\"std\"],\"libm\":[\"num-traits/libm\"],\"randtest\":[\"rand/std\",\"rand/std_rng\"],\"rkyv\":[\"rkyv_32\"],\"rkyv_08\":[\"rkyv_08_32\"],\"rkyv_08_16\":[\"dep:rkyv_08\",\"rkyv_08?/pointer_width_16\"],\"rkyv_08_32\":[\"dep:rkyv_08\",\"rkyv_08?/pointer_width_32\"],\"rkyv_08_64\":[\"dep:rkyv_08\",\"rkyv_08?/pointer_width_64\"],\"rkyv_08_ck\":[\"dep:rkyv_08\",\"rkyv_08?/bytecheck\"],\"rkyv_16\":[\"dep:rkyv\",\"rkyv?/size_16\"],\"rkyv_32\":[\"dep:rkyv\",\"rkyv?/size_32\"],\"rkyv_64\":[\"dep:rkyv\",\"rkyv?/size_64\"],\"rkyv_ck\":[\"rkyv?/validation\"],\"serde\":[\"dep:serde\",\"rand?/serde1\"],\"std\":[\"num-traits/std\"]}}", "ordered-stream_0.2.0": "{\"dependencies\":[{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"futures-executor\",\"req\":\"^0.3.25\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.25\"}],\"features\":{}}", "os_info_3.14.0": "{\"dependencies\":[{\"name\":\"android_system_properties\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"features\":[\"feature\"],\"name\":\"nix\",\"req\":\"^0.30\",\"target\":\"cfg(any(target_os = \\\"aix\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"illumos\\\", target_os = \\\"linux\\\", target_os = \\\"macos\\\", target_os = \\\"netbsd\\\", target_os = \\\"openbsd\\\", target_os = \\\"cygwin\\\"))\"},{\"name\":\"objc2\",\"req\":\"^0.6\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"features\":[\"NSString\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"features\":[\"NSData\",\"NSError\",\"NSEnumerator\",\"NSString\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"objc2-ui-kit\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1\"},{\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0.3\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_LibraryLoader\",\"Win32_System_Registry\",\"Win32_System_SystemInformation\",\"Win32_System_SystemServices\",\"Win32_System_Threading\",\"Win32_UI_WindowsAndMessaging\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"serde\"]}}", "os_pipe_1.2.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.62\",\"target\":\"cfg(not(windows))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Pipes\",\"Win32_Security\"],\"name\":\"windows-sys\",\"req\":\">=0.28, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"io_safety\":[]}}", "outref_0.5.2": "{\"dependencies\":[],\"features\":{}}", "owo-colors_4.3.0": "{\"dependencies\":[{\"name\":\"supports-color\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"supports-color-2\",\"optional\":true,\"package\":\"supports-color\",\"req\":\"^2.0\"}],\"features\":{\"alloc\":[],\"supports-colors\":[\"dep:supports-color-2\",\"supports-color\"]}}", "p256_0.13.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"blobby\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"der\"],\"name\":\"ecdsa-core\",\"optional\":true,\"package\":\"ecdsa\",\"req\":\"^0.16\"},{\"default_features\":false,\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"ecdsa-core\",\"package\":\"ecdsa\",\"req\":\"^0.16\"},{\"default_features\":false,\"features\":[\"hazmat\",\"sec1\"],\"name\":\"elliptic-curve\",\"req\":\"^0.13.1\"},{\"name\":\"hex-literal\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"primeorder\",\"optional\":true,\"req\":\"^0.13\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"primeorder\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"}],\"features\":{\"alloc\":[\"ecdsa-core?/alloc\",\"elliptic-curve/alloc\"],\"arithmetic\":[\"dep:primeorder\",\"elliptic-curve/arithmetic\"],\"bits\":[\"arithmetic\",\"elliptic-curve/bits\"],\"default\":[\"arithmetic\",\"ecdsa\",\"pem\",\"std\"],\"digest\":[\"ecdsa-core/digest\",\"ecdsa-core/hazmat\"],\"ecdh\":[\"arithmetic\",\"elliptic-curve/ecdh\"],\"ecdsa\":[\"arithmetic\",\"ecdsa-core/signing\",\"ecdsa-core/verifying\",\"sha256\"],\"expose-field\":[\"arithmetic\"],\"hash2curve\":[\"arithmetic\",\"elliptic-curve/hash2curve\"],\"jwk\":[\"elliptic-curve/jwk\"],\"pem\":[\"elliptic-curve/pem\",\"ecdsa-core/pem\",\"pkcs8\"],\"pkcs8\":[\"ecdsa-core?/pkcs8\",\"elliptic-curve/pkcs8\"],\"serde\":[\"ecdsa-core?/serde\",\"elliptic-curve/serde\",\"primeorder?/serde\",\"serdect\"],\"sha256\":[\"digest\",\"sha2\"],\"std\":[\"alloc\",\"ecdsa-core?/std\",\"elliptic-curve/std\"],\"test-vectors\":[\"dep:hex-literal\"],\"voprf\":[\"elliptic-curve/voprf\",\"sha2\"]}}", + "palette_0.7.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"approx\",\"optional\":true,\"req\":\"^0.5\"},{\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"enterpolation\",\"req\":\"^0.2.0\"},{\"name\":\"fast-srgb8\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"png\"],\"kind\":\"dev\",\"name\":\"image\",\"req\":\"^0.23.14\"},{\"default_features\":false,\"name\":\"libm\",\"optional\":true,\"req\":\"^0.2.1\"},{\"name\":\"palette_derive\",\"req\":\"^0.7.6\"},{\"default_features\":false,\"features\":[\"macros\"],\"name\":\"phf\",\"optional\":true,\"req\":\"^0.11.0\"},{\"default_features\":false,\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"features\":[\"rand-traits\"],\"kind\":\"dev\",\"name\":\"rand_mt\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"ron\",\"req\":\"=0.8.0\"},{\"features\":[\"serde_derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"wide\",\"optional\":true,\"req\":\"^0.7.3\"}],\"features\":{\"alloc\":[],\"default\":[\"named_from_str\",\"std\",\"approx\"],\"find-crate\":[\"palette_derive/find-crate\"],\"named\":[],\"named_from_str\":[\"named\",\"phf\"],\"random\":[\"rand\"],\"serializing\":[\"serde\",\"std\"],\"std\":[\"alloc\",\"approx?/std\"]}}", + "palette_derive_0.7.6": "{\"dependencies\":[{\"name\":\"by_address\",\"req\":\"^1.2.1\"},{\"name\":\"find-crate\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"derive\",\"parsing\",\"printing\",\"clone-impls\",\"extra-traits\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0.13\"}],\"features\":{}}", "parking_2.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"}],\"features\":{}}", "parking_lot_0.12.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.3\"},{\"name\":\"lock_api\",\"req\":\"^0.4.14\"},{\"name\":\"parking_lot_core\",\"req\":\"^0.9.12\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"}],\"features\":{\"arc_lock\":[\"lock_api/arc_lock\"],\"deadlock_detection\":[\"parking_lot_core/deadlock_detection\"],\"default\":[],\"hardware-lock-elision\":[],\"nightly\":[\"parking_lot_core/nightly\",\"lock_api/nightly\"],\"owning_ref\":[\"lock_api/owning_ref\"],\"send_guard\":[],\"serde\":[\"lock_api/serde\"]}}", "parking_lot_core_0.9.12": "{\"dependencies\":[{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.60\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.95\",\"target\":\"cfg(unix)\"},{\"name\":\"petgraph\",\"optional\":true,\"req\":\"^0.6.0\"},{\"name\":\"redox_syscall\",\"req\":\"^0.5\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"name\":\"smallvec\",\"req\":\"^1.6.1\"},{\"name\":\"windows-link\",\"req\":\"^0.2.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"deadlock_detection\":[\"petgraph\",\"backtrace\"],\"nightly\":[]}}", @@ -1361,6 +1370,7 @@ "pulldown-cmark-escape_0.10.1": "{\"dependencies\":[],\"features\":{\"simd\":[]}}", "pulldown-cmark_0.10.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"getopts\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"memchr\",\"req\":\"^2.5\"},{\"name\":\"pulldown-cmark-escape\",\"optional\":true,\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.6\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.61\"},{\"name\":\"unicase\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"getopts\",\"html\"],\"gen-tests\":[],\"html\":[\"pulldown-cmark-escape\"],\"simd\":[\"pulldown-cmark-escape?/simd\"]}}", "pxfm_0.1.27": "{\"dependencies\":[{\"name\":\"num-traits\",\"req\":\"^0.2.3\"}],\"features\":{}}", + "quantette_0.5.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"bitvec\",\"req\":\"^1.0.1\"},{\"features\":[\"derive\",\"min_const_generics\",\"extern_crate_alloc\"],\"name\":\"bytemuck\",\"req\":\"^1.24.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.3.0\"},{\"kind\":\"dev\",\"name\":\"color_quant\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"dssim\",\"req\":\"^3.2.4\"},{\"kind\":\"dev\",\"name\":\"exoquant\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"image\",\"optional\":true,\"req\":\"^0.25.0\"},{\"default_features\":false,\"features\":[\"jpeg\",\"png\"],\"kind\":\"dev\",\"name\":\"image\",\"req\":\"^0.25.0\"},{\"kind\":\"dev\",\"name\":\"imagequant\",\"req\":\"^4.2.0\"},{\"default_features\":false,\"name\":\"libm\",\"req\":\"^0.2.11\"},{\"default_features\":false,\"features\":[\"libm\"],\"name\":\"num-traits\",\"req\":\"^0.2.16\"},{\"default_features\":false,\"name\":\"ordered-float\",\"req\":\"^5.1.0\"},{\"default_features\":false,\"features\":[\"alloc\",\"libm\",\"bytemuck\"],\"name\":\"palette\",\"req\":\"^0.7.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9.2\"},{\"name\":\"rand_xoshiro\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.10.0\"},{\"name\":\"ref-cast\",\"req\":\"^1.0.23\"},{\"features\":[\"as-bytes\"],\"kind\":\"dev\",\"name\":\"rgb\",\"req\":\"^0.8.36\"},{\"default_features\":false,\"name\":\"tap\",\"optional\":true,\"req\":\"^1.0.1\",\"target\":\"cfg(any())\"},{\"default_features\":false,\"name\":\"wide\",\"req\":\"^0.8.1\"}],\"features\":{\"default\":[\"kmeans\",\"threads\",\"image\"],\"image\":[\"dep:image\",\"std\"],\"kmeans\":[\"rand\",\"rand_xoshiro\"],\"std\":[\"wide/std\",\"num-traits/std\"],\"threads\":[\"rayon\",\"std\"]}}", "quick-error_2.0.1": "{\"dependencies\":[],\"features\":{}}", "quick-xml_0.38.4": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\">=0.4, <0.8\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\">=1.0.139\"},{\"kind\":\"dev\",\"name\":\"serde-value\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.206\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"async-tokio\":[\"tokio\"],\"default\":[],\"encoding\":[\"encoding_rs\"],\"escape-html\":[],\"overlapped-lists\":[],\"serde-types\":[\"serde/derive\"],\"serialize\":[\"serde\"]}}", "quinn-proto_0.11.14": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.1\"},{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.9\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"fastbloom\",\"optional\":true,\"req\":\"^0.14\"},{\"default_features\":false,\"features\":[\"wasm_js\"],\"name\":\"getrandom\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"lru-slab\",\"req\":\"^0.1.2\"},{\"name\":\"qlog\",\"optional\":true,\"req\":\"^0.15.2\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"features\":[\"wasm32_unknown_unknown_js\"],\"name\":\"ring\",\"req\":\"^0.17\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.5\"},{\"features\":[\"web\"],\"name\":\"rustls-pki-types\",\"req\":\"^1.7\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"slab\",\"req\":\"^0.4.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"features\":[\"alloc\",\"alloc\"],\"name\":\"tinyvec\",\"req\":\"^1.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.10\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"ansi\",\"time\",\"local-time\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.45\"},{\"name\":\"web-time\",\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"}],\"features\":{\"__rustls-post-quantum-test\":[],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs?/aws-lc-sys\",\"aws-lc-rs?/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"aws-lc-rs\",\"aws-lc-rs?/fips\"],\"bloom\":[\"dep:fastbloom\"],\"default\":[\"rustls-ring\",\"log\",\"bloom\"],\"log\":[\"tracing/log\"],\"platform-verifier\":[\"dep:rustls-platform-verifier\"],\"qlog\":[\"dep:qlog\"],\"ring\":[\"dep:ring\"],\"rustls\":[\"rustls-ring\"],\"rustls-aws-lc-rs\":[\"dep:rustls\",\"rustls?/aws-lc-rs\",\"aws-lc-rs\"],\"rustls-aws-lc-rs-fips\":[\"rustls-aws-lc-rs\",\"aws-lc-rs-fips\"],\"rustls-log\":[\"rustls?/logging\"],\"rustls-ring\":[\"dep:rustls\",\"rustls?/ring\",\"ring\"]}}", @@ -1370,6 +1380,7 @@ "quote_1.0.45": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.80\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"default\":[\"proc-macro\"],\"proc-macro\":[\"proc-macro2/proc-macro\"]}}", "r-efi_5.3.0": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"efiapi\":[],\"examples\":[\"native\"],\"native\":[],\"rustc-dep-of-std\":[\"core\"]}}", "r-efi_6.0.0": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"native\":[],\"rustc-dep-of-std\":[\"core\"]}}", + "radium_0.7.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"}],\"features\":{}}", "radix_trie_0.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"endian-type\",\"req\":\"^0.1.2\"},{\"name\":\"nibble_vec\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.3\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{}}", "radix_trie_0.3.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"endian-type\",\"req\":\"^0.2.0\"},{\"name\":\"nibble_vec\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{}}", "rama-core_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"asynk-strim\",\"req\":\"^0.1\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"trace\"],\"name\":\"opentelemetry\",\"optional\":true,\"req\":\"^0.31\"},{\"features\":[\"semconv_experimental\"],\"name\":\"opentelemetry-semantic-conventions\",\"optional\":true,\"req\":\"^0.31\"},{\"default_features\":false,\"features\":[\"trace\",\"rt-tokio\"],\"name\":\"opentelemetry_sdk\",\"optional\":true,\"req\":\"^0.31\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"name\":\"rama-error\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-macros\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"macros\",\"fs\",\"io-std\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.48\"},{\"name\":\"tokio-graceful\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"features\":[\"codec\",\"io\",\"io-util\"],\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"tracing-opentelemetry\",\"optional\":true,\"req\":\"^0.32\"}],\"features\":{\"default\":[],\"opentelemetry\":[\"dep:opentelemetry\",\"dep:opentelemetry-semantic-conventions\",\"dep:opentelemetry_sdk\",\"dep:tracing-opentelemetry\"]}}", @@ -1395,6 +1406,7 @@ "rand_core_0.6.4": "{\"dependencies\":[{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.2\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[],\"serde1\":[\"serde\"],\"std\":[\"alloc\",\"getrandom\",\"getrandom/std\"]}}", "rand_core_0.9.5": "{\"dependencies\":[{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"os_rng\":[\"dep:getrandom\"],\"serde\":[\"dep:serde\"],\"std\":[\"getrandom?/std\"]}}", "rand_xorshift_0.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.118\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", + "rand_xoshiro_0.7.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", "ratatui-macros_0.6.0": "{\"dependencies\":[{\"features\":[\"user-hooks\"],\"kind\":\"dev\",\"name\":\"cargo-husky\",\"req\":\"^1.5.0\"},{\"name\":\"ratatui\",\"req\":\"^0.29.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.101\"}],\"features\":{}}", "rayon-core_1.13.0": "{\"dependencies\":[{\"name\":\"crossbeam-deque\",\"req\":\"^0.8.1\"},{\"name\":\"crossbeam-utils\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"scoped-tls\",\"req\":\"^1.0\"},{\"name\":\"wasm_sync\",\"optional\":true,\"req\":\"^0.1.0\"}],\"features\":{\"web_spin_lock\":[\"dep:wasm_sync\"]}}", "rayon_1.11.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"either\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.4\"},{\"name\":\"rayon-core\",\"req\":\"^1.13.0\"},{\"name\":\"wasm_sync\",\"optional\":true,\"req\":\"^0.1.0\"}],\"features\":{\"web_spin_lock\":[\"dep:wasm_sync\",\"rayon-core/web_spin_lock\"]}}", @@ -1442,6 +1454,7 @@ "rustversion_1.0.22": "{\"dependencies\":[{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.49\"}],\"features\":{}}", "rustyline_14.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.2\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"default_features\":false,\"name\":\"buffer-redux\",\"optional\":true,\"req\":\"^1.0\",\"target\":\"cfg(unix)\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"clipboard-win\",\"req\":\"^5.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"fd-lock\",\"optional\":true,\"req\":\"^4.0.0\"},{\"name\":\"home\",\"optional\":true,\"req\":\"^0.5.4\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"memchr\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"fs\",\"ioctl\",\"poll\",\"signal\",\"term\"],\"name\":\"nix\",\"req\":\"^0.28\",\"target\":\"cfg(unix)\"},{\"name\":\"radix_trie\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5.5\"},{\"default_features\":false,\"features\":[\"bundled\",\"backup\"],\"name\":\"rusqlite\",\"optional\":true,\"req\":\"^0.31.0\"},{\"name\":\"rustyline-derive\",\"optional\":true,\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"signal-hook\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"name\":\"skim\",\"optional\":true,\"req\":\"^0.10\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"},{\"name\":\"termios\",\"optional\":true,\"req\":\"^0.3.3\",\"target\":\"cfg(unix)\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.0\"},{\"name\":\"unicode-width\",\"req\":\"^0.1\"},{\"name\":\"utf8parse\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Console\",\"Win32_Security\",\"Win32_System_Threading\",\"Win32_UI_Input_KeyboardAndMouse\"],\"name\":\"windows-sys\",\"req\":\"^0.52.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"case_insensitive_history_search\":[\"regex\"],\"custom-bindings\":[\"radix_trie\"],\"default\":[\"custom-bindings\",\"with-dirs\",\"with-file-history\"],\"derive\":[\"rustyline-derive\"],\"with-dirs\":[\"home\"],\"with-file-history\":[\"fd-lock\"],\"with-fuzzy\":[\"skim\"],\"with-sqlite-history\":[\"rusqlite\"]}}", "ryu_1.0.22": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.4\"}],\"features\":{\"small\":[]}}", + "safe_arch_0.9.3": "{\"dependencies\":[{\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.2\"}],\"features\":{\"default\":[]}}", "salsa20_0.10.2": "{\"dependencies\":[{\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3.3\"}],\"features\":{\"std\":[\"cipher/std\"],\"zeroize\":[\"cipher/zeroize\"]}}", "same-file_1.0.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"winapi-util\",\"req\":\"^0.1.1\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "scc_2.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"name\":\"equivalent\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"name\":\"sdd\",\"req\":\"^3.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.47\"}],\"features\":{\"loom\":[\"dep:loom\",\"sdd/loom\"]}}", @@ -1560,6 +1573,7 @@ "system-configuration_0.7.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"core-foundation\",\"req\":\"^0.9\"},{\"name\":\"system-configuration-sys\",\"req\":\"^0.6\"}],\"features\":{}}", "system-deps_7.0.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.5\"},{\"features\":[\"targets\"],\"name\":\"cfg-expr\",\"req\":\">=0.17, <0.21\"},{\"name\":\"heck\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"pkg-config\",\"req\":\"^0.3.25\"},{\"default_features\":false,\"features\":[\"parse\",\"std\"],\"name\":\"toml\",\"req\":\"^0.9\"},{\"name\":\"version-compare\",\"req\":\"^0.2\"}],\"features\":{}}", "tagptr_0.2.0": "{\"dependencies\":[],\"features\":{}}", + "tap_1.0.1": "{\"dependencies\":[],\"features\":{}}", "tar_0.4.44": "{\"dependencies\":[{\"name\":\"filetime\",\"req\":\"^0.2.8\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"name\":\"xattr\",\"optional\":true,\"req\":\"^1.1.3\",\"target\":\"cfg(unix)\"}],\"features\":{\"default\":[\"xattr\"]}}", "tar_0.4.45": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"astral-tokio-tar\",\"req\":\"^0.5\"},{\"name\":\"filetime\",\"req\":\"^0.2.8\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"xattr\",\"optional\":true,\"req\":\"^1.1.3\",\"target\":\"cfg(unix)\"}],\"features\":{\"default\":[\"xattr\"]}}", "target-lexicon_0.13.3": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"arch_z80\":[],\"arch_zkasm\":[],\"default\":[],\"serde_support\":[\"serde\",\"std\"],\"std\":[]}}", @@ -1717,6 +1731,7 @@ "which_6.0.3": "{\"dependencies\":[{\"name\":\"either\",\"req\":\"^1.9.0\"},{\"name\":\"home\",\"req\":\"^0.5.9\",\"target\":\"cfg(any(windows, unix, target_os = \\\"redox\\\"))\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.10.2\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"req\":\"^0.38.30\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\", target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.9.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.40\"},{\"features\":[\"kernel\"],\"name\":\"winsafe\",\"req\":\"^0.0.19\",\"target\":\"cfg(windows)\"}],\"features\":{\"regex\":[\"dep:regex\"],\"tracing\":[\"dep:tracing\"]}}", "which_8.0.0": "{\"dependencies\":[{\"name\":\"env_home\",\"optional\":true,\"req\":\"^0.1.0\",\"target\":\"cfg(any(windows, unix, target_os = \\\"redox\\\"))\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.10.2\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"optional\":true,\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\", target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.9.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.40\"},{\"features\":[\"kernel\"],\"name\":\"winsafe\",\"optional\":true,\"req\":\"^0.0.19\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"real-sys\"],\"real-sys\":[\"dep:env_home\",\"dep:rustix\",\"dep:winsafe\"],\"regex\":[\"dep:regex\"],\"tracing\":[\"dep:tracing\"]}}", "whoami_1.6.1": "{\"dependencies\":[{\"name\":\"libredox\",\"req\":\"^0.1.1\",\"target\":\"cfg(all(target_os = \\\"redox\\\", not(target_arch = \\\"wasm32\\\")))\"},{\"name\":\"wasite\",\"req\":\"^0.1\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\"))\"},{\"features\":[\"Navigator\",\"Document\",\"Window\",\"Location\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\"), not(daku)))\"}],\"features\":{\"default\":[\"web\"],\"web\":[\"web-sys\"]}}", + "wide_0.8.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.3\"},{\"name\":\"bytemuck\",\"req\":\"^1.24.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"features\":[\"bytemuck\"],\"name\":\"safe_arch\",\"req\":\"^0.9.2\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"std\"],\"serde\":[\"dep:serde\"],\"std\":[]}}", "widestring_1.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"debugger_test\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"debugger_test_parser\",\"req\":\"^0.1\"},{\"features\":[\"Win32_System_Diagnostics_Debug\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.59\"}],\"features\":{\"alloc\":[],\"debugger_visualizer\":[\"alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "wildcard_0.3.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.10.5\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.203\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.8.14\"},{\"kind\":\"dev\",\"name\":\"wildmatch\",\"req\":\"^2.3.4\"}],\"features\":{\"fatal-warnings\":[]}}", "wildmatch_2.6.1": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"ntest\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.10.2\"},{\"kind\":\"dev\",\"name\":\"regex-lite\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", @@ -1801,6 +1816,7 @@ "wit-parser_0.244.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"id-arena\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2.7.0\"},{\"kind\":\"dev\",\"name\":\"libtest-mimic\",\"req\":\"^0.8.1\"},{\"name\":\"log\",\"req\":\"^0.4.17\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.3.0\"},{\"default_features\":false,\"name\":\"semver\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.166\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0.166\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"unicode-xid\",\"req\":\"^0.2.2\"},{\"default_features\":false,\"features\":[\"simd\",\"std\",\"validate\",\"component-model\",\"features\"],\"name\":\"wasmparser\",\"optional\":true,\"req\":\"^0.244.0\"},{\"default_features\":false,\"features\":[\"component-model\"],\"name\":\"wat\",\"optional\":true,\"req\":\"^1.244.0\"}],\"features\":{\"decoding\":[\"dep:wasmparser\"],\"default\":[\"serde\",\"decoding\"],\"serde\":[\"dep:serde\",\"dep:serde_derive\",\"indexmap/serde\",\"serde_json\"],\"wat\":[\"decoding\",\"dep:wat\"]}}", "wl-clipboard-rs_0.9.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.168\"},{\"name\":\"log\",\"req\":\"^0.4.11\"},{\"features\":[\"io_safety\"],\"name\":\"os_pipe\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"proptest-derive\",\"req\":\"^0.7\"},{\"features\":[\"fs\",\"event\"],\"name\":\"rustix\",\"req\":\"^1.0.2\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"name\":\"tree_magic_mini\",\"req\":\"^3\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.11\"},{\"name\":\"wayland-client\",\"req\":\"^0.31.11\"},{\"features\":[\"client\",\"staging\"],\"name\":\"wayland-protocols\",\"req\":\"^0.32.9\"},{\"features\":[\"server\",\"staging\"],\"kind\":\"dev\",\"name\":\"wayland-protocols\",\"req\":\"^0.32.9\"},{\"features\":[\"client\"],\"name\":\"wayland-protocols-wlr\",\"req\":\"^0.3.9\"},{\"features\":[\"server\"],\"kind\":\"dev\",\"name\":\"wayland-protocols-wlr\",\"req\":\"^0.3.9\"},{\"kind\":\"dev\",\"name\":\"wayland-server\",\"req\":\"^0.31.10\"}],\"features\":{\"dlopen\":[\"native_lib\",\"wayland-backend/dlopen\",\"wayland-backend/dlopen\"],\"native_lib\":[\"wayland-backend/client_system\",\"wayland-backend/server_system\"]}}", "writeable_0.6.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"either\",\"optional\":true,\"req\":\"^1.9.0\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\"],\"either\":[\"dep:either\"]}}", + "wyz_0.5.1": "{\"dependencies\":[{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"tap\",\"req\":\"^1\"},{\"name\":\"typemap\",\"optional\":true,\"req\":\"^0.3\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"garbage\":[\"once_cell\",\"typemap\"],\"std\":[\"alloc\"]}}", "x11rb-protocol_0.13.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"all-extensions\":[\"composite\",\"damage\",\"dbe\",\"dpms\",\"dri2\",\"dri3\",\"glx\",\"present\",\"randr\",\"record\",\"render\",\"res\",\"screensaver\",\"shape\",\"shm\",\"sync\",\"xevie\",\"xf86dri\",\"xf86vidmode\",\"xfixes\",\"xinerama\",\"xinput\",\"xkb\",\"xprint\",\"xselinux\",\"xtest\",\"xv\",\"xvmc\"],\"composite\":[\"xfixes\"],\"damage\":[\"xfixes\"],\"dbe\":[],\"default\":[\"std\"],\"dpms\":[],\"dri2\":[],\"dri3\":[],\"extra-traits\":[],\"glx\":[],\"present\":[\"randr\",\"xfixes\",\"sync\",\"dri3\"],\"randr\":[\"render\"],\"record\":[],\"render\":[],\"request-parsing\":[],\"res\":[],\"resource_manager\":[\"std\"],\"screensaver\":[],\"shape\":[],\"shm\":[],\"std\":[],\"sync\":[],\"xevie\":[],\"xf86dri\":[],\"xf86vidmode\":[],\"xfixes\":[\"render\",\"shape\"],\"xinerama\":[],\"xinput\":[\"xfixes\"],\"xkb\":[],\"xprint\":[],\"xselinux\":[],\"xtest\":[],\"xv\":[\"shm\"],\"xvmc\":[\"xv\"]}}", "x11rb_0.13.2": "{\"dependencies\":[{\"name\":\"as-raw-xcb-connection\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"gethostname\",\"req\":\"^1.0\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"libloading\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.19\"},{\"kind\":\"dev\",\"name\":\"polling\",\"req\":\"^3.4\"},{\"name\":\"raw-window-handle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"default_features\":false,\"features\":[\"std\",\"event\",\"fs\",\"net\",\"system\"],\"name\":\"rustix\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"x11rb-protocol\",\"req\":\"^0.13.2\"},{\"name\":\"xcursor\",\"optional\":true,\"req\":\"^0.3.7\"}],\"features\":{\"all-extensions\":[\"x11rb-protocol/all-extensions\",\"composite\",\"damage\",\"dbe\",\"dpms\",\"dri2\",\"dri3\",\"glx\",\"present\",\"randr\",\"record\",\"render\",\"res\",\"screensaver\",\"shape\",\"shm\",\"sync\",\"xevie\",\"xf86dri\",\"xf86vidmode\",\"xfixes\",\"xinerama\",\"xinput\",\"xkb\",\"xprint\",\"xselinux\",\"xtest\",\"xv\",\"xvmc\"],\"allow-unsafe-code\":[\"libc\",\"as-raw-xcb-connection\"],\"composite\":[\"x11rb-protocol/composite\",\"xfixes\"],\"cursor\":[\"render\",\"resource_manager\",\"xcursor\"],\"damage\":[\"x11rb-protocol/damage\",\"xfixes\"],\"dbe\":[\"x11rb-protocol/dbe\"],\"dl-libxcb\":[\"allow-unsafe-code\",\"libloading\",\"once_cell\"],\"dpms\":[\"x11rb-protocol/dpms\"],\"dri2\":[\"x11rb-protocol/dri2\"],\"dri3\":[\"x11rb-protocol/dri3\"],\"extra-traits\":[\"x11rb-protocol/extra-traits\"],\"glx\":[\"x11rb-protocol/glx\"],\"image\":[],\"present\":[\"x11rb-protocol/present\",\"randr\",\"xfixes\",\"sync\"],\"randr\":[\"x11rb-protocol/randr\",\"render\"],\"record\":[\"x11rb-protocol/record\"],\"render\":[\"x11rb-protocol/render\"],\"request-parsing\":[\"x11rb-protocol/request-parsing\"],\"res\":[\"x11rb-protocol/res\"],\"resource_manager\":[\"x11rb-protocol/resource_manager\"],\"screensaver\":[\"x11rb-protocol/screensaver\"],\"shape\":[\"x11rb-protocol/shape\"],\"shm\":[\"x11rb-protocol/shm\"],\"sync\":[\"x11rb-protocol/sync\"],\"xevie\":[\"x11rb-protocol/xevie\"],\"xf86dri\":[\"x11rb-protocol/xf86dri\"],\"xf86vidmode\":[\"x11rb-protocol/xf86vidmode\"],\"xfixes\":[\"x11rb-protocol/xfixes\",\"render\",\"shape\"],\"xinerama\":[\"x11rb-protocol/xinerama\"],\"xinput\":[\"x11rb-protocol/xinput\",\"xfixes\"],\"xkb\":[\"x11rb-protocol/xkb\"],\"xprint\":[\"x11rb-protocol/xprint\"],\"xselinux\":[\"x11rb-protocol/xselinux\"],\"xtest\":[\"x11rb-protocol/xtest\"],\"xv\":[\"x11rb-protocol/xv\",\"shm\"],\"xvmc\":[\"x11rb-protocol/xvmc\",\"xv\"]}}", "x25519-dalek_2.0.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"zeroize_derive\"],\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"curve25519-dalek/alloc\",\"serde?/alloc\",\"zeroize?/alloc\"],\"default\":[\"alloc\",\"precomputed-tables\",\"zeroize\"],\"getrandom\":[\"rand_core/getrandom\"],\"precomputed-tables\":[\"curve25519-dalek/precomputed-tables\"],\"reusable_secrets\":[],\"serde\":[\"dep:serde\",\"curve25519-dalek/serde\"],\"static_secrets\":[],\"zeroize\":[\"dep:zeroize\",\"curve25519-dalek/zeroize\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ec654051e4..cbc8f524d9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1322,6 +1322,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -1411,11 +1423,31 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "byteorder" @@ -3760,6 +3792,7 @@ dependencies = [ "diffy", "dirs", "dunce", + "icy_sixel", "image", "insta", "itertools 0.14.0", @@ -5469,6 +5502,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "faster-hex" version = "0.10.0" @@ -5780,6 +5819,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -7586,6 +7631,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icy_sixel" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85518b9086bf01117761b90e7691c0ef3236fa8adfb1fb44dd248fe5f87215d5" +dependencies = [ + "quantette", + "thiserror 2.0.18", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -8955,7 +9010,7 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "getrandom 0.2.17", "http 1.4.0", @@ -9390,6 +9445,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -9423,7 +9487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -9454,6 +9518,30 @@ dependencies = [ "sha2", ] +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "bytemuck", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "parking" version = "2.2.1" @@ -10127,6 +10215,26 @@ dependencies = [ "num-traits", ] +[[package]] +name = "quantette" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c98fecda8b16396ff9adac67644a523dd1778c42b58606a29df5c31ca925d174" +dependencies = [ + "bitvec", + "bytemuck", + "image", + "libm", + "num-traits", + "ordered-float", + "palette", + "rand 0.9.3", + "rand_xoshiro", + "rayon", + "ref-cast", + "wide", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -10213,6 +10321,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "radix_trie" version = "0.2.1" @@ -10611,6 +10725,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "ratatui" version = "0.29.0" @@ -11136,6 +11259,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +[[package]] +name = "safe_arch" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629516c85c29fe757770fa03f2074cf1eac43d44c02a3de9fc2ef7b0e207dfdd" +dependencies = [ + "bytemuck", +] + [[package]] name = "salsa20" version = "0.10.2" @@ -12516,6 +12648,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.45" @@ -14024,6 +14162,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ca908d26e4786149c48efcf6c0ea09ab0e06d1fe3c17dc1b4b0f1ca4a7e788" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "widestring" version = "1.2.1" @@ -14067,7 +14215,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -14689,6 +14837,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11rb" version = "0.13.2" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index dd0cd7e491..d82f68fde9 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -293,6 +293,7 @@ icu_locale_core = "2.1" icu_provider = { version = "2.1", features = ["sync"] } ignore = "0.4.23" image = { version = "^0.25.9", default-features = false } +icy_sixel = "0.5" include_dir = "0.7.4" indexmap = "2.12.0" insta = "1.46.3" diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 39fd0a442f..1d94c9d919 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -695,6 +695,12 @@ pub struct Tui { #[serde(default)] pub theme: Option, + /// Pet id to preselect in the terminal pet picker. + /// + /// Custom pet ids resolve against CODEX_HOME/pets//pet.json. + #[serde(default)] + pub pet: Option, + /// Preferred layout for resume/fork session picker results. #[serde(default)] pub session_picker_view: Option, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index edbf45694f..4c14beb51b 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2606,6 +2606,11 @@ "default": true, "description": "Enable desktop notifications from the TUI. Defaults to `true`." }, + "pet": { + "default": null, + "description": "Pet id to preselect in the terminal pet picker.\n\nCustom pet ids resolve against CODEX_HOME/pets//pet.json.", + "type": "string" + }, "raw_output_mode": { "default": false, "description": "Start the TUI in raw scrollback mode for copy-friendly transcript output. Defaults to `false`.", @@ -4595,4 +4600,4 @@ }, "title": "ConfigToml", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 9bda54a9a7..93565f3c78 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -563,6 +563,7 @@ fn config_toml_deserializes_model_availability_nux() { status_line_use_colors: true, terminal_title: None, theme: None, + pet: None, session_picker_view: None, keymap: TuiKeymap::default(), model_availability_nux: ModelAvailabilityNuxConfig { @@ -2177,6 +2178,19 @@ session_picker_view = "dense" ); } +#[test] +fn tui_pet_deserializes_from_toml() { + let cfg = r#" +[tui] +pet = "chefito" +"#; + let parsed = toml::from_str::(cfg).expect("TOML deserialization should succeed"); + assert_eq!( + parsed.tui.as_ref().and_then(|t| t.pet.as_deref()), + Some("chefito"), + ); +} + #[test] fn tui_session_picker_view_defaults_to_none() { let cfg = r#" @@ -2189,6 +2203,15 @@ fn tui_session_picker_view_defaults_to_none() { ); } +#[test] +fn tui_pet_defaults_to_none() { + let cfg = r#" +[tui] +"#; + let parsed = toml::from_str::(cfg).expect("TOML deserialization should succeed"); + assert_eq!(parsed.tui.as_ref().and_then(|t| t.pet.as_deref()), None); +} + #[test] fn tui_config_missing_notifications_field_defaults_to_enabled() { let cfg = r#" @@ -2212,6 +2235,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() { status_line_use_colors: true, terminal_title: None, theme: None, + pet: None, session_picker_view: None, keymap: TuiKeymap::default(), model_availability_nux: ModelAvailabilityNuxConfig::default(), @@ -7083,6 +7107,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { tui_status_line_use_colors: true, tui_terminal_title: None, tui_theme: None, + tui_pet: None, tui_session_picker_view: SessionPickerViewMode::Dense, otel: OtelConfig::default(), }, @@ -7503,6 +7528,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { tui_status_line_use_colors: true, tui_terminal_title: None, tui_theme: None, + tui_pet: None, tui_session_picker_view: SessionPickerViewMode::Dense, otel: OtelConfig::default(), }; @@ -7661,6 +7687,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { tui_status_line_use_colors: true, tui_terminal_title: None, tui_theme: None, + tui_pet: None, tui_session_picker_view: SessionPickerViewMode::Dense, otel: OtelConfig::default(), }; @@ -7804,6 +7831,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { tui_status_line_use_colors: true, tui_terminal_title: None, tui_theme: None, + tui_pet: None, tui_session_picker_view: SessionPickerViewMode::Dense, otel: OtelConfig::default(), }; diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index 02ec9dc898..6ae85702cb 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -91,6 +91,14 @@ pub fn syntax_theme_edit(name: &str) -> ConfigEdit { } } +/// Produces a config edit that sets [tui].pet = "". +pub fn tui_pet_edit(name: &str) -> ConfigEdit { + ConfigEdit::SetPath { + segments: vec!["tui".to_string(), "pet".to_string()], + value: value(name.to_string()), + } +} + /// Produces a config edit that sets `[tui].session_picker_view = ""`. pub fn session_picker_view_edit(mode: SessionPickerViewMode) -> ConfigEdit { ConfigEdit::SetPath { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index b1277eda48..66aa30153f 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -546,6 +546,9 @@ pub struct Config { /// Syntax highlighting theme override (kebab-case name). pub tui_theme: Option, + /// Pet id preselected by the terminal pet picker. + pub tui_pet: Option, + /// Preferred layout for resume/fork session picker results. pub tui_session_picker_view: SessionPickerViewMode, @@ -3184,6 +3187,7 @@ impl Config { .unwrap_or(true), tui_terminal_title: cfg.tui.as_ref().and_then(|t| t.terminal_title.clone()), tui_theme: cfg.tui.as_ref().and_then(|t| t.theme.clone()), + tui_pet: cfg.tui.as_ref().and_then(|t| t.pet.clone()), tui_session_picker_view: config_profile .tui .as_ref() diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 68a95bd2d9..e3642d31bd 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -203,6 +203,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R tui_terminal_title: None, tui_theme: None, tui_raw_output_mode: false, + tui_pet: None, terminal_resize_reflow: TerminalResizeReflowConfig::default(), tui_keymap: TuiKeymap::default(), tui_session_picker_view: SessionPickerViewMode::Dense, diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index d3d28461b6..77064fe39e 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -73,6 +73,7 @@ diffy = { workspace = true } dirs = { workspace = true } dunce = { workspace = true } image = { workspace = true, features = ["jpeg", "png", "gif", "webp"] } +icy_sixel = { workspace = true } itertools = { workspace = true } lazy_static = { workspace = true } pathdiff = { workspace = true } diff --git a/codex-rs/tui/pets/boba/pet.json b/codex-rs/tui/pets/boba/pet.json new file mode 100644 index 0000000000..f9944dc72c --- /dev/null +++ b/codex-rs/tui/pets/boba/pet.json @@ -0,0 +1,62 @@ +{ + "id": "boba", + "displayName": "Boba", + "description": "A tiny otter sipping bubble tea while keeping you company in Codex.", + "spritesheetPath": "spritesheet.webp", + "frame": { + "width": 192, + "height": 208, + "columns": 8, + "rows": 9 + }, + "animations": { + "idle": { + "frames": [0, 1, 2, 3, 4, 5], + "fps": 5 + }, + "move_left": { + "frames": [8, 9, 10, 11, 12, 13, 14, 15], + "fps": 10 + }, + "move_right": { + "frames": [16, 17, 18, 19, 20, 21, 22, 23], + "fps": 10 + }, + "wave": { + "frames": [24, 25, 26, 27], + "fps": 7, + "loop": false, + "fallback": "idle" + }, + "sit": { + "frames": [32, 33, 34, 35, 36], + "fps": 6 + }, + "sad": { + "frames": [40, 41, 42, 43, 44, 45, 46], + "fps": 6 + }, + "sleep": { + "frames": [43, 44, 47], + "fps": 3 + }, + "sip": { + "frames": [48, 49, 50, 51, 52, 53], + "fps": 8, + "loop": false, + "fallback": "idle" + }, + "bounce": { + "frames": [56, 57, 58, 59, 60, 61], + "fps": 9, + "loop": false, + "fallback": "idle" + }, + "grumpy": { + "frames": [64, 65, 66, 67, 68, 69], + "fps": 6, + "loop": false, + "fallback": "idle" + } + } +} diff --git a/codex-rs/tui/pets/boba/spritesheet.webp b/codex-rs/tui/pets/boba/spritesheet.webp new file mode 100644 index 0000000000..83ebad4ab1 Binary files /dev/null and b/codex-rs/tui/pets/boba/spritesheet.webp differ diff --git a/codex-rs/tui/pets/codex/pet.json b/codex-rs/tui/pets/codex/pet.json new file mode 100644 index 0000000000..7669cbebce --- /dev/null +++ b/codex-rs/tui/pets/codex/pet.json @@ -0,0 +1,23 @@ +{ + "id": "codex", + "displayName": "Codex", + "description": "The original Codex companion.", + "spritesheetPath": "spritesheet.webp", + "frame": { + "width": 192, + "height": 208, + "columns": 8, + "rows": 9 + }, + "animations": { + "idle": { "frames": [0, 1, 2, 3, 4, 5], "fps": 1.5 }, + "move_right": { "frames": [8, 9, 10, 11, 12, 13, 14, 15], "fps": 8 }, + "move_left": { "frames": [16, 17, 18, 19, 20, 21, 22, 23], "fps": 8 }, + "wave": { "frames": [24, 25, 26, 27], "fps": 7, "loop": false, "fallback": "idle" }, + "bounce": { "frames": [32, 33, 34, 35, 36], "fps": 7, "loop": false, "fallback": "idle" }, + "failed": { "frames": [40, 41, 42, 43, 44, 45, 46, 47], "fps": 7 }, + "waiting": { "frames": [48, 49, 50, 51, 52, 53], "fps": 6 }, + "running": { "frames": [56, 57, 58, 59, 60, 61], "fps": 8 }, + "review": { "frames": [64, 65, 66, 67, 68, 69], "fps": 6 } + } +} diff --git a/codex-rs/tui/pets/codex/spritesheet.webp b/codex-rs/tui/pets/codex/spritesheet.webp new file mode 100644 index 0000000000..8979a9eab6 Binary files /dev/null and b/codex-rs/tui/pets/codex/spritesheet.webp differ diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index dff288493c..46c1345bd2 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -155,6 +155,7 @@ use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use crossterm::event::KeyModifiers; use ratatui::backend::Backend; +use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Paragraph; @@ -1056,13 +1057,18 @@ See the Codex keymap documentation for supported actions and examples." if let Err(err) = app_server.shutdown().await { tracing::warn!(error = %err, "failed to shut down embedded app server"); } + let clear_pet_result = tui.clear_ambient_pet_image(); let clear_result = tui.terminal.clear(); let exit_reason = match exit_reason_result { Ok(exit_reason) => { + clear_pet_result?; clear_result?; exit_reason } Err(err) => { + if let Err(clear_pet_err) = clear_pet_result { + tracing::warn!(error = %clear_pet_err, "failed to clear ambient pet image"); + } if let Err(clear_err) = clear_result { tracing::warn!(error = %clear_err, "failed to clear terminal UI"); } @@ -1149,6 +1155,16 @@ See the Codex keymap documentation for supported actions and examples." } })?; } + let terminal_size = tui.terminal.size()?; + let ambient_pet_area = Rect::new( + /*x*/ 0, + /*y*/ 0, + terminal_size.width, + terminal_size.height, + ); + tui.draw_ambient_pet_image( + self.chat_widget.ambient_pet_draw(ambient_pet_area), + )?; if self.chat_widget.external_editor_state() == ExternalEditorState::Requested { self.chat_widget .set_external_editor_state(ExternalEditorState::Active); diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index e50c2782ca..1ee5ef73db 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -515,6 +515,11 @@ impl App { self.chat_widget.set_tui_theme(Some(name)); } + pub(super) fn sync_tui_pet_selection(&mut self, pet: String) { + self.config.tui_pet = Some(pet.clone()); + self.chat_widget.set_tui_pet(Some(pet)); + } + pub(super) fn restore_runtime_theme_from_config(&self) { if let Some(name) = self.config.tui_theme.as_deref() && let Some(theme) = @@ -732,4 +737,17 @@ terminal_resize_reflow_max_rows = 9000 Some("dracula") ); } + + #[tokio::test] + async fn sync_tui_pet_selection_updates_chat_widget_config_copy() { + let mut app = make_test_app().await; + + app.sync_tui_pet_selection("chefito".to_string()); + + assert_eq!(app.config.tui_pet.as_deref(), Some("chefito")); + assert_eq!( + app.chat_widget.config_ref().tui_pet.as_deref(), + Some("chefito") + ); + } } diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index b2261f155a..9b5323bcca 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -391,6 +391,23 @@ impl App { AppEvent::OpenUrlInBrowser { url } => { self.open_url_in_browser(url); } + AppEvent::PetSelected { pet_id } => { + let edit = crate::legacy_core::config::edit::tui_pet_edit(&pet_id); + let apply_result = ConfigEditsBuilder::new(&self.config.codex_home) + .with_edits([edit]) + .apply() + .await; + match apply_result { + Ok(()) => { + self.sync_tui_pet_selection(pet_id); + tui.frame_requester().schedule_frame(); + } + Err(err) => { + self.chat_widget + .add_error_message(format!("Failed to save pet selection: {err}")); + } + } + } AppEvent::RefreshConnectors { force_refetch } => { self.chat_widget.refresh_connectors(force_refetch); } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index b72e909293..0173a2b472 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -297,6 +297,11 @@ pub(crate) enum AppEvent { url: String, }, + /// Persist a pet selection and reload the ambient pet. + PetSelected { + pet_id: String, + }, + /// Refresh app connector state and mention bindings. RefreshConnectors { force_refetch: bool, diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 2bd553ba6b..88ddd36a65 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -7656,6 +7656,60 @@ mod tests { } } + #[test] + fn slash_popup_pets_for_pet_ui() { + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + + let mut composer = ChatComposer::new( + /*has_input_focus*/ true, + sender, + /*enhanced_keys_supported*/ false, + "Ask Codex to do anything".to_string(), + /*disable_paste_burst*/ false, + ); + + type_chars_humanlike(&mut composer, &['/', 'p', 'e', 't']); + + let mut terminal = Terminal::new(TestBackend::new(60, 5)).expect("terminal"); + terminal + .draw(|f| composer.render(f.area(), f.buffer_mut())) + .expect("draw composer"); + + insta::assert_snapshot!("slash_popup_pet", terminal.backend()); + } + + #[test] + fn slash_popup_pets_for_pet_logic() { + use super::super::command_popup::CommandItem; + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + /*has_input_focus*/ true, + sender, + /*enhanced_keys_supported*/ false, + "Ask Codex to do anything".to_string(), + /*disable_paste_burst*/ false, + ); + type_chars_humanlike(&mut composer, &['/', 'p', 'e', 't']); + + match &composer.active_popup { + ActivePopup::Command(popup) => match popup.selected_item() { + Some(CommandItem::Builtin(cmd)) => { + assert_eq!(cmd.command(), "pets") + } + Some(CommandItem::ServiceTier(command)) => { + panic!("expected pets command, got service tier {command:?}") + } + None => panic!("no selected command for '/pet'"), + }, + _ => panic!("slash popup not active after typing '/pet'"), + } + } + #[test] fn service_tier_slash_command_dispatches_from_catalog_name() { let (tx, _rx) = unbounded_channel::(); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap new file mode 100644 index 0000000000..93b8e763c0 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap @@ -0,0 +1,9 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› /pet " +" " +" " +" /pets choose the terminal pet " diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 85a9c3a8f3..b5f53560e7 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -879,6 +879,8 @@ pub(crate) struct ChatWidget { recent_auto_review_denials: RecentAutoReviewDenials, // Active hook runs render in a dedicated live cell so they can run alongside tools. active_hook_cell: Option, + // Ambient companion rendered over the transcript area, never inside the footer rows. + ambient_pet: Option, // Semantic status used for terminal-title status rendering. terminal_title_status_kind: TerminalTitleStatusKind, // Previous status header to restore after a transient stream retry. @@ -2473,6 +2475,10 @@ impl ChatWidget { self.set_status_header(String::from("Working")); self.full_reasoning_buffer.clear(); self.reasoning_buffer.clear(); + self.set_ambient_pet_notification( + crate::pets::PetNotificationKind::Running, + /*body*/ None, + ); self.request_redraw(); } @@ -2564,6 +2570,10 @@ impl ChatWidget { self.suppressed_exec_calls.clear(); self.last_unified_wait = None; self.unified_exec_wait_streak = None; + if !from_replay { + let body = Notification::agent_turn_preview(¬ification_response); + self.set_ambient_pet_notification(crate::pets::PetNotificationKind::Review, body); + } self.request_redraw(); let had_pending_steers = !self.pending_steers.is_empty(); @@ -3035,6 +3045,10 @@ impl ChatWidget { self.submit_pending_steers_after_interrupt = false; self.finalize_turn(); self.add_to_history(history_cell::new_error_event(message)); + self.set_ambient_pet_notification( + crate::pets::PetNotificationKind::Failed, + /*body*/ None, + ); self.request_redraw(); // After an error ends the turn, try sending the next queued input. @@ -4211,6 +4225,9 @@ impl ChatWidget { self.update_due_hook_visibility(); self.schedule_hook_timer_if_needed(); self.bottom_pane.pre_draw_tick(); + if let Some(pet) = self.ambient_pet.as_ref() { + pet.schedule_next_frame(); + } self.refresh_plan_mode_nudge(); self.refresh_goal_status_indicator_for_time_tick(); if self.terminal_title_shows_action_required() != self.last_terminal_title_requires_action { @@ -4535,6 +4552,10 @@ impl ChatWidget { }; self.bottom_pane .push_approval_request(request, &self.config.features); + self.set_ambient_pet_notification( + crate::pets::PetNotificationKind::Waiting, + /*body*/ None, + ); self.request_redraw(); } @@ -4551,6 +4572,10 @@ impl ChatWidget { }; self.bottom_pane .push_approval_request(request, &self.config.features); + self.set_ambient_pet_notification( + crate::pets::PetNotificationKind::Waiting, + /*body*/ None, + ); self.request_redraw(); self.notify(Notification::EditApprovalRequested { cwd: self.config.cwd.to_path_buf(), @@ -4609,12 +4634,20 @@ impl ChatWidget { } } } + self.set_ambient_pet_notification( + crate::pets::PetNotificationKind::Waiting, + /*body*/ None, + ); self.request_redraw(); } pub(crate) fn push_approval_request(&mut self, request: ApprovalRequest) { self.bottom_pane .push_approval_request(request, &self.config.features); + self.set_ambient_pet_notification( + crate::pets::PetNotificationKind::Waiting, + /*body*/ None, + ); self.request_redraw(); } @@ -4624,6 +4657,10 @@ impl ChatWidget { ) { self.bottom_pane .push_mcp_server_elicitation_request(request); + self.set_ambient_pet_notification( + crate::pets::PetNotificationKind::Waiting, + /*body*/ None, + ); self.request_redraw(); } @@ -4638,6 +4675,10 @@ impl ChatWidget { }; self.notify(Notification::PlanModePrompt { title }); self.bottom_pane.push_user_input_request(ev); + self.set_ambient_pet_notification( + crate::pets::PetNotificationKind::Waiting, + /*body*/ None, + ); self.request_redraw(); } @@ -4652,6 +4693,10 @@ impl ChatWidget { }; self.bottom_pane .push_approval_request(request, &self.config.features); + self.set_ambient_pet_notification( + crate::pets::PetNotificationKind::Waiting, + /*body*/ None, + ); self.request_redraw(); } @@ -4910,6 +4955,7 @@ impl ChatWidget { &chat_keymap.edit_queued_message, current_terminal_info, ); + let ambient_pet = load_ambient_pet(&config, frame_requester.clone()); let mut widget = Self { app_event_tx: app_event_tx.clone(), frame_requester: frame_requester.clone(), @@ -4996,6 +5042,7 @@ impl ChatWidget { pending_guardian_review_status: PendingGuardianReviewStatus::default(), recent_auto_review_denials: RecentAutoReviewDenials::default(), active_hook_cell: None, + ambient_pet, terminal_title_status_kind: TerminalTitleStatusKind::Working, retry_status_header: None, pending_status_indicator_restore: false, @@ -6747,6 +6794,24 @@ impl ChatWidget { self.frame_requester.schedule_frame(); } + fn set_ambient_pet_notification( + &mut self, + kind: crate::pets::PetNotificationKind, + body: Option, + ) { + if let Some(pet) = self.ambient_pet.as_mut() { + pet.set_notification(kind, body); + } + } + + pub(crate) fn ambient_pet_draw(&self, area: Rect) -> Option { + self.bottom_pane.no_modal_or_popup_active().then(|| { + self.ambient_pet + .as_ref()? + .draw_request(area, self.footer_height(area.width)) + })? + } + fn bump_active_cell_revision(&mut self) { // Wrapping avoids overflow; wraparound would require 2^64 bumps and at // worst causes a one-time cache-key collision. @@ -7048,6 +7113,14 @@ impl ChatWidget { self.bottom_pane.show_selection_view(params); } + fn open_pets_picker(&mut self) { + let params = crate::pets::build_pet_picker_params( + self.config.tui_pet.as_deref(), + &self.config.codex_home, + ); + self.bottom_pane.show_selection_view(params); + } + fn status_line_context_window_size(&self) -> Option { self.token_info .as_ref() @@ -9364,6 +9437,13 @@ impl ChatWidget { self.config.tui_theme = theme; } + /// Set the pet preselected by the TUI picker in the widget's config copy. + pub(crate) fn set_tui_pet(&mut self, pet: Option) { + self.config.tui_pet = pet; + self.ambient_pet = load_ambient_pet(&self.config, self.frame_requester.clone()); + self.request_redraw(); + } + /// Set the model in the widget's config copy and stored collaboration mode. pub(crate) fn set_model(&mut self, model: &str) { self.current_collaboration_mode = self.current_collaboration_mode.with_updates( @@ -10933,9 +11013,47 @@ impl Drop for ChatWidget { } } +fn load_ambient_pet( + config: &Config, + frame_requester: FrameRequester, +) -> Option { + match crate::pets::AmbientPet::load( + config.tui_pet.as_deref(), + &config.codex_home, + frame_requester.clone(), + ) { + Ok(pet) => Some(pet), + Err(err) if config.tui_pet.is_some() => { + tracing::warn!( + error = %err, + "failed to load configured ambient pet; falling back to default" + ); + crate::pets::AmbientPet::load( + /*selected_pet*/ None, + &config.codex_home, + frame_requester, + ) + .map_err(|fallback_err| { + tracing::warn!(error = %fallback_err, "failed to load default ambient pet"); + fallback_err + }) + .ok() + } + Err(err) => { + tracing::warn!(error = %err, "failed to load ambient pet"); + None + } + } +} + impl Renderable for ChatWidget { fn render(&self, area: Rect, buf: &mut Buffer) { self.as_renderable().render(area, buf); + if self.bottom_pane.no_modal_or_popup_active() + && let Some(pet) = self.ambient_pet.as_ref() + { + pet.render_overlay(area, self.footer_height(area.width), buf); + } self.last_rendered_width.set(Some(area.width as usize)); } @@ -10952,6 +11070,12 @@ impl Renderable for ChatWidget { } } +impl ChatWidget { + fn footer_height(&self, width: u16) -> u16 { + self.bottom_pane.desired_height(width).saturating_add(1) + } +} + #[derive(Debug)] enum Notification { AgentTurnComplete { response: String }, diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index cedc0e9686..11d13140c8 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -404,6 +404,9 @@ impl ChatWidget { SlashCommand::Theme => { self.open_theme_picker(); } + SlashCommand::Pets => { + self.open_pets_picker(); + } SlashCommand::Ps => { self.add_ps_output(); } @@ -769,6 +772,10 @@ impl ChatWidget { self.app_event_tx .send(AppEvent::BeginWindowsSandboxGrantReadRoot { path: args }); } + SlashCommand::Pets if !trimmed.is_empty() => { + self.app_event_tx + .send(AppEvent::PetSelected { pet_id: args }); + } _ => self.dispatch_command(cmd), } if source == SlashCommandDispatchSource::Live && cmd != SlashCommand::Goal { @@ -958,7 +965,8 @@ impl ChatWidget { | SlashCommand::Hooks | SlashCommand::Title | SlashCommand::Statusline - | SlashCommand::Theme => QueueDrain::Stop, + | SlashCommand::Theme + | SlashCommand::Pets => QueueDrain::Stop, } } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap index 15511611a1..f26da349a1 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/exec_flow.rs expression: terminal.backend().vt100().screen().contents() --- diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_multiline_prefix_no_execpolicy.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_multiline_prefix_no_execpolicy.snap index 3c256fe923..00cc144ebb 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_multiline_prefix_no_execpolicy.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_multiline_prefix_no_execpolicy.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/exec_flow.rs expression: contents --- diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap index 04b278a126..7395f8d577 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/exec_flow.rs expression: terminal.backend().vt100().screen().contents() --- diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap index 8635b66682..acd2af60cd 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap @@ -2,6 +2,8 @@ source: tui/src/chatwidget/tests/exec_flow.rs expression: contents --- + + Would you like to make the following edits? Reason: The model wants to apply changes diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap index f0956946c9..52ac576149 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/permissions.rs expression: popup --- Update Model Permissions diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap index 52779fd840..dfcfae199f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: term.backend().vt100().screen().contents() +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) --- diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap index a21401ff95..9642be5f12 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap @@ -1,6 +1,5 @@ --- source: tui/src/chatwidget/tests/status_and_layout.rs -assertion_line: 2288 expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) --- diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap index 3086586b11..b757f7621a 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap @@ -1,6 +1,5 @@ --- source: tui/src/chatwidget/tests/exec_flow.rs -assertion_line: 40 expression: "format!(\"{buf:?}\")" --- Buffer { diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_good_result_consent_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_good_result_consent_popup.snap index ffc3e7f15a..2ba27d409f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_good_result_consent_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_good_result_consent_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Upload logs? diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap index 01f1175f3c..b027333ad7 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Upload logs? diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__full_access_confirmation_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__full_access_confirmation_popup.snap index 71dac5f590..d98ce2b772 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__full_access_confirmation_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__full_access_confirmation_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/permissions.rs expression: popup --- Enable full access? diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_enable_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_enable_prompt.snap index f9b0daefcc..a229db53b3 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_enable_prompt.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__memories_enable_prompt.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Enable memories? diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_reasoning_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_reasoning_selection_popup.snap index 27c7d061ee..baef861f4c 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_reasoning_selection_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_reasoning_selection_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Select Reasoning Level for gpt-5.4 diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap index 0d57fceac9..58478fa919 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__multi_agent_enable_prompt.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Enable subagents? diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__personality_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__personality_selection_popup.snap index d9a6e0a23c..8613d66814 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__personality_selection_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__personality_selection_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Select Personality diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup.snap index 25897974fa..c64088b38a 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/plan_mode.rs expression: popup --- Implement this plan? diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_no_selected.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_no_selected.snap index e8083a0e01..a24ce666d8 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_no_selected.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_no_selected.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/plan_mode.rs expression: popup --- Implement this plan? diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_error_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_error_popup.snap index 5305a9fc78..7350ccc0b9 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_error_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_error_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Plugins diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installable.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installable.snap index e55edcae89..c7edae1714 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installable.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installable.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: popup +source: tui/src/chatwidget/tests/popups_and_settings.rs +expression: strip_osc8_for_snapshot(&popup) --- Plugins Figma · Can be installed · ChatGPT Marketplace diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installed.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installed.snap index 272ebb7c2a..4d91b17f34 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installed.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugin_detail_popup_installed.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: popup +source: tui/src/chatwidget/tests/popups_and_settings.rs +expression: strip_osc8_for_snapshot(&popup) --- Plugins Figma · Installed · ChatGPT Marketplace diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_marketplace_remove_confirmation.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_marketplace_remove_confirmation.snap index f46bbec9e7..dec8f1d77e 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_marketplace_remove_confirmation.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_marketplace_remove_confirmation.snap @@ -1,6 +1,5 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 411 expression: confirmation --- Plugins diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap index 515b700925..08c575692b 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_newly_installed_marketplace.snap @@ -1,6 +1,5 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 339 expression: popup --- Plugins diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap index 849b90b2ce..a5d0d2824d 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap @@ -1,6 +1,5 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 767 expression: popup --- Plugins diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_switch_prompt_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_switch_prompt_popup.snap index bb217615d9..83213897ef 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_switch_prompt_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_switch_prompt_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/status_and_layout.rs expression: popup --- Approaching rate limits diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup.snap index 8c60f961f9..05c5acac40 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Settings diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup_narrow.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup_narrow.snap index 8c60f961f9..05c5acac40 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup_narrow.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup_narrow.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Settings diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_microphone_picker_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_microphone_picker_popup.snap index 418fb5c9ef..76ee7ec7e6 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_microphone_picker_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_microphone_picker_popup.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Select Microphone diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__renamed_thread_footer_title.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__renamed_thread_footer_title.snap index e024f534ae..2616a5981c 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__renamed_thread_footer_title.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__renamed_thread_footer_title.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: normalized_backend_snapshot(terminal.backend()) --- " " " " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_pets_picker.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_pets_picker.snap new file mode 100644 index 0000000000..3d125274ff --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_pets_picker.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/chatwidget/tests/slash_commands.rs +expression: popup +--- + Select Pet + Choose a pet to wake in the terminal. + + Type to filter pets... + Boba A tiny otter sipping bubble tea while keeping you company + in Codex. +› Codex (current) The original Codex companion. + + Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_rename_prefilled_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_rename_prefilled_prompt.snap index f1eb0d223b..1f454aa193 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_rename_prefilled_prompt.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_rename_prefilled_prompt.snap @@ -1,6 +1,5 @@ --- source: tui/src/chatwidget/tests/slash_commands.rs -assertion_line: 135 expression: popup --- ▌ Rename thread diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_fast_mode_footer.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_fast_mode_footer.snap index ad644699c4..c558ac4726 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_fast_mode_footer.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_fast_mode_footer.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: normalized_backend_snapshot(terminal.backend()) --- " " " " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_plan_mode_footer.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_plan_mode_footer.snap index 0eef02e0a8..fc82518882 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_plan_mode_footer.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_plan_mode_footer.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: normalized_backend_snapshot(terminal.backend()) --- " " " " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap index 5e6e33dece..532b44c6cb 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: normalized_backend_snapshot(terminal.backend()) --- " " " " diff --git a/codex-rs/tui/src/chatwidget/tests/guardian.rs b/codex-rs/tui/src/chatwidget/tests/guardian.rs index 7cdc9f760b..9e7013e087 100644 --- a/codex-rs/tui/src/chatwidget/tests/guardian.rs +++ b/codex-rs/tui/src/chatwidget/tests/guardian.rs @@ -149,7 +149,7 @@ async fn guardian_approved_exec_renders_approved_request() { let width: u16 = 120; let ui_height: u16 = chat.desired_height(width); - let vt_height: u16 = 12; + let vt_height: u16 = ui_height.saturating_add(1).max(12); let viewport = Rect::new(0, vt_height - ui_height - 1, width, ui_height); let backend = VT100Backend::new(width, vt_height); @@ -227,7 +227,7 @@ async fn guardian_approved_request_permissions_renders_request_summary() { let width: u16 = 110; let ui_height: u16 = chat.desired_height(width); - let vt_height: u16 = 12; + let vt_height: u16 = ui_height.saturating_add(1).max(12); let viewport = Rect::new(0, vt_height - ui_height - 1, width, ui_height); let backend = VT100Backend::new(width, vt_height); @@ -412,7 +412,7 @@ async fn app_server_guardian_review_denied_renders_denied_request_snapshot() { let width: u16 = 140; let ui_height: u16 = chat.desired_height(width); - let vt_height: u16 = 16; + let vt_height: u16 = ui_height.saturating_add(1).max(16); let viewport = Rect::new(0, vt_height - ui_height - 1, width, ui_height); let backend = VT100Backend::new(width, vt_height); @@ -493,7 +493,7 @@ async fn app_server_guardian_review_timed_out_renders_timed_out_request_snapshot let width: u16 = 140; let ui_height: u16 = chat.desired_height(width); - let vt_height: u16 = 16; + let vt_height: u16 = ui_height.saturating_add(1).max(16); let viewport = Rect::new(0, vt_height - ui_height - 1, width, ui_height); let backend = VT100Backend::new(width, vt_height); diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 05f967b5a4..a72329ade6 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -183,6 +183,7 @@ pub(super) async fn make_chatwidget_manual( let current_collaboration_mode = base_mode; let active_collaboration_mask = collaboration_modes::default_mask(model_catalog.as_ref()); let effective_service_tier = cfg.service_tier.clone(); + let ambient_pet = load_ambient_pet(&cfg, FrameRequester::test_dummy()); let mut widget = ChatWidget { app_event_tx, codex_op_target: super::CodexOpTarget::Direct(op_tx), @@ -260,6 +261,7 @@ pub(super) async fn make_chatwidget_manual( full_reasoning_buffer: String::new(), current_status: StatusIndicatorState::working(), active_hook_cell: None, + ambient_pet, retry_status_header: None, pending_status_indicator_restore: false, suppress_queue_autosend: false, diff --git a/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs b/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs index 977bea2887..da4777bfb1 100644 --- a/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs +++ b/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs @@ -102,7 +102,7 @@ async fn app_server_mcp_startup_failure_renders_warning_history() { let width: u16 = 120; let ui_height: u16 = chat.desired_height(width); - let vt_height: u16 = 10; + let vt_height: u16 = ui_height.saturating_add(1).max(10); let viewport = Rect::new(0, vt_height - ui_height - 1, width, ui_height); let backend = VT100Backend::new(width, vt_height); diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 4c5319a640..92e386eada 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -1790,6 +1790,34 @@ async fn slash_resume_with_arg_requests_named_session() { assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); } +#[tokio::test] +async fn slash_pets_opens_picker() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.dispatch_command(SlashCommand::Pets); + + assert!(chat.bottom_pane.has_active_view()); + assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); + + let popup = render_bottom_popup(&chat, /*width*/ 80); + assert_chatwidget_snapshot!("slash_pets_picker", popup); +} + +#[tokio::test] +async fn slash_pets_with_arg_selects_named_pet() { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.bottom_pane + .set_composer_text("/pets chefito".to_string(), Vec::new(), Vec::new()); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + assert_matches!( + rx.try_recv(), + Ok(AppEvent::PetSelected { pet_id }) if pet_id == "chefito" + ); + assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); +} + #[tokio::test] async fn slash_fork_requests_current_fork() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__approval_requests__exec_approval_modal_exec.snap b/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__approval_requests__exec_approval_modal_exec.snap index 055a6292f1..7e766d67ed 100644 --- a/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__approval_requests__exec_approval_modal_exec.snap +++ b/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__approval_requests__exec_approval_modal_exec.snap @@ -1,5 +1,5 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/approval_requests.rs expression: "format!(\"{buf:?}\")" --- Buffer { diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index df88bcec93..f7e5425a21 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -1198,6 +1198,77 @@ async fn ui_snapshots_small_heights_task_running() { } } +#[tokio::test] +async fn ambient_pet_defaults_to_codex_and_stays_above_the_footer() { + use ratatui::layout::Rect; + + let (chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + assert_eq!( + chat.ambient_pet + .as_ref() + .map(crate::pets::AmbientPet::selected_pet_id), + Some("codex"), + ); + + let area = Rect::new( + /*x*/ 0, /*y*/ 0, /*width*/ 60, /*height*/ 20, + ); + let draw = chat + .ambient_pet_draw(area) + .expect("ambient pet draw request"); + assert_eq!(draw.x, 51); + assert_eq!(draw.y, 10); + assert_eq!(draw.columns, 9); + assert_eq!(draw.rows, 5); +} + +#[tokio::test] +async fn ambient_pet_draw_uses_terminal_screen_area_not_short_inline_viewport() { + use ratatui::layout::Rect; + + let (chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + assert!( + chat.ambient_pet_draw(Rect::new( + /*x*/ 0, /*y*/ 21, /*width*/ 80, /*height*/ 3, + )) + .is_none(), + "a normal short inline viewport cannot fit the ambient pet" + ); + + let draw = chat + .ambient_pet_draw(Rect::new( + /*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 24, + )) + .expect("full terminal screen has room for the ambient pet"); + assert_eq!(draw.x, 71); + assert_eq!(draw.y, 14); +} + +#[tokio::test] +async fn ambient_pet_uses_the_app_notification_labels() { + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + for (kind, label) in [ + (crate::pets::PetNotificationKind::Running, "Running"), + (crate::pets::PetNotificationKind::Waiting, "Needs input"), + (crate::pets::PetNotificationKind::Review, "Ready"), + (crate::pets::PetNotificationKind::Failed, "Blocked"), + ] { + chat.set_ambient_pet_notification(kind, /*body*/ None); + let mut terminal = Terminal::new(TestBackend::new(60, 20)).expect("create terminal"); + terminal + .draw(|f| chat.render(f.area(), f.buffer_mut())) + .expect("draw ambient pet notification"); + assert!( + normalized_backend_snapshot(terminal.backend()).contains(label), + "expected {label} notification to render" + ); + } +} + // Snapshot test: status widget + approval modal active together // The modal takes precedence visually; this captures the layout with a running // task (status indicator active) while an approval request is shown. diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index ac5b489af1..1cccd6c825 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -110,6 +110,7 @@ mod clipboard_paste; mod collaboration_modes; mod color; pub(crate) mod custom_terminal; +mod pets; pub use custom_terminal::Terminal; mod auto_review_denials; mod cwd_prompt; diff --git a/codex-rs/tui/src/pets/ambient.rs b/codex-rs/tui/src/pets/ambient.rs new file mode 100644 index 0000000000..be1bc94d14 --- /dev/null +++ b/codex-rs/tui/src/pets/ambient.rs @@ -0,0 +1,340 @@ +//! Ambient terminal rendering for the Codex companion. +//! +//! Ambient pets reuse the same extracted image frames as the full-screen viewer. The surrounding +//! TUI still owns the notification text and layout slot; the sprite itself is emitted through the +//! terminal image protocol after ratatui finishes drawing the frame. + +use std::path::PathBuf; +use std::time::Duration; +use std::time::Instant; + +use anyhow::Context; +use anyhow::Result; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; + +use crate::tui::FrameRequester; + +use super::DEFAULT_PET_ID; +use super::frames; +use super::image_protocol::ImageProtocol; +use super::image_protocol::ProtocolSelection; +use super::model::Animation; +use super::model::Pet; + +const PET_TARGET_HEIGHT_PX: u16 = 75; + +const RUNNING_LIFETIME: Duration = Duration::from_secs(3 * 60); +const FAILED_LIFETIME: Duration = Duration::from_secs(60 * 60); +const WAITING_LIFETIME: Duration = Duration::from_secs(24 * 60 * 60); +const REVIEW_LIFETIME: Duration = Duration::from_secs(7 * 24 * 60 * 60); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PetNotificationKind { + Running, + Waiting, + Review, + Failed, +} + +impl PetNotificationKind { + fn animation_name(self) -> &'static str { + match self { + Self::Running => "running", + Self::Waiting => "waiting", + Self::Review => "review", + Self::Failed => "failed", + } + } + + fn label(self) -> &'static str { + match self { + Self::Running => "Running", + Self::Waiting => "Needs input", + Self::Review => "Ready", + Self::Failed => "Blocked", + } + } + + fn fallback_body(self) -> &'static str { + match self { + Self::Running => "Thinking", + Self::Waiting => "Needs input", + Self::Review => "Ready", + Self::Failed => "Blocked", + } + } + + fn lifetime(self) -> Duration { + match self { + Self::Running => RUNNING_LIFETIME, + Self::Waiting => WAITING_LIFETIME, + Self::Review => REVIEW_LIFETIME, + Self::Failed => FAILED_LIFETIME, + } + } +} + +#[derive(Debug, Clone)] +struct PetNotification { + kind: PetNotificationKind, + body: String, + updated_at: Instant, +} + +impl PetNotification { + fn new(kind: PetNotificationKind, body: Option) -> Self { + Self { + kind, + body: body.unwrap_or_else(|| kind.fallback_body().to_string()), + updated_at: Instant::now(), + } + } + + fn is_expired(&self, now: Instant) -> bool { + now.saturating_duration_since(self.updated_at) >= self.kind.lifetime() + } +} + +#[derive(Debug, Clone)] +pub(crate) struct AmbientPetDraw { + pub(crate) frame: PathBuf, + pub(crate) protocol: ImageProtocol, + pub(crate) x: u16, + pub(crate) y: u16, + pub(crate) columns: u16, + pub(crate) rows: u16, + pub(crate) height_px: u16, + pub(crate) sixel_dir: PathBuf, +} + +pub(crate) struct AmbientPet { + pet: Pet, + protocol: ImageProtocol, + frames: Vec, + sixel_dir: PathBuf, + frame_requester: FrameRequester, + notification: Option, + animation_started_at: Instant, +} + +impl AmbientPet { + pub(crate) fn load( + selected_pet: Option<&str>, + codex_home: &std::path::Path, + frame_requester: FrameRequester, + ) -> Result { + let pet = + Pet::load_with_codex_home(selected_pet.unwrap_or(DEFAULT_PET_ID), Some(codex_home)) + .with_context(|| "load ambient pet")?; + let cache_dir = frames::cache_dir().join("tui-pets").join(&pet.id); + let frame_dir = cache_dir.join("frames"); + let sixel_dir = cache_dir.join("sixel"); + let frames = frames::prepare_png_frames(&pet, &frame_dir)?; + Ok(Self { + pet, + protocol: ProtocolSelection::Auto.resolve(), + frames, + sixel_dir, + frame_requester, + notification: None, + animation_started_at: Instant::now(), + }) + } + + pub(crate) fn set_notification(&mut self, kind: PetNotificationKind, body: Option) { + self.notification = Some(PetNotification::new(kind, body)); + self.animation_started_at = Instant::now(); + } + + pub(crate) fn schedule_next_frame(&self) { + let animation = self.current_animation(); + if animation.frames.len() <= 1 { + return; + } + let frame_duration = Duration::from_secs_f64(1.0 / animation.fps.max(0.1)); + let elapsed = self.animation_started_at.elapsed(); + let rem = elapsed.as_nanos() % frame_duration.as_nanos(); + let delay = if rem == 0 { + frame_duration + } else { + frame_duration.saturating_sub(Duration::from_nanos(rem as u64)) + }; + self.frame_requester.schedule_frame_in(delay); + } + + pub(crate) fn draw_request(&self, area: Rect, footer_height: u16) -> Option { + let size = self.image_size(); + let notification = self.visible_notification(Instant::now()); + let notification_height = notification.map_or(0, notification_height); + let notification_width = notification.map_or(0, notification_width); + let required_height = size.rows.saturating_add(notification_height); + if area.height < required_height.saturating_add(footer_height) + || area.width < size.columns.max(notification_width) + { + return None; + } + + let x = area.x + area.width.saturating_sub(size.columns); + let y = area + .bottom() + .saturating_sub(footer_height) + .saturating_sub(size.rows); + Some(AmbientPetDraw { + frame: self.current_frame_path(), + protocol: self.protocol, + x, + y, + columns: size.columns, + rows: size.rows, + height_px: size.height_px, + sixel_dir: self.sixel_dir.clone(), + }) + } + + #[cfg(test)] + pub(crate) fn selected_pet_id(&self) -> &str { + &self.pet.id + } + + pub(crate) fn render_overlay(&self, area: Rect, footer_height: u16, buf: &mut Buffer) { + let notification = self.visible_notification(Instant::now()); + let size = self.image_size(); + let notification_height = notification.map_or(0, notification_height); + let notification_width = notification.map_or(0, notification_width); + let required_height = size.rows.saturating_add(notification_height); + if area.height < required_height.saturating_add(footer_height) + || area.width < size.columns.max(notification_width) + { + return; + } + + if let Some(notification) = notification { + let x = area.x + + area + .width + .saturating_sub(notification_width.max(size.columns)); + let y = area + .bottom() + .saturating_sub(footer_height) + .saturating_sub(size.rows + notification_height); + render_notification(notification, x, y, buf); + } + } + + fn visible_notification(&self, now: Instant) -> Option<&PetNotification> { + self.notification + .as_ref() + .filter(|notification| !notification.is_expired(now)) + } + + fn current_animation(&self) -> &Animation { + let animation_name = self + .visible_notification(Instant::now()) + .map_or("idle", |notification| notification.kind.animation_name()); + let Some(animation) = self + .pet + .animations + .get(animation_name) + .or_else(|| self.pet.animations.get("idle")) + else { + unreachable!("ambient pets always have an idle animation"); + }; + if !animation.loop_animation { + let elapsed_frames = (self.animation_started_at.elapsed().as_secs_f64() + * animation.fps.max(0.1)) + .floor() as usize; + if elapsed_frames >= animation.frames.len() + && let Some(fallback) = self.pet.animations.get(&animation.fallback) + { + return fallback; + } + } + animation + } + + fn current_frame_path(&self) -> PathBuf { + let animation = self.current_animation(); + let frame_index = current_animation_frame(animation, self.animation_started_at.elapsed()); + let sprite_index = animation.frames[frame_index]; + self.frames[sprite_index.min(self.frames.len().saturating_sub(1))].clone() + } + + fn image_size(&self) -> ImageSize { + let rows = ((f64::from(PET_TARGET_HEIGHT_PX) / 15.0).round() as u16).max(1); + let aspect = f64::from(self.pet.frame_height) / f64::from(self.pet.frame_width) * 0.52; + let columns = (f64::from(rows) / aspect).round() as u16; + ImageSize { + columns: columns.max(1), + rows, + height_px: PET_TARGET_HEIGHT_PX, + } + } +} + +#[derive(Debug, Clone, Copy)] +struct ImageSize { + columns: u16, + rows: u16, + height_px: u16, +} + +fn current_animation_frame(animation: &Animation, elapsed: Duration) -> usize { + if animation.frames.len() <= 1 { + return 0; + } + let elapsed_frames = (elapsed.as_secs_f64() * animation.fps.max(0.1)).floor() as usize; + if animation.loop_animation { + elapsed_frames % animation.frames.len() + } else { + elapsed_frames.min(animation.frames.len() - 1) + } +} + +fn notification_height(notification: &PetNotification) -> u16 { + if notification.body == notification.kind.label() { + 1 + } else { + 2 + } +} + +fn notification_width(notification: &PetNotification) -> u16 { + notification + .kind + .label() + .len() + .max(notification.body.len()) + .try_into() + .unwrap_or(u16::MAX) +} + +fn render_notification(notification: &PetNotification, x: u16, y: u16, buf: &mut Buffer) { + let width = buf.area.right().saturating_sub(x); + let mut lines = vec![notification.kind.label()]; + if notification.body != notification.kind.label() { + lines.push(notification.body.as_str()); + } + for (offset, line) in lines.into_iter().enumerate() { + buf.set_stringn( + x, + y + offset as u16, + line, + width as usize, + ratatui::style::Style::default(), + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn notification_labels_match_codex_app_vocabulary() { + assert_eq!(PetNotificationKind::Running.label(), "Running"); + assert_eq!(PetNotificationKind::Waiting.label(), "Needs input"); + assert_eq!(PetNotificationKind::Review.label(), "Ready"); + assert_eq!(PetNotificationKind::Failed.label(), "Blocked"); + } +} diff --git a/codex-rs/tui/src/pets/frames.rs b/codex-rs/tui/src/pets/frames.rs new file mode 100644 index 0000000000..20cdfc5c13 --- /dev/null +++ b/codex-rs/tui/src/pets/frames.rs @@ -0,0 +1,117 @@ +use std::fs; +use std::path::Path; +use std::path::PathBuf; + +use anyhow::Context; +use anyhow::Result; +use image::GenericImageView; + +use super::model::Pet; + +pub(super) fn cache_dir() -> PathBuf { + if let Some(cache_home) = std::env::var_os("XDG_CACHE_HOME") { + return PathBuf::from(cache_home); + } + if let Some(home) = std::env::var_os("HOME") { + return PathBuf::from(home).join(".cache"); + } + std::env::temp_dir() +} + +pub(super) fn prepare_png_frames(pet: &Pet, frame_dir: &Path) -> Result> { + fs::create_dir_all(frame_dir).with_context(|| format!("create {}", frame_dir.display()))?; + + let expected: Vec = (0..pet.frame_count()) + .map(|index| frame_dir.join(format!("frame_{index:03}.png"))) + .collect(); + + let complete = expected.iter().all(|path| path.exists()); + if !complete { + for stale in glob_frame_files(frame_dir)? { + let _ = fs::remove_file(stale); + } + + let spritesheet = image::open(&pet.spritesheet_path) + .with_context(|| format!("read {}", pet.spritesheet_path.display()))?; + for row in 0..pet.rows { + for column in 0..pet.columns { + let index = row * pet.columns + column; + let frame = spritesheet.view( + column * pet.frame_width, + row * pet.frame_height, + pet.frame_width, + pet.frame_height, + ); + frame + .to_image() + .save_with_format(&expected[index as usize], image::ImageFormat::Png) + .with_context(|| format!("write {}", expected[index as usize].display()))?; + } + } + } + + Ok(expected) +} + +fn glob_frame_files(frame_dir: &Path) -> Result> { + if !frame_dir.exists() { + return Ok(Vec::new()); + } + + let mut paths = Vec::new(); + for entry in fs::read_dir(frame_dir).with_context(|| format!("read {}", frame_dir.display()))? { + let path = entry?.path(); + if path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with("frame_") && name.ends_with(".png")) + { + paths.push(path); + } + } + Ok(paths) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use image::ImageBuffer; + use image::Rgba; + + use super::*; + + #[test] + fn prepare_png_frames_slices_spritesheet_without_external_command() { + let dir = tempfile::tempdir().unwrap(); + let spritesheet_path = dir.path().join("spritesheet.png"); + let spritesheet: ImageBuffer, Vec> = ImageBuffer::from_fn(2, 1, |x, _| { + if x == 0 { + Rgba([255, 0, 0, 255]) + } else { + Rgba([0, 255, 0, 255]) + } + }); + spritesheet.save(&spritesheet_path).unwrap(); + + let frames = prepare_png_frames( + &Pet { + id: "tiny".to_string(), + display_name: "Tiny".to_string(), + description: String::new(), + spritesheet_path, + frame_width: 1, + frame_height: 1, + columns: 2, + rows: 1, + animations: HashMap::new(), + }, + &dir.path().join("frames"), + ) + .unwrap(); + + assert_eq!(frames.len(), 2); + assert!(frames[0].exists()); + assert!(frames[1].exists()); + } +} diff --git a/codex-rs/tui/src/pets/image_protocol.rs b/codex-rs/tui/src/pets/image_protocol.rs new file mode 100644 index 0000000000..f29750f649 --- /dev/null +++ b/codex-rs/tui/src/pets/image_protocol.rs @@ -0,0 +1,248 @@ +use std::env; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::Context; +use anyhow::Result; +use anyhow::bail; +use base64::Engine as _; +use base64::engine::general_purpose; +use icy_sixel::BackgroundMode; +use icy_sixel::PixelAspectRatio; +use icy_sixel::SixelImage; +use image::imageops::FilterType; + +const ESC: &str = "\x1b"; +const ST: &str = "\x1b\\"; +const KITTY_CHUNK_SIZE: usize = 4096; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ImageProtocol { + Kitty, + Sixel, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProtocolSelection { + Auto, + Kitty, + Sixel, +} + +impl ProtocolSelection { + pub fn resolve(self) -> ImageProtocol { + match self { + Self::Kitty => ImageProtocol::Kitty, + Self::Sixel => ImageProtocol::Sixel, + Self::Auto => detect_protocol(), + } + } +} + +impl FromStr for ProtocolSelection { + type Err = anyhow::Error; + + fn from_str(value: &str) -> Result { + match value { + "auto" => Ok(Self::Auto), + "kitty" => Ok(Self::Kitty), + "sixel" => Ok(Self::Sixel), + other => bail!("unknown protocol {other}; expected auto, kitty, or sixel"), + } + } +} + +fn detect_protocol() -> ImageProtocol { + let term = env::var("TERM").unwrap_or_default().to_ascii_lowercase(); + if env::var_os("KITTY_WINDOW_ID").is_some() || term.contains("kitty") { + return ImageProtocol::Kitty; + } + + let term_program = env::var("TERM_PROGRAM") + .unwrap_or_default() + .to_ascii_lowercase(); + if term.contains("sixel") + || term.contains("mlterm") + || term.contains("foot") + || env::var_os("WEZTERM_EXECUTABLE").is_some() + || term_program.contains("wezterm") + || term_program.contains("iterm") + { + return ImageProtocol::Sixel; + } + + ImageProtocol::Kitty +} + +pub fn kitty_delete_image(image_id: u32) -> String { + wrap_for_tmux_if_needed(&format!("{ESC}_Ga=d,d=I,i={image_id},q=2;{ST}")) +} + +pub fn kitty_transmit_png_with_id( + path: &Path, + columns: u16, + rows: u16, + image_id: Option, +) -> Result { + let png = fs::read(path).with_context(|| format!("read {}", path.display()))?; + let payload = general_purpose::STANDARD.encode(png); + let chunks = payload + .as_bytes() + .chunks(KITTY_CHUNK_SIZE) + .collect::>(); + + let mut command = String::new(); + for (index, chunk) in chunks.iter().enumerate() { + let chunk = std::str::from_utf8(chunk).context("base64 payload is not valid UTF-8")?; + let has_more = index + 1 < chunks.len(); + if index == 0 { + let image_id = image_id + .map(|image_id| format!(",i={image_id}")) + .unwrap_or_default(); + command.push_str(&format!( + "{ESC}_Ga=T,t=d,f=100,c={columns},r={rows},q=2{image_id},m={};{chunk}{ST}", + if has_more { 1 } else { 0 }, + )); + } else { + command.push_str(&format!( + "{ESC}_Gm={};{chunk}{ST}", + if has_more { 1 } else { 0 }, + )); + } + } + + Ok(wrap_for_tmux_if_needed(&command)) +} + +fn wrap_for_tmux_if_needed(command: &str) -> String { + if env::var_os("TMUX").is_none() { + return command.to_string(); + } + + let escaped = command.replace(ESC, "\x1b\x1b"); + format!("{ESC}Ptmux;{escaped}{ST}") +} + +pub fn sixel_frame(frame_path: &Path, cache_dir: &Path, height_px: u16) -> Result { + fs::create_dir_all(cache_dir).with_context(|| format!("create {}", cache_dir.display()))?; + + let stem = frame_path + .file_stem() + .and_then(|stem| stem.to_str()) + .context("frame path has no valid file stem")?; + let path = cache_dir.join(format!("{stem}_h{height_px}.six")); + if path.exists() { + return Ok(path); + } + + let frame = + image::open(frame_path).with_context(|| format!("read {}", frame_path.display()))?; + let height = u32::from(height_px).max(1); + let width = ((u64::from(frame.width()) * u64::from(height)) / u64::from(frame.height())) + .try_into() + .unwrap_or(u32::MAX) + .max(1); + let rgba = frame.resize(width, height, FilterType::Lanczos3).to_rgba8(); + let (width, height) = rgba.dimensions(); + let sixel = SixelImage::try_from_rgba(rgba.into_raw(), width as usize, height as usize) + .map_err(anyhow::Error::from)? + .with_aspect_ratio(PixelAspectRatio::Square) + .with_background_mode(BackgroundMode::Transparent) + .encode() + .map_err(anyhow::Error::from)?; + + fs::write(&path, sixel).with_context(|| format!("write {}", path.display()))?; + Ok(path) +} + +#[cfg(test)] +mod tests { + use serial_test::serial; + + use super::*; + + struct TmuxEnvGuard { + previous: Option, + } + + impl TmuxEnvGuard { + fn new(value: Option<&str>) -> Self { + let previous = env::var_os("TMUX"); + match value { + Some(value) => unsafe { env::set_var("TMUX", value) }, + None => unsafe { env::remove_var("TMUX") }, + } + Self { previous } + } + } + + impl Drop for TmuxEnvGuard { + fn drop(&mut self) { + match self.previous.take() { + Some(value) => unsafe { env::set_var("TMUX", value) }, + None => unsafe { env::remove_var("TMUX") }, + } + } + } + + #[test] + #[serial] + fn kitty_png_transmission_encodes_inline_data() { + let _guard = TmuxEnvGuard::new(/*value*/ None); + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("frame.png"); + fs::write(&path, b"png").unwrap(); + + let command = kitty_transmit_png_with_id( + &path, /*columns*/ 4, /*rows*/ 3, /*image_id*/ None, + ) + .unwrap(); + + assert!(command.starts_with("\x1b_Ga=T,t=d,f=100,c=4,r=3,q=2,m=0;")); + assert!(command.contains("cG5n")); + assert!(command.ends_with("\x1b\\")); + } + + #[test] + #[serial] + fn tmux_passthrough_wraps_and_escapes_control_sequence() { + let _guard = TmuxEnvGuard::new(Some("session")); + assert_eq!( + wrap_for_tmux_if_needed("\x1b_Gx;\x1b\\"), + "\x1bPtmux;\x1b\x1b_Gx;\x1b\x1b\\\x1b\\" + ); + } + + #[test] + fn parses_protocol_selection() { + assert_eq!( + "auto".parse::().unwrap(), + ProtocolSelection::Auto + ); + assert_eq!( + "kitty".parse::().unwrap(), + ProtocolSelection::Kitty + ); + assert_eq!( + "sixel".parse::().unwrap(), + ProtocolSelection::Sixel + ); + } + + #[test] + fn sixel_frame_encodes_with_rust_crate() { + let dir = tempfile::tempdir().unwrap(); + let frame_path = dir.path().join("frame.png"); + let rgba = image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 0, 0, 255])); + rgba.save(&frame_path).unwrap(); + + let sixel_path = + sixel_frame(&frame_path, &dir.path().join("sixel"), /*height_px*/ 1).unwrap(); + let sixel = fs::read_to_string(sixel_path).unwrap(); + + assert!(sixel.starts_with("\x1bP")); + assert!(sixel.ends_with("\x1b\\")); + } +} diff --git a/codex-rs/tui/src/pets/mod.rs b/codex-rs/tui/src/pets/mod.rs new file mode 100644 index 0000000000..b7219d03e2 --- /dev/null +++ b/codex-rs/tui/src/pets/mod.rs @@ -0,0 +1,125 @@ +//! Ambient terminal pets configured from the /pets slash command. +//! +//! The Codex app stores custom pets under $CODEX_HOME/pets//pet.json. +//! This module keeps that package shape intact while rendering the selected pet inline in the TUI. + +use std::io::Write; + +use anyhow::Context; +use anyhow::Result; +mod ambient; +mod frames; +mod image_protocol; +mod model; +mod picker; + +pub(crate) use ambient::AmbientPet; +pub(crate) use ambient::AmbientPetDraw; +pub(crate) use ambient::PetNotificationKind; +pub(crate) use picker::build_pet_picker_params; + +pub(crate) const DEFAULT_PET_ID: &str = "codex"; + +pub(crate) fn render_ambient_pet_image( + writer: &mut impl Write, + request: Option, +) -> Result<()> { + use crossterm::cursor::MoveTo; + use crossterm::cursor::RestorePosition; + use crossterm::cursor::SavePosition; + use crossterm::queue; + use image_protocol::ImageProtocol; + + const AMBIENT_PET_IMAGE_ID: u32 = 0xC0DE; + + write!( + writer, + "{}", + image_protocol::kitty_delete_image(AMBIENT_PET_IMAGE_ID) + )?; + let Some(request) = request else { + writer.flush()?; + return Ok(()); + }; + + let payload = match request.protocol { + ImageProtocol::Kitty => { + AmbientPetPayload::Text(image_protocol::kitty_transmit_png_with_id( + &request.frame, + request.columns, + request.rows, + Some(AMBIENT_PET_IMAGE_ID), + )?) + } + ImageProtocol::Sixel => { + let path = + image_protocol::sixel_frame(&request.frame, &request.sixel_dir, request.height_px)?; + let sixel = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?; + AmbientPetPayload::Bytes(sixel) + } + }; + + queue!(writer, SavePosition, MoveTo(request.x, request.y))?; + match payload { + AmbientPetPayload::Text(payload) => write!(writer, "{payload}")?, + AmbientPetPayload::Bytes(payload) => writer.write_all(&payload)?, + } + queue!(writer, RestorePosition)?; + writer.flush()?; + Ok(()) +} + +enum AmbientPetPayload { + Text(String), + Bytes(Vec), +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::image_protocol::ImageProtocol; + use super::*; + + #[test] + fn ambient_pet_image_restores_cursor_after_drawing() { + let dir = tempfile::tempdir().unwrap(); + let frame = dir.path().join("frame.png"); + std::fs::write(&frame, b"png").unwrap(); + let request = AmbientPetDraw { + frame, + protocol: ImageProtocol::Kitty, + x: 2, + y: 3, + columns: 4, + rows: 5, + height_px: 75, + sixel_dir: PathBuf::new(), + }; + let mut output = Vec::new(); + + render_ambient_pet_image(&mut output, Some(request)).unwrap(); + + let output = String::from_utf8(output).unwrap(); + let save = output.find("\x1b7").expect("saves cursor position"); + let move_to = output.find("\x1b[4;3H").expect("moves to pet position"); + let image = output.find("cG5n").expect("writes image payload"); + let restore = output.find("\x1b8").expect("restores cursor position"); + assert!(save < move_to); + assert!(move_to < image); + assert!(image < restore); + } + + #[test] + fn ambient_pet_image_clear_deletes_without_moving_cursor() { + let mut output = Vec::new(); + + render_ambient_pet_image(&mut output, /*request*/ None).unwrap(); + + let output = String::from_utf8(output).unwrap(); + assert!(output.contains("Ga=d,d=I,i=49374,q=2;")); + assert!(!output.contains("\x1b7")); + assert!(!output.contains("\x1b[")); + assert!(!output.contains("\x1b8")); + } +} diff --git a/codex-rs/tui/src/pets/model.rs b/codex-rs/tui/src/pets/model.rs new file mode 100644 index 0000000000..d0cd0d67b5 --- /dev/null +++ b/codex-rs/tui/src/pets/model.rs @@ -0,0 +1,346 @@ +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use std::path::PathBuf; + +use anyhow::Context; +use anyhow::Result; +use anyhow::bail; +use serde::Deserialize; + +#[derive(Debug, Clone)] +pub struct Animation { + pub frames: Vec, + pub fps: f64, + pub loop_animation: bool, + pub fallback: String, +} + +#[derive(Debug, Clone)] +pub struct Pet { + pub id: String, + pub display_name: String, + pub description: String, + pub spritesheet_path: PathBuf, + pub frame_width: u32, + pub frame_height: u32, + pub columns: u32, + pub rows: u32, + pub animations: HashMap, +} + +impl Pet { + pub(super) fn load(value: &str) -> Result { + Self::load_with_codex_home( + value, + crate::legacy_core::config::find_codex_home() + .ok() + .as_deref(), + ) + } + + pub(super) fn load_with_codex_home(value: &str, codex_home: Option<&Path>) -> Result { + let pet_dir = resolve_pet_dir(value, codex_home)?; + let config_path = pet_dir.join("pet.json"); + let raw = fs::read_to_string(&config_path) + .with_context(|| format!("read {}", config_path.display()))?; + let mut file: PetFile = serde_json::from_str(&raw) + .with_context(|| format!("parse {}", config_path.display()))?; + + if file.id.is_empty() { + file.id = pet_dir + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("pet") + .to_string(); + } + if file.display_name.is_empty() { + file.display_name.clone_from(&file.id); + } + if file.spritesheet_path.is_empty() { + file.spritesheet_path = "spritesheet.webp".to_string(); + } + + let frame = file.frame.unwrap_or_default(); + let spritesheet_path = if Path::new(&file.spritesheet_path).is_absolute() { + PathBuf::from(&file.spritesheet_path) + } else { + pet_dir.join(&file.spritesheet_path) + }; + if !spritesheet_path.exists() { + bail!("missing spritesheet {}", spritesheet_path.display()); + } + + Ok(Self { + id: file.id, + display_name: file.display_name, + description: file.description, + spritesheet_path, + frame_width: frame.width, + frame_height: frame.height, + columns: frame.columns, + rows: frame.rows, + animations: load_animations(file.animations), + }) + } + + pub fn frame_count(&self) -> usize { + (self.columns * self.rows) as usize + } +} + +#[derive(Debug, Deserialize)] +struct PetFile { + #[serde(default)] + id: String, + #[serde(default, rename = "displayName")] + display_name: String, + #[serde(default)] + description: String, + #[serde(default, rename = "spritesheetPath")] + spritesheet_path: String, + frame: Option, + #[serde(default)] + animations: HashMap, +} + +#[derive(Debug, Clone, Copy, Deserialize)] +struct FrameSpec { + width: u32, + height: u32, + columns: u32, + rows: u32, +} + +impl Default for FrameSpec { + fn default() -> Self { + Self { + width: 192, + height: 208, + columns: 8, + rows: 9, + } + } +} + +#[derive(Debug, Deserialize)] +struct AnimationSpec { + #[serde(default)] + frames: Vec, + fps: Option, + #[serde(rename = "loop")] + loop_animation: Option, + #[serde(default)] + fallback: String, +} + +fn resolve_pet_dir(value: &str, codex_home: Option<&Path>) -> Result { + if path_like(value) { + let path = expand_path(value)?; + let metadata = + fs::metadata(&path).with_context(|| format!("pet path {}", path.display()))?; + let dir = if metadata.is_dir() { + path + } else { + path.parent() + .context("pet json path has no containing directory")? + .to_path_buf() + }; + return dir + .canonicalize() + .with_context(|| format!("resolve {}", dir.display())); + } + + Ok(resolve_named_pet_dir(value, codex_home)) +} + +fn resolve_named_pet_dir(value: &str, codex_home: Option<&Path>) -> PathBuf { + if let Some(codex_home) = codex_home { + let installed_pet = codex_home.join("pets").join(value); + if installed_pet.join("pet.json").is_file() { + return installed_pet; + } + } + + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("pets") + .join(value) +} + +fn path_like(value: &str) -> bool { + value == "." + || value == ".." + || value.starts_with("~/") + || value.starts_with("../") + || value.starts_with("./") + || Path::new(value).is_absolute() + || value.contains('/') + || value.contains('\\') +} + +fn expand_path(value: &str) -> Result { + if value == "~" || value.starts_with("~/") { + let home = std::env::var_os("HOME").context("HOME is not set")?; + if value == "~" { + return Ok(PathBuf::from(home)); + } + return Ok(PathBuf::from(home).join(&value[2..])); + } + + Ok(PathBuf::from(value)) +} + +fn load_animations(specs: HashMap) -> HashMap { + let mut animations = default_animations(); + if specs.is_empty() { + return animations; + } + + for (name, spec) in specs { + if spec.frames.is_empty() { + continue; + } + + let fps = spec.fps.filter(|fps| *fps > 0.0).unwrap_or(8.0); + let fallback = if spec.fallback.is_empty() { + "idle".to_string() + } else { + spec.fallback + }; + + animations.insert( + name.clone(), + Animation { + frames: spec.frames, + fps, + loop_animation: spec.loop_animation.unwrap_or(true), + fallback, + }, + ); + } + + animations + .entry("idle".to_string()) + .or_insert_with(idle_animation); + animations +} + +fn default_animations() -> HashMap { + let idle = idle_animation(); + [ + ("idle", idle.frames, idle.fps, idle.loop_animation, "idle"), + ( + "move_left", + vec![8, 9, 10, 11, 12, 13, 14, 15], + 10.0, + true, + "idle", + ), + ( + "move_right", + vec![16, 17, 18, 19, 20, 21, 22, 23], + 10.0, + true, + "idle", + ), + ("wave", vec![24, 25, 26, 27], 7.0, false, "idle"), + ("sit", vec![32, 33, 34, 35, 36], 6.0, true, "idle"), + ("sad", vec![40, 41, 42, 43, 44, 45, 46], 6.0, true, "idle"), + ("sleep", vec![43, 44, 47], 3.0, true, "idle"), + ("sip", vec![48, 49, 50, 51, 52, 53], 8.0, false, "idle"), + ("bounce", vec![56, 57, 58, 59, 60, 61], 9.0, false, "idle"), + ("grumpy", vec![64, 65, 66, 67, 68, 69], 6.0, false, "idle"), + ] + .into_iter() + .map(|(name, frames, fps, loop_animation, fallback)| { + ( + name.to_string(), + Animation { + frames, + fps, + loop_animation, + fallback: fallback.to_string(), + }, + ) + }) + .collect() +} + +fn idle_animation() -> Animation { + Animation { + frames: vec![0, 1, 2, 3, 4, 5], + fps: 5.0, + loop_animation: true, + fallback: "idle".to_string(), + } +} + +#[cfg(test)] +mod tests { + use std::io::Write; + + use super::*; + + fn write_minimal_pet() -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + fs::write( + dir.path().join("pet.json"), + r#"{ + "id": "chefito", + "displayName": "Chefito", + "description": "A tiny recipe-loving chef", + "spritesheetPath": "spritesheet.webp" + }"#, + ) + .unwrap(); + fs::File::create(dir.path().join("spritesheet.webp")) + .unwrap() + .write_all(b"not-used-by-loader") + .unwrap(); + dir + } + + #[test] + fn load_pet_directory_uses_installed_pet_defaults() { + let dir = write_minimal_pet(); + + let pet = Pet::load(dir.path().to_str().unwrap()).unwrap(); + + assert_eq!(pet.id, "chefito"); + assert_eq!(pet.display_name, "Chefito"); + assert_eq!(pet.frame_width, 192); + assert_eq!(pet.frame_height, 208); + assert_eq!(pet.columns, 8); + assert_eq!(pet.rows, 9); + assert!(!pet.animations["idle"].frames.is_empty()); + } + + #[test] + fn load_pet_json_path_uses_containing_directory() { + let dir = write_minimal_pet(); + + let pet = Pet::load(dir.path().join("pet.json").to_str().unwrap()).unwrap(); + let expected = dir.path().join("spritesheet.webp").canonicalize().unwrap(); + + assert_eq!(pet.spritesheet_path, expected); + } + + #[test] + fn named_pet_prefers_codex_home_installation() { + let dir = write_minimal_pet(); + let codex_home = tempfile::tempdir().unwrap(); + let pet_dir = codex_home.path().join("pets").join("chefito"); + fs::create_dir_all(&pet_dir).unwrap(); + fs::copy(dir.path().join("pet.json"), pet_dir.join("pet.json")).unwrap(); + fs::copy( + dir.path().join("spritesheet.webp"), + pet_dir.join("spritesheet.webp"), + ) + .unwrap(); + + let pet = Pet::load_with_codex_home("chefito", Some(codex_home.path())).unwrap(); + + assert_eq!(pet.id, "chefito"); + assert_eq!(pet.spritesheet_path, pet_dir.join("spritesheet.webp"),); + } +} diff --git a/codex-rs/tui/src/pets/picker.rs b/codex-rs/tui/src/pets/picker.rs new file mode 100644 index 0000000000..db81114843 --- /dev/null +++ b/codex-rs/tui/src/pets/picker.rs @@ -0,0 +1,171 @@ +//! Builds the /pets picker dialog for the TUI. + +use std::fs; +use std::path::Path; + +use crate::app_event::AppEvent; +use crate::bottom_pane::SelectionItem; +use crate::bottom_pane::SelectionViewParams; +use crate::bottom_pane::popup_consts::standard_popup_hint_line; + +use super::DEFAULT_PET_ID; +use super::model::Pet; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct PetPickerEntry { + selector: String, + display_name: String, + description: Option, +} + +pub(crate) fn build_pet_picker_params( + current_pet: Option<&str>, + codex_home: &Path, +) -> SelectionViewParams { + let current_pet = current_pet.unwrap_or(DEFAULT_PET_ID); + let mut entries = available_pet_entries(codex_home); + entries.sort_by(|left, right| left.display_name.cmp(&right.display_name)); + + let mut initial_selected_idx = None; + let items = entries + .into_iter() + .enumerate() + .map(|(idx, entry)| { + let is_current = current_pet == entry.selector; + if is_current { + initial_selected_idx = Some(idx); + } + let pet_id = entry.selector.clone(); + SelectionItem { + name: entry.display_name, + description: entry.description, + is_current, + dismiss_on_select: true, + search_value: Some(entry.selector), + actions: vec![Box::new(move |tx| { + tx.send(AppEvent::PetSelected { + pet_id: pet_id.clone(), + }); + })], + ..Default::default() + } + }) + .collect(); + + SelectionViewParams { + title: Some("Select Pet".to_string()), + subtitle: Some("Choose a pet to wake in the terminal.".to_string()), + footer_hint: Some(standard_popup_hint_line()), + items, + is_searchable: true, + search_placeholder: Some("Type to filter pets...".to_string()), + initial_selected_idx, + ..Default::default() + } +} + +fn available_pet_entries(codex_home: &Path) -> Vec { + let mut entries = vec![pet_picker_entry(DEFAULT_PET_ID), pet_picker_entry("boba")]; + let pets_dir = codex_home.join("pets"); + let Ok(children) = fs::read_dir(pets_dir) else { + return entries; + }; + + for child in children.flatten() { + let path = child.path(); + if !path.join("pet.json").is_file() { + continue; + } + let Some(selector) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; + entries.push(pet_picker_entry_from_path(selector, &path)); + } + entries +} + +fn pet_picker_entry(selector: &str) -> PetPickerEntry { + match Pet::load(selector) { + Ok(pet) => PetPickerEntry { + selector: selector.to_string(), + display_name: pet.display_name, + description: (!pet.description.is_empty()).then_some(pet.description), + }, + Err(_) => PetPickerEntry { + selector: selector.to_string(), + display_name: selector.to_string(), + description: None, + }, + } +} + +fn pet_picker_entry_from_path(selector: &str, path: &Path) -> PetPickerEntry { + match Pet::load(path.to_string_lossy().as_ref()) { + Ok(pet) => PetPickerEntry { + selector: selector.to_string(), + display_name: pet.display_name, + description: (!pet.description.is_empty()).then_some(pet.description), + }, + Err(_) => PetPickerEntry { + selector: selector.to_string(), + display_name: selector.to_string(), + description: None, + }, + } +} + +#[cfg(test)] +mod tests { + use std::io::Write; + + use super::*; + + fn write_pet(dir: &Path, folder_name: &str, display_name: &str) { + let pet_dir = dir.join("pets").join(folder_name); + fs::create_dir_all(&pet_dir).unwrap(); + fs::write( + pet_dir.join("pet.json"), + format!( + r#"{{ + "id": "{folder_name}", + "displayName": "{display_name}", + "description": "custom pet", + "spritesheetPath": "spritesheet.webp" + }}"# + ), + ) + .unwrap(); + fs::File::create(pet_dir.join("spritesheet.webp")) + .unwrap() + .write_all(b"not-used-by-loader") + .unwrap(); + } + + #[test] + fn picker_lists_bundled_and_installed_pets() { + let codex_home = tempfile::tempdir().unwrap(); + write_pet(codex_home.path(), "chefito", "Chefito"); + + let params = build_pet_picker_params(Some("chefito"), codex_home.path()); + + assert_eq!( + params + .items + .iter() + .map(|item| item.name.as_str()) + .collect::>(), + vec!["Boba", "Chefito", "Codex"], + ); + assert_eq!(params.initial_selected_idx, Some(1)); + } + + #[test] + fn picker_defaults_to_codex_when_no_pet_is_configured() { + let codex_home = tempfile::tempdir().unwrap(); + let params = build_pet_picker_params(/*current_pet*/ None, codex_home.path()); + + assert_eq!(params.initial_selected_idx, Some(1)); + assert_eq!(params.items[1].name, "Codex"); + assert!(params.items[1].is_current); + } +} diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index cce0bc38df..338c9804d2 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -48,6 +48,7 @@ pub enum SlashCommand { Title, Statusline, Theme, + Pets, Mcp, Apps, Plugins, @@ -98,6 +99,7 @@ impl SlashCommand { SlashCommand::Title => "configure which items appear in the terminal title", SlashCommand::Statusline => "configure which items appear in the status line", SlashCommand::Theme => "choose a syntax highlighting theme", + SlashCommand::Pets => "choose the terminal pet", SlashCommand::Ps => "list background terminals", SlashCommand::Stop => "stop all background terminals", SlashCommand::MemoryDrop => "DO NOT USE", @@ -151,6 +153,7 @@ impl SlashCommand { | SlashCommand::Keymap | SlashCommand::Mcp | SlashCommand::Raw + | SlashCommand::Pets | SlashCommand::Side | SlashCommand::Resume | SlashCommand::SandboxReadRoot @@ -222,7 +225,7 @@ impl SlashCommand { SlashCommand::Settings => true, SlashCommand::Collab => true, SlashCommand::Agent | SlashCommand::MultiAgents => true, - SlashCommand::Theme => false, + SlashCommand::Theme | SlashCommand::Pets => false, } } diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 06417e31ea..00d2850181 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -75,6 +75,14 @@ fn should_emit_notification(condition: NotificationCondition, terminal_focused: } } +impl Drop for Tui { + fn drop(&mut self) { + if let Err(err) = self.clear_ambient_pet_image() { + tracing::debug!(error = %err, "failed to clear ambient pet image on TUI drop"); + } + } +} + #[cfg(test)] mod tests { use std::io::Write as _; @@ -863,6 +871,21 @@ impl Tui { })? } + pub fn draw_ambient_pet_image( + &mut self, + request: Option, + ) -> Result<()> { + stdout().sync_update(|_| { + crate::pets::render_ambient_pet_image(self.terminal.backend_mut(), request) + .map_err(std::io::Error::other) + })? + } + + pub fn clear_ambient_pet_image(&mut self) -> Result<()> { + crate::pets::render_ambient_pet_image(self.terminal.backend_mut(), /*request*/ None) + .map_err(std::io::Error::other) + } + /// Draw a frame using the resize-reflow viewport and history insertion rules. /// /// This is the feature-gated counterpart to `draw`. It intentionally skips