use std::borrow::Cow;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct StatusContext {
pub tool: Tool,
pub model: Option<ModelInfo>,
pub workspace: Option<WorkspaceInfo>,
pub context_window: Option<ContextWindow>,
pub cost: Option<CostMetrics>,
pub effort: Option<EffortLevel>,
pub vim: Option<VimMode>,
pub output_style: Option<OutputStyle>,
pub agent_name: Option<String>,
pub version: Option<String>,
pub raw: Arc<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Tool {
ClaudeCode,
QwenCode,
CodexCli,
CopilotCli,
Other(Cow<'static, str>),
}
impl std::fmt::Display for Tool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ClaudeCode => f.write_str("claude"),
Self::QwenCode => f.write_str("qwen"),
Self::CodexCli => f.write_str("codex"),
Self::CopilotCli => f.write_str("copilot"),
Self::Other(name) => f.write_str(name),
}
}
}
#[derive(Debug, Clone)]
pub struct ModelInfo {
pub display_name: String,
}
#[derive(Debug, Clone)]
pub struct WorkspaceInfo {
pub project_dir: PathBuf,
pub git_worktree: Option<GitWorktree>,
}
#[derive(Debug, Clone)]
pub struct GitWorktree {
pub name: String,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct ContextWindow {
pub used: Option<Percent>,
pub size: Option<u32>,
pub total_input_tokens: Option<u64>,
pub total_output_tokens: Option<u64>,
pub current_usage: Option<TurnUsage>,
}
impl ContextWindow {
#[must_use]
pub fn remaining(&self) -> Option<Percent> {
self.used.map(Percent::complement)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct TurnUsage {
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_creation_input_tokens: u64,
pub cache_read_input_tokens: u64,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct CostMetrics {
pub total_cost_usd: Option<f64>,
pub total_duration_ms: Option<u64>,
pub total_api_duration_ms: Option<u64>,
pub total_lines_added: Option<u64>,
pub total_lines_removed: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EffortLevel {
Low,
Medium,
High,
Max,
XHigh,
}
impl EffortLevel {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Max => "max",
Self::XHigh => "xhigh",
}
}
}
impl std::str::FromStr for EffortLevel {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"low" => Ok(Self::Low),
"medium" => Ok(Self::Medium),
"high" => Ok(Self::High),
"max" => Ok(Self::Max),
"xhigh" => Ok(Self::XHigh),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum VimMode {
Normal,
Insert,
Visual,
Command,
Replace,
}
impl VimMode {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Normal => "normal",
Self::Insert => "insert",
Self::Visual => "visual",
Self::Command => "command",
Self::Replace => "replace",
}
}
}
impl std::str::FromStr for VimMode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(Self::Normal),
"insert" => Ok(Self::Insert),
"visual" => Ok(Self::Visual),
"command" => Ok(Self::Command),
"replace" => Ok(Self::Replace),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct OutputStyle {
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, serde::Serialize)]
pub struct Percent(f32);
impl Percent {
#[must_use]
pub fn new(value: f32) -> Option<Self> {
if (0.0..=100.0).contains(&value) {
Some(Self(value))
} else {
None
}
}
#[must_use]
pub fn from_f64(value: f64) -> Option<Self> {
if (0.0..=100.0).contains(&value) {
Some(Self(value as f32))
} else {
None
}
}
#[must_use]
pub fn from_f64_clamped(value: f64) -> Option<Self> {
if value.is_nan() {
return None;
}
Some(Self(value.clamp(0.0, 100.0) as f32))
}
#[must_use]
pub fn value(self) -> f32 {
self.0
}
#[must_use]
pub fn complement(self) -> Self {
Self(100.0 - self.0)
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ParseOpts {
pub tool: Option<Tool>,
}
impl ParseOpts {
#[must_use]
pub fn with_tool(mut self, tool: Tool) -> Self {
self.tool = Some(tool);
self
}
}
pub fn parse(input: &[u8]) -> Result<StatusContext, ParseError> {
parse_with_opts(input, &ParseOpts::default())
}
pub fn parse_with_opts(input: &[u8], opts: &ParseOpts) -> Result<StatusContext, ParseError> {
let raw_value: serde_json::Value =
serde_json::from_slice(input).map_err(|err| ParseError::InvalidJson {
message: err.to_string(),
location: (err.line() > 0).then(|| SourcePos {
line: err.line(),
column: err.column(),
}),
})?;
let raw = Arc::new(raw_value);
normalizers::dispatch(raw, opts)
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ParseError {
InvalidJson {
message: String,
location: Option<SourcePos>,
},
MissingField {
tool: Tool,
path: String,
},
TypeMismatch {
tool: Tool,
path: String,
expected: JsonType,
got: JsonType,
},
InvalidValue {
tool: Tool,
path: String,
reason: &'static str,
},
NormalizerError {
tool: Tool,
message: String,
},
}
#[derive(Debug, Clone, Copy)]
pub struct SourcePos {
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JsonType {
Object,
Array,
String,
Number,
Bool,
Null,
}
impl JsonType {
#[must_use]
pub fn of(value: &serde_json::Value) -> Self {
match value {
serde_json::Value::Object(_) => Self::Object,
serde_json::Value::Array(_) => Self::Array,
serde_json::Value::String(_) => Self::String,
serde_json::Value::Number(_) => Self::Number,
serde_json::Value::Bool(_) => Self::Bool,
serde_json::Value::Null => Self::Null,
}
}
}
impl std::fmt::Display for JsonType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::Object => "object",
Self::Array => "array",
Self::String => "string",
Self::Number => "number",
Self::Bool => "bool",
Self::Null => "null",
};
f.write_str(name)
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidJson { message, location } => match location {
Some(pos) => write!(f, "invalid JSON at {}:{}: {message}", pos.line, pos.column),
None => write!(f, "invalid JSON: {message}"),
},
Self::MissingField { tool, path } => {
write!(f, "missing field {} for {tool}", display_path(path))
}
Self::TypeMismatch {
tool,
path,
expected,
got,
} => {
write!(
f,
"type mismatch at {} for {tool}: expected {expected}, got {got}",
display_path(path)
)
}
Self::InvalidValue { tool, path, reason } => {
write!(
f,
"invalid value at {} for {tool}: {reason}",
display_path(path)
)
}
Self::NormalizerError { tool, message } => {
write!(f, "normalizer error for {tool}: {message}")
}
}
}
}
fn display_path(path: &str) -> String {
if path.is_empty() {
"<root>".to_string()
} else {
format!("{path:?}")
}
}
impl std::error::Error for ParseError {}
mod normalizers;
#[cfg(test)]
mod tests;