Skip to main content

codex_cli/prompt_segment/
mod.rs

1use std::path::Path;
2
3use crate::auth;
4use crate::paths;
5use crate::rate_limits::cache;
6use nils_common::env as shared_env;
7
8mod lock;
9mod refresh;
10mod render;
11
12pub use render::CacheEntry;
13
14pub struct PromptSegmentOptions {
15    pub no_5h: bool,
16    pub ttl: Option<String>,
17    pub time_format: Option<String>,
18    pub show_timezone: bool,
19    pub refresh: bool,
20    pub is_enabled: bool,
21}
22
23const DEFAULT_TTL_SECONDS: u64 = 180;
24const DEFAULT_TIME_FORMAT: &str = "%m-%d %H:%M";
25const DEFAULT_TIME_FORMAT_WITH_TIMEZONE: &str = "%m-%d %H:%M %:z";
26
27pub fn run(options: &PromptSegmentOptions) -> i32 {
28    if options.is_enabled {
29        return if prompt_segment_enabled() { 0 } else { 1 };
30    }
31
32    let ttl_seconds = match resolve_ttl_seconds(options.ttl.as_deref()) {
33        Ok(value) => value,
34        Err(_) => {
35            print_ttl_usage();
36            return 2;
37        }
38    };
39
40    if !prompt_segment_enabled() {
41        return 0;
42    }
43
44    let target_file = match paths::resolve_auth_file() {
45        Some(path) => path,
46        None => return 0,
47    };
48
49    let show_5h =
50        shared_env::env_truthy_or("CODEX_PROMPT_SEGMENT_SHOW_5H_ENABLED", true) && !options.no_5h;
51    let time_format = match options.time_format.as_deref() {
52        Some(value) => value,
53        None if options.show_timezone => DEFAULT_TIME_FORMAT_WITH_TIMEZONE,
54        None => DEFAULT_TIME_FORMAT,
55    };
56    let stale_suffix = std::env::var("CODEX_PROMPT_SEGMENT_STALE_SUFFIX")
57        .unwrap_or_else(|_| " (stale)".to_string());
58
59    let prefix = resolve_name_prefix(&target_file);
60
61    if options.refresh {
62        if let Some(entry) = refresh::refresh_blocking(&target_file)
63            && let Some(line) = render::render_line(&entry, &prefix, show_5h, time_format)
64            && !line.trim().is_empty()
65        {
66            println!("{line}");
67        }
68        return 0;
69    }
70
71    let (cached, is_stale) = read_cached_entry(&target_file, ttl_seconds);
72    if let Some(entry) = cached.clone()
73        && let Some(mut line) = render::render_line(&entry, &prefix, show_5h, time_format)
74    {
75        if is_stale {
76            line.push_str(&stale_suffix);
77        }
78        if !line.trim().is_empty() {
79            println!("{line}");
80        }
81    }
82
83    if cached.is_none() || is_stale {
84        refresh::enqueue_background_refresh(&target_file);
85    }
86
87    0
88}
89
90fn prompt_segment_enabled() -> bool {
91    shared_env::env_truthy("CODEX_PROMPT_SEGMENT_ENABLED")
92}
93
94fn resolve_ttl_seconds(cli_ttl: Option<&str>) -> Result<u64, ()> {
95    if let Some(raw) = cli_ttl {
96        return shared_env::parse_duration_seconds(raw).ok_or(());
97    }
98
99    if let Ok(raw) = std::env::var("CODEX_PROMPT_SEGMENT_TTL")
100        && let Some(value) = shared_env::parse_duration_seconds(&raw)
101    {
102        return Ok(value);
103    }
104
105    Ok(DEFAULT_TTL_SECONDS)
106}
107
108fn print_ttl_usage() {
109    eprintln!("codex-cli prompt-segment: invalid --ttl");
110    eprintln!(
111        "usage: codex-cli prompt-segment [--no-5h] [--ttl <duration>] [--time-format <strftime>] [--show-timezone] [--refresh] [--is-enabled]"
112    );
113}
114
115fn read_cached_entry(target_file: &Path, ttl_seconds: u64) -> (Option<CacheEntry>, bool) {
116    let cache_file = match cache::cache_file_for_target(target_file) {
117        Ok(value) => value,
118        Err(_) => return (None, false),
119    };
120    if !cache_file.is_file() {
121        return (None, false);
122    }
123
124    let entry = render::read_cache_file(&cache_file);
125    let Some(entry) = entry else {
126        return (None, false);
127    };
128
129    let now_epoch = chrono::Utc::now().timestamp();
130    if now_epoch <= 0 || entry.fetched_at_epoch <= 0 {
131        return (Some(entry), true);
132    }
133
134    let ttl_i64 = i64::try_from(ttl_seconds).unwrap_or(i64::MAX);
135    let stale = now_epoch.saturating_sub(entry.fetched_at_epoch) > ttl_i64;
136    (Some(entry), stale)
137}
138
139fn resolve_name_prefix(target_file: &Path) -> String {
140    let name = resolve_name(target_file);
141    match name {
142        Some(value) if !value.trim().is_empty() => format!("{} ", value.trim()),
143        _ => String::new(),
144    }
145}
146
147fn resolve_name(target_file: &Path) -> Option<String> {
148    let name_source = std::env::var("CODEX_PROMPT_SEGMENT_NAME_SOURCE")
149        .ok()
150        .map(|value| value.to_ascii_lowercase())
151        .unwrap_or_else(|| "secret".to_string());
152
153    let show_fallback = shared_env::env_truthy("CODEX_PROMPT_SEGMENT_SHOW_FALLBACK_NAME_ENABLED");
154    let show_full_email = shared_env::env_truthy("CODEX_PROMPT_SEGMENT_SHOW_FULL_EMAIL_ENABLED");
155
156    if name_source == "email" {
157        if let Ok(Some(email)) = auth::email_from_auth_file(target_file) {
158            return Some(format_email_name(&email, show_full_email));
159        }
160        if show_fallback && let Ok(Some(identity)) = auth::identity_from_auth_file(target_file) {
161            return Some(format_email_name(&identity, show_full_email));
162        }
163        return None;
164    }
165
166    if let Some(secret_name) = cache::secret_name_for_target(target_file) {
167        return Some(secret_name);
168    }
169
170    if show_fallback && let Ok(Some(identity)) = auth::identity_from_auth_file(target_file) {
171        return Some(format_email_name(&identity, show_full_email));
172    }
173
174    None
175}
176
177fn format_email_name(raw: &str, show_full_email: bool) -> String {
178    let trimmed = raw.trim();
179    if show_full_email {
180        return trimmed.to_string();
181    }
182    trimmed.split('@').next().unwrap_or(trimmed).to_string()
183}