mirror of
https://github.com/openai/codex.git
synced 2026-05-16 01:02:48 +00:00
Compare commits
2 Commits
docs/updat
...
fix-unicod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95c5b9ef6b | ||
|
|
c6a470f573 |
@@ -34,6 +34,8 @@ use crate::config_types::HistoryPersistence;
|
|||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
/// Filename that stores the message history inside `~/.codex`.
|
/// Filename that stores the message history inside `~/.codex`.
|
||||||
const HISTORY_FILENAME: &str = "history.jsonl";
|
const HISTORY_FILENAME: &str = "history.jsonl";
|
||||||
@@ -125,18 +127,17 @@ pub(crate) async fn append_entry(text: &str, session_id: &Uuid, config: &Config)
|
|||||||
/// times if the lock is currently held by another process. This prevents a
|
/// times if the lock is currently held by another process. This prevents a
|
||||||
/// potential indefinite wait while still giving other writers some time to
|
/// potential indefinite wait while still giving other writers some time to
|
||||||
/// finish their operation.
|
/// finish their operation.
|
||||||
|
#[cfg(unix)]
|
||||||
async fn acquire_exclusive_lock_with_retry(file: &File) -> Result<()> {
|
async fn acquire_exclusive_lock_with_retry(file: &File) -> Result<()> {
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
for _ in 0..MAX_RETRIES {
|
for _ in 0..MAX_RETRIES {
|
||||||
match file.try_lock() {
|
match try_flock_exclusive(file) {
|
||||||
Ok(()) => return Ok(()),
|
Ok(()) => return Ok(()),
|
||||||
Err(e) => match e {
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
std::fs::TryLockError::WouldBlock => {
|
sleep(RETRY_SLEEP).await;
|
||||||
sleep(RETRY_SLEEP).await;
|
}
|
||||||
}
|
Err(e) => return Err(e),
|
||||||
other => return Err(other.into()),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +147,12 @@ async fn acquire_exclusive_lock_with_retry(file: &File) -> Result<()> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
async fn acquire_exclusive_lock_with_retry(_file: &File) -> Result<()> {
|
||||||
|
// On non-Unix, skip locking; appends are still atomic with O_APPEND.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Asynchronously fetch the history file's *identifier* (inode on Unix) and
|
/// Asynchronously fetch the history file's *identifier* (inode on Unix) and
|
||||||
/// the current number of entries by counting newline characters.
|
/// the current number of entries by counting newline characters.
|
||||||
pub(crate) async fn history_metadata(config: &Config) -> (u64, usize) {
|
pub(crate) async fn history_metadata(config: &Config) -> (u64, usize) {
|
||||||
@@ -261,14 +268,12 @@ pub(crate) fn lookup(log_id: u64, offset: usize, config: &Config) -> Option<Hist
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn acquire_shared_lock_with_retry(file: &File) -> Result<()> {
|
fn acquire_shared_lock_with_retry(file: &File) -> Result<()> {
|
||||||
for _ in 0..MAX_RETRIES {
|
for _ in 0..MAX_RETRIES {
|
||||||
match file.try_lock_shared() {
|
match try_flock_shared(file) {
|
||||||
Ok(()) => return Ok(()),
|
Ok(()) => return Ok(()),
|
||||||
Err(e) => match e {
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
std::fs::TryLockError::WouldBlock => {
|
std::thread::sleep(RETRY_SLEEP);
|
||||||
std::thread::sleep(RETRY_SLEEP);
|
}
|
||||||
}
|
Err(e) => return Err(e),
|
||||||
other => return Err(other.into()),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +283,45 @@ fn acquire_shared_lock_with_retry(file: &File) -> Result<()> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn acquire_shared_lock_with_retry(_file: &File) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn try_flock_exclusive(file: &File) -> Result<()> {
|
||||||
|
let fd = file.as_raw_fd();
|
||||||
|
let rc = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
|
||||||
|
if rc == 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let err = std::io::Error::last_os_error();
|
||||||
|
match err.raw_os_error() {
|
||||||
|
Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN => Err(
|
||||||
|
std::io::Error::new(std::io::ErrorKind::WouldBlock, "lock would block"),
|
||||||
|
),
|
||||||
|
_ => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn try_flock_shared(file: &File) -> Result<()> {
|
||||||
|
let fd = file.as_raw_fd();
|
||||||
|
let rc = unsafe { libc::flock(fd, libc::LOCK_SH | libc::LOCK_NB) };
|
||||||
|
if rc == 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let err = std::io::Error::last_os_error();
|
||||||
|
match err.raw_os_error() {
|
||||||
|
Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN => Err(
|
||||||
|
std::io::Error::new(std::io::ErrorKind::WouldBlock, "lock would block"),
|
||||||
|
),
|
||||||
|
_ => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// On Unix systems ensure the file permissions are `0o600` (rw-------). If the
|
/// On Unix systems ensure the file permissions are `0o600` (rw-------). If the
|
||||||
/// permissions cannot be changed the error is propagated to the caller.
|
/// permissions cannot be changed the error is propagated to the caller.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
|||||||
61
codex-rs/tui/src/emoji_width.rs
Normal file
61
codex-rs/tui/src/emoji_width.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use ratatui::crossterm::execute;
|
||||||
|
use ratatui::crossterm::style::Print;
|
||||||
|
use ratatui::crossterm::terminal::Clear;
|
||||||
|
use ratatui::crossterm::terminal::ClearType;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
static EMOJI_OK: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
/// Returns true if common emoji we use advance the cursor by the same number of
|
||||||
|
/// columns as reported by `unicode-width` on this terminal. The value is cached.
|
||||||
|
pub fn emojis_render_as_expected() -> bool {
|
||||||
|
// Optimistic default: render emoji unless we have measured otherwise.
|
||||||
|
EMOJI_OK.get().copied().unwrap_or(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that emoji width has been probed and cached. Call during TUI init.
|
||||||
|
pub fn ensure_probed() {
|
||||||
|
let _ = EMOJI_OK.get_or_init(detect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a small runtime probe by printing a few glyphs at (0,0) and reading the
|
||||||
|
/// cursor position. If any measured width differs from `unicode-width`, we
|
||||||
|
/// conclude emoji rendering is unreliable and the UI should fall back to ASCII.
|
||||||
|
pub fn detect() -> bool {
|
||||||
|
// Only probe a small curated set that we actually use in the UI.
|
||||||
|
const TESTS: &[&str] = &["📂", "📖", "🔎", "🧪", "⚡", "⚙︎", "✏︎", "✓", "✗", "🖐"];
|
||||||
|
|
||||||
|
let mut out = std::io::stdout();
|
||||||
|
// Best effort: on error, default to false (use ASCII) to avoid broken layout.
|
||||||
|
let _ = execute!(out, Clear(ClearType::All));
|
||||||
|
for s in TESTS {
|
||||||
|
let expected = s.width();
|
||||||
|
// Move to origin, print, flush, read cursor position.
|
||||||
|
if execute!(out, ratatui::crossterm::cursor::MoveTo(0, 0), Print(*s)).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if out.flush().is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Ok((x, _y)) = ratatui::crossterm::cursor::position() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if x as usize != expected {
|
||||||
|
// Clear the line before returning.
|
||||||
|
let _ = execute!(
|
||||||
|
out,
|
||||||
|
ratatui::crossterm::cursor::MoveTo(0, 0),
|
||||||
|
Clear(ClearType::CurrentLine)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = execute!(
|
||||||
|
out,
|
||||||
|
ratatui::crossterm::cursor::MoveTo(0, 0),
|
||||||
|
Clear(ClearType::All)
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
@@ -244,7 +244,7 @@ fn new_parsed_command(
|
|||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
match output {
|
match output {
|
||||||
None => {
|
None => {
|
||||||
let mut spans = vec!["⚙︎ Working".magenta().bold()];
|
let mut spans = vec![crate::icons::working().magenta().bold()];
|
||||||
if let Some(st) = start_time {
|
if let Some(st) = start_time {
|
||||||
let dur = exec_duration(st);
|
let dur = exec_duration(st);
|
||||||
spans.push(format!(" • {dur}").dim());
|
spans.push(format!(" • {dur}").dim());
|
||||||
@@ -252,11 +252,14 @@ fn new_parsed_command(
|
|||||||
lines.push(Line::from(spans));
|
lines.push(Line::from(spans));
|
||||||
}
|
}
|
||||||
Some(o) if o.exit_code == 0 => {
|
Some(o) if o.exit_code == 0 => {
|
||||||
lines.push(Line::from(vec!["✓".green(), " Completed".into()]));
|
lines.push(Line::from(vec![
|
||||||
|
crate::icons::completed_label().green(),
|
||||||
|
" Completed".into(),
|
||||||
|
]));
|
||||||
}
|
}
|
||||||
Some(o) => {
|
Some(o) => {
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
"✗".red(),
|
crate::icons::failed_label().red(),
|
||||||
format!(" Failed (exit {})", o.exit_code).into(),
|
format!(" Failed (exit {})", o.exit_code).into(),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
@@ -282,22 +285,22 @@ fn new_parsed_command(
|
|||||||
|
|
||||||
for (i, parsed) in parsed_commands.iter().enumerate() {
|
for (i, parsed) in parsed_commands.iter().enumerate() {
|
||||||
let text = match parsed {
|
let text = match parsed {
|
||||||
ParsedCommand::Read { name, .. } => format!("📖 {name}"),
|
ParsedCommand::Read { name, .. } => format!("{} {name}", crate::icons::book()),
|
||||||
ParsedCommand::ListFiles { cmd, path } => match path {
|
ParsedCommand::ListFiles { cmd, path } => match path {
|
||||||
Some(p) => format!("📂 {p}"),
|
Some(p) => format!("{} {p}", crate::icons::folder()),
|
||||||
None => format!("📂 {cmd}"),
|
None => format!("{} {cmd}", crate::icons::folder()),
|
||||||
},
|
},
|
||||||
ParsedCommand::Search { query, path, cmd } => match (query, path) {
|
ParsedCommand::Search { query, path, cmd } => match (query, path) {
|
||||||
(Some(q), Some(p)) => format!("🔎 {q} in {p}"),
|
(Some(q), Some(p)) => format!("{} {q} in {p}", crate::icons::search()),
|
||||||
(Some(q), None) => format!("🔎 {q}"),
|
(Some(q), None) => format!("{} {q}", crate::icons::search()),
|
||||||
(None, Some(p)) => format!("🔎 {p}"),
|
(None, Some(p)) => format!("{} {p}", crate::icons::search()),
|
||||||
(None, None) => format!("🔎 {cmd}"),
|
(None, None) => format!("{} {cmd}", crate::icons::search()),
|
||||||
},
|
},
|
||||||
ParsedCommand::Format { .. } => "✨ Formatting".to_string(),
|
ParsedCommand::Format { .. } => format!("{} Formatting", crate::icons::formatting()),
|
||||||
ParsedCommand::Test { cmd } => format!("🧪 {cmd}"),
|
ParsedCommand::Test { cmd } => format!("{} {cmd}", crate::icons::test()),
|
||||||
ParsedCommand::Lint { cmd, .. } => format!("🧹 {cmd}"),
|
ParsedCommand::Lint { cmd, .. } => format!("{} {cmd}", crate::icons::lint()),
|
||||||
ParsedCommand::Unknown { cmd } => format!("⌨️ {cmd}"),
|
ParsedCommand::Unknown { cmd } => format!("{} {cmd}", crate::icons::keyboard_cmd()),
|
||||||
ParsedCommand::Noop { cmd } => format!("🔄 {cmd}"),
|
ParsedCommand::Noop { cmd } => format!("{} {cmd}", crate::icons::noop()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let first_prefix = if i == 0 { " └ " } else { " " };
|
let first_prefix = if i == 0 { " └ " } else { " " };
|
||||||
@@ -325,7 +328,7 @@ fn new_exec_command_generic(
|
|||||||
let command_escaped = strip_bash_lc_and_escape(command);
|
let command_escaped = strip_bash_lc_and_escape(command);
|
||||||
let mut cmd_lines = command_escaped.lines();
|
let mut cmd_lines = command_escaped.lines();
|
||||||
if let Some(first) = cmd_lines.next() {
|
if let Some(first) = cmd_lines.next() {
|
||||||
let mut spans: Vec<Span> = vec!["⚡ Running".magenta()];
|
let mut spans: Vec<Span> = vec![crate::icons::running().magenta()];
|
||||||
if let Some(st) = start_time {
|
if let Some(st) = start_time {
|
||||||
let dur = exec_duration(st);
|
let dur = exec_duration(st);
|
||||||
spans.push(format!(" • {dur}").dim());
|
spans.push(format!(" • {dur}").dim());
|
||||||
@@ -514,7 +517,11 @@ pub(crate) fn new_status_output(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 📂 Workspace
|
// 📂 Workspace
|
||||||
lines.push(Line::from(vec!["📂 ".into(), "Workspace".bold()]));
|
lines.push(Line::from(vec![
|
||||||
|
crate::icons::workspace().into(),
|
||||||
|
" ".into(),
|
||||||
|
"Workspace".bold(),
|
||||||
|
]));
|
||||||
// Path (home-relative, e.g., ~/code/project)
|
// Path (home-relative, e.g., ~/code/project)
|
||||||
let cwd_str = match relativize_to_home(&config.cwd) {
|
let cwd_str = match relativize_to_home(&config.cwd) {
|
||||||
Some(rel) if !rel.as_os_str().is_empty() => format!("~/{}", rel.display()),
|
Some(rel) if !rel.as_os_str().is_empty() => format!("~/{}", rel.display()),
|
||||||
@@ -545,7 +552,11 @@ pub(crate) fn new_status_output(
|
|||||||
if let Ok(auth) = try_read_auth_json(&auth_file)
|
if let Ok(auth) = try_read_auth_json(&auth_file)
|
||||||
&& let Some(tokens) = auth.tokens.clone()
|
&& let Some(tokens) = auth.tokens.clone()
|
||||||
{
|
{
|
||||||
lines.push(Line::from(vec!["👤 ".into(), "Account".bold()]));
|
lines.push(Line::from(vec![
|
||||||
|
crate::icons::account().into(),
|
||||||
|
" ".into(),
|
||||||
|
"Account".bold(),
|
||||||
|
]));
|
||||||
lines.push(Line::from(" • Signed in with ChatGPT"));
|
lines.push(Line::from(" • Signed in with ChatGPT"));
|
||||||
|
|
||||||
let info = tokens.id_token;
|
let info = tokens.id_token;
|
||||||
@@ -572,7 +583,11 @@ pub(crate) fn new_status_output(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🧠 Model
|
// 🧠 Model
|
||||||
lines.push(Line::from(vec!["🧠 ".into(), "Model".bold()]));
|
lines.push(Line::from(vec![
|
||||||
|
crate::icons::model().into(),
|
||||||
|
" ".into(),
|
||||||
|
"Model".bold(),
|
||||||
|
]));
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
" • Name: ".into(),
|
" • Name: ".into(),
|
||||||
config.model.clone().into(),
|
config.model.clone().into(),
|
||||||
@@ -601,7 +616,11 @@ pub(crate) fn new_status_output(
|
|||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
|
|
||||||
// 📊 Token Usage
|
// 📊 Token Usage
|
||||||
lines.push(Line::from(vec!["📊 ".into(), "Token Usage".bold()]));
|
lines.push(Line::from(vec![
|
||||||
|
crate::icons::token_usage().into(),
|
||||||
|
" ".into(),
|
||||||
|
"Token Usage".bold(),
|
||||||
|
]));
|
||||||
if let Some(session_id) = session_id {
|
if let Some(session_id) = session_id {
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
" • Session ID: ".into(),
|
" • Session ID: ".into(),
|
||||||
@@ -715,7 +734,14 @@ pub(crate) fn new_mcp_tools_output(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
|
pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
|
||||||
let lines: Vec<Line<'static>> = vec![vec!["🖐 ".red().bold(), message.into()].into(), "".into()];
|
let lines: Vec<Line<'static>> = vec![
|
||||||
|
vec![
|
||||||
|
format!("{} ", crate::icons::wave_error()).red().bold(),
|
||||||
|
message.into(),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
"".into(),
|
||||||
|
];
|
||||||
PlainHistoryCell { lines }
|
PlainHistoryCell { lines }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,7 +766,7 @@ pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlainHistoryCell {
|
|||||||
let empty = width.saturating_sub(filled);
|
let empty = width.saturating_sub(filled);
|
||||||
|
|
||||||
let mut header: Vec<Span> = Vec::new();
|
let mut header: Vec<Span> = Vec::new();
|
||||||
header.push(Span::raw("📋"));
|
header.push(Span::raw(crate::icons::clipboard()));
|
||||||
header.push(Span::styled(
|
header.push(Span::styled(
|
||||||
" Update plan",
|
" Update plan",
|
||||||
Style::default().add_modifier(Modifier::BOLD).magenta(),
|
Style::default().add_modifier(Modifier::BOLD).magenta(),
|
||||||
@@ -830,12 +856,12 @@ pub(crate) fn new_patch_event(
|
|||||||
PatchEventType::ApprovalRequest => "proposed patch",
|
PatchEventType::ApprovalRequest => "proposed patch",
|
||||||
PatchEventType::ApplyBegin {
|
PatchEventType::ApplyBegin {
|
||||||
auto_approved: true,
|
auto_approved: true,
|
||||||
} => "✏️ Applying patch",
|
} => crate::icons::apply_patch(),
|
||||||
PatchEventType::ApplyBegin {
|
PatchEventType::ApplyBegin {
|
||||||
auto_approved: false,
|
auto_approved: false,
|
||||||
} => {
|
} => {
|
||||||
let lines: Vec<Line<'static>> = vec![
|
let lines: Vec<Line<'static>> = vec![
|
||||||
Line::from("✏️ Applying patch".magenta().bold()),
|
Line::from(crate::icons::apply_patch().magenta().bold()),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
];
|
];
|
||||||
return PlainHistoryCell { lines };
|
return PlainHistoryCell { lines };
|
||||||
|
|||||||
156
codex-rs/tui/src/icons.rs
Normal file
156
codex-rs/tui/src/icons.rs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/// Icons used throughout the TUI. We fall back to ASCII-safe variants
|
||||||
|
/// when runtime detection shows emoji cell widths are inconsistent with
|
||||||
|
/// what we use for wrapping.
|
||||||
|
use crate::emoji_width::emojis_render_as_expected;
|
||||||
|
|
||||||
|
pub fn running() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"⚡ Running"
|
||||||
|
} else {
|
||||||
|
"> Running"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn working() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"⚙︎ Working"
|
||||||
|
} else {
|
||||||
|
"* Working"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn completed_label() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"✓"
|
||||||
|
} else {
|
||||||
|
"OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn failed_label() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"✗"
|
||||||
|
} else {
|
||||||
|
"X"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn folder() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"📂"
|
||||||
|
} else {
|
||||||
|
"[dir]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn book() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"📖"
|
||||||
|
} else {
|
||||||
|
"[read]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"🔎"
|
||||||
|
} else {
|
||||||
|
"[find]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn formatting() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"✨"
|
||||||
|
} else {
|
||||||
|
"[fmt]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"🧪"
|
||||||
|
} else {
|
||||||
|
"[test]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lint() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"🧹"
|
||||||
|
} else {
|
||||||
|
"[lint]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keyboard_cmd() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"⌨︎"
|
||||||
|
} else {
|
||||||
|
"[cmd]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn noop() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"🔄"
|
||||||
|
} else {
|
||||||
|
"[noop]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clipboard() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"📋"
|
||||||
|
} else {
|
||||||
|
"[plan]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_patch() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"✏︎ Applying patch"
|
||||||
|
} else {
|
||||||
|
"Applying patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn workspace() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"📂"
|
||||||
|
} else {
|
||||||
|
"[dir]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn account() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"👤"
|
||||||
|
} else {
|
||||||
|
"[acct]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn model() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"🧠"
|
||||||
|
} else {
|
||||||
|
"[model]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token_usage() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"📊"
|
||||||
|
} else {
|
||||||
|
"[tokens]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wave_error() -> &'static str {
|
||||||
|
if emojis_render_as_expected() {
|
||||||
|
"🖐"
|
||||||
|
} else {
|
||||||
|
"[!]"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,10 +33,12 @@ mod cli;
|
|||||||
mod common;
|
mod common;
|
||||||
pub mod custom_terminal;
|
pub mod custom_terminal;
|
||||||
mod diff_render;
|
mod diff_render;
|
||||||
|
mod emoji_width;
|
||||||
mod exec_command;
|
mod exec_command;
|
||||||
mod file_search;
|
mod file_search;
|
||||||
mod get_git_diff;
|
mod get_git_diff;
|
||||||
mod history_cell;
|
mod history_cell;
|
||||||
|
mod icons;
|
||||||
pub mod insert_history;
|
pub mod insert_history;
|
||||||
pub mod live_wrap;
|
pub mod live_wrap;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use ratatui::text::Line;
|
|||||||
|
|
||||||
use crate::custom_terminal;
|
use crate::custom_terminal;
|
||||||
use crate::custom_terminal::Terminal as CustomTerminal;
|
use crate::custom_terminal::Terminal as CustomTerminal;
|
||||||
|
use crate::emoji_width;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tokio_stream::Stream;
|
use tokio_stream::Stream;
|
||||||
|
|
||||||
@@ -74,6 +75,10 @@ pub fn init() -> Result<Terminal> {
|
|||||||
// Clear screen and move cursor to top-left before drawing UI
|
// Clear screen and move cursor to top-left before drawing UI
|
||||||
execute!(stdout(), Clear(ClearType::All), MoveTo(0, 0))?;
|
execute!(stdout(), Clear(ClearType::All), MoveTo(0, 0))?;
|
||||||
|
|
||||||
|
// Proactively probe emoji widths so downstream rendering can pick icons
|
||||||
|
// safely without doing terminal mutations mid-render.
|
||||||
|
emoji_width::ensure_probed();
|
||||||
|
|
||||||
let backend = CrosstermBackend::new(stdout());
|
let backend = CrosstermBackend::new(stdout());
|
||||||
let tui = CustomTerminal::with_options(backend)?;
|
let tui = CustomTerminal::with_options(backend)?;
|
||||||
Ok(tui)
|
Ok(tui)
|
||||||
|
|||||||
Reference in New Issue
Block a user