mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
## What changed - Add the disabled-by-default `tui_extensions_statusline` feature to render configured status items and their picker preview through a shared UI tree renderer. - Support text, HTTP(S) links, rows, spacers, dividers, and gaps with rendering budgets, text sanitization, and width-aware truncation that keeps the ellipsis outside hyperlinks. - Use plain text for the extension status line and hide the picker's theme color toggle while preserving the saved color preference. ## Testing Add coverage for tree serialization and URL validation, Unicode widths, hyperlink clipping, picker previews, color preference preservation, and token usage updates across renderer switches. GitOrigin-RevId: a6b87c2b1096765d61296d73e142834fcd53ceee
111 lines
3.2 KiB
Rust
111 lines
3.2 KiB
Rust
//! Bounded, display-only subset of the `codex-tui/1` tree contract.
|
|
//!
|
|
//! The wire tree is independent of its producer. Producers normalize text into leaf nodes and
|
|
//! arrays into composite nodes; unsupported primitives and properties fail deserialization. This
|
|
//! first slice renders one horizontal line of plain text, links, rows, spacers, dividers, and
|
|
//! numeric gaps. Links underline their text. Keys are preserved for future reconciliation.
|
|
|
|
mod render;
|
|
|
|
#[cfg(test)]
|
|
#[path = "ui_tree_tests.rs"]
|
|
mod tests;
|
|
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
/// A single display node, or no content.
|
|
pub(crate) type UiTree = Option<UiNode>;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "kebab-case", deny_unknown_fields)]
|
|
pub(crate) enum UiNode {
|
|
Text {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
key: Option<Key>,
|
|
props: TextProps,
|
|
},
|
|
Link {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
key: Option<Key>,
|
|
props: LinkProps,
|
|
children: Vec<UiTree>,
|
|
},
|
|
Row {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
key: Option<Key>,
|
|
props: StackProps,
|
|
children: Vec<UiTree>,
|
|
},
|
|
Spacer {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
key: Option<Key>,
|
|
},
|
|
Divider {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
key: Option<Key>,
|
|
},
|
|
}
|
|
|
|
impl UiNode {
|
|
pub(crate) fn text(key: Option<Key>, text: impl Into<String>) -> Self {
|
|
Self::Text {
|
|
key,
|
|
props: TextProps { text: text.into() },
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(crate) struct LinkProps {
|
|
pub(crate) href: LinkHref,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub(crate) title: Option<String>,
|
|
}
|
|
|
|
/// An absolute HTTP(S) destination safe to embed in a terminal hyperlink.
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[serde(try_from = "String")]
|
|
pub(crate) struct LinkHref(String);
|
|
|
|
impl TryFrom<String> for LinkHref {
|
|
type Error = &'static str;
|
|
|
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
|
Self::try_from(value.as_str())
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&str> for LinkHref {
|
|
type Error = &'static str;
|
|
|
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
crate::terminal_hyperlinks::web_destination(value)
|
|
.filter(|destination| destination == value)
|
|
.map(Self)
|
|
.ok_or("link href must be an absolute HTTP(S) URL without control characters")
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub(crate) enum Key {
|
|
String(String),
|
|
Number(serde_json::Number),
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(crate) struct TextProps {
|
|
pub(crate) text: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(crate) struct StackProps {
|
|
/// Numeric spacing uses the profile's four-point scale.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub(crate) gap: Option<f64>,
|
|
}
|