use std::env;
use std::io::{self, Write};
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
Terminal,
};
use xei_core::App;
mod event;
mod gfx;
mod gpu_frame;
mod kitty_gfx;
mod term_caps;
mod ui;
fn main() -> io::Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
match args[1].as_str() {
"--version" | "-V" => {
println!("xei {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
"--help" | "-h" => {
println!("xei (晴) — a modern Vim-like terminal editor\n");
println!("Usage: xei [FILE] Open a file for editing");
println!(" xei --version Print version");
println!(" xei --help Show this help");
println!(" xei --debug FILE Open with debug logging\n");
println!("Homepage: https://github.com/stremtec/xei");
return Ok(());
}
_ => {}
}
}
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
gpu_frame::force_end_sync();
let _ = disable_raw_mode();
let _ = execute!(
io::stdout(),
crossterm::event::PopKeyboardEnhancementFlags,
LeaveAlternateScreen,
DisableMouseCapture
);
prev_hook(info);
}));
let mut app = if args.len() > 1 && !args[1].starts_with('-') {
App::open_file(&args[1])
} else if args.len() > 2 && args[1] == "--debug" {
let mut a = App::open_file(&args[2]);
a.debug = true;
a
} else {
let mut a = App::new();
a.restore_session();
a
};
if let Some(saved_theme) = xei_core::config::load_theme() {
if let Some(t) = xei_core::theme::find(&saved_theme) {
app.theme = t;
}
}
let caps = term_caps::TerminalCaps::detect();
app.cell_px = caps.cell_px;
app.cell_px_h = caps.cell_px_h;
app.set_term_caps(
caps.summary(),
caps.sync_output,
caps.undercurl,
caps.underline_color,
caps.hyperlinks,
caps.modern,
caps.kitty_graphics,
);
{
let cfg = xei_core::config::load();
app.apply_pet_from_config(&cfg);
if cfg.update_check {
app.update.start_check(env!("CARGO_PKG_VERSION"));
}
}
xei_core::set_cursor_esc(app.theme.cursor);
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let kbd_enhanced =
crossterm::terminal::supports_keyboard_enhancement().unwrap_or(false);
if kbd_enhanced {
let _ = execute!(
stdout,
crossterm::event::PushKeyboardEnhancementFlags(
crossterm::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
)
);
} else {
app.message =
"⌨ legacy terminal: Ctrl+Shift/Ctrl+,/Ctrl+. unavailable — use SPC leader · :settings · :help"
.into();
}
gpu_frame::reset_underline_sgr_stdout();
term_caps::drain_input_noise();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
if app.gpu_acc && caps.modern {
app.message = format!("xei · {}", caps.summary());
}
let result = run_app(&mut terminal, &mut app, caps);
gpu_frame::force_end_sync();
disable_raw_mode()?;
if kbd_enhanced {
let _ = execute!(
terminal.backend_mut(),
crossterm::event::PopKeyboardEnhancementFlags
);
}
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
let _ = std::panic::take_hook();
result
}
fn paint_pet(
app: &mut App,
caps: &term_caps::TerminalCaps,
pet_placed: &mut bool,
last_frame_idx: &mut usize,
last_pos: &mut (u16, u16),
editor_cursor: Option<(u16, u16)>,
) -> bool {
let cell_px: u32 = if caps.cell_px >= 4 { caps.cell_px } else { 14 };
let ok = app.pet.enabled
&& app.pet.has_frames()
&& app.pet_graphics_ok()
&& kitty_gfx::available(app.gpu_acc && app.gpu_graphics, caps);
if !ok {
if *pet_placed && kitty_gfx::available(app.gpu_acc && app.gpu_graphics, caps) {
let mut out = io::stdout();
let _ = kitty_gfx::delete_image_flush(&mut out, app.pet.image_id);
}
*pet_placed = false;
*last_frame_idx = usize::MAX;
*last_pos = (u16::MAX, u16::MAX);
if app.pet.enabled && !app.pet_graphics_ok() {
app.pet.enabled = false;
}
return false;
}
app.pet.ensure_display_cache(cell_px);
let frame_changed = app.pet.tick();
let idx = app.pet.frame_idx();
let (sx, sy) = app.pet_screen_xy();
let pos = (sx, sy);
let pos_changed = pos != *last_pos;
let need_paint = frame_changed || !*pet_placed || idx != *last_frame_idx || pos_changed;
if !need_paint {
return false;
}
let Some(disp) = app.pet.current_display() else {
return false;
};
if disp.width == 0 || disp.height == 0 {
return false;
}
let use_sync = gpu_frame::should_sync(app.gpu_acc, caps);
let mut out = io::stdout();
if kitty_gfx::place_rgba_rect_b64(
&mut out,
app.pet.image_id,
disp.width,
disp.height,
Some(&disp.b64),
&disp.rgba,
sx as u32,
sy as u32,
editor_cursor,
use_sync,
)
.is_ok()
{
*pet_placed = true;
*last_frame_idx = idx;
*last_pos = pos;
return true;
}
false
}
fn paint_media_preview(
app: &mut App,
caps: &term_caps::TerminalCaps,
editor_cursor: Option<(u16, u16)>,
last_sig: &mut Option<(u32, u16, u32, u32)>,
) -> bool {
let show = app.mode == xei_core::Mode::Preview
&& app.preview.open
&& !app.preview.closing
&& matches!(app.preview.kind, Some(xei_core::PreviewKind::Image))
&& app.preview_image.is_some()
&& kitty_gfx::available(app.gpu_acc && app.gpu_graphics, caps);
if !show {
if last_sig.is_some() && kitty_gfx::available(app.gpu_acc && app.gpu_graphics, caps) {
if let Some((id, _, _, _)) = *last_sig {
let mut out = io::stdout();
let _ = kitty_gfx::delete_image_flush(&mut out, id);
}
}
*last_sig = None;
return false;
}
let Some(img) = app.preview_image.as_ref() else {
return false;
};
if img.cached_w == 0 || img.cached_h == 0 || img.cached_b64.is_empty() {
return false;
}
let sig = (img.kitty_id, img.width_cells, img.cached_w, img.cached_h);
if *last_sig == Some(sig) {
return false; }
let col = (app.viewport.x.saturating_add(6)) as u32;
let row = (app.viewport.y.saturating_add(3)) as u32;
let mut out = io::stdout();
let use_sync = gpu_frame::should_sync(app.gpu_acc, caps);
let ok = kitty_gfx::place_rgba_rect_b64(
&mut out,
img.kitty_id,
img.cached_w,
img.cached_h,
Some(&img.cached_b64),
&img.cached_rgba,
col,
row,
editor_cursor,
use_sync,
)
.is_ok();
if ok {
*last_sig = Some(sig);
}
ok
}
fn run_app(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
caps: term_caps::TerminalCaps,
) -> io::Result<()> {
let mut lsp_sync_tick: u32 = 0;
let mut pet_placed = false;
let mut pet_last_frame = usize::MAX;
let mut pet_last_pos: (u16, u16) = (u16::MAX, u16::MAX);
let mut media_sig: Option<(u32, u16, u32, u32)> = None;
let mut gfx_registry = gfx::GfxRegistry::new();
let mut full_until = std::time::Instant::now() + std::time::Duration::from_millis(700);
let mut last_draw = std::time::Instant::now() - std::time::Duration::from_secs(1);
while app.running {
let now = std::time::Instant::now();
let live_surface = app.terminal.open
|| (app.pet.enabled && app.pet.has_frames())
|| app.git_wb.is_loading()
|| app.pr_review.loading
|| app.dap.is_session();
let draw_now = now < full_until
|| live_surface
|| now.duration_since(last_draw) >= std::time::Duration::from_millis(100);
if draw_now {
last_draw = now;
let use_sync = gpu_frame::should_sync(app.gpu_acc, &caps);
gpu_frame::draw_synced(terminal, use_sync, |f| ui::draw(f, app))?;
let editor_cursor = if kitty_gfx::available(app.gpu_acc && app.gpu_graphics, &caps) {
terminal.get_cursor_position().ok().map(|p| (p.x, p.y))
} else {
None
};
if app.peek.open {
if kitty_gfx::available(app.gpu_acc && app.gpu_graphics, &caps) {
let mut out = io::stdout();
let _ = kitty_gfx::place_shadow_bar(&mut out, 42, 56, 10, 3, 3);
}
if gpu_frame::should_hyperlink(app.gpu_acc && app.gpu_hyperlinks, &caps) {
let url = term_caps::file_url(&app.peek.path.display().to_string());
let label = app
.peek
.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("file");
let mut out = io::stdout();
let _ = write!(
out,
"\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
url, label
);
let _ = out.flush();
}
} else if kitty_gfx::available(app.gpu_acc && app.gpu_graphics, &caps) {
let mut out = io::stdout();
let _ = kitty_gfx::delete_image(&mut out, 42);
let _ = out.flush();
}
let painted_media = paint_media_preview(app, &caps, editor_cursor, &mut media_sig);
let painted_gfx = gfx_registry.flush(app, &caps, editor_cursor);
let painted_pet = paint_pet(
app,
&caps,
&mut pet_placed,
&mut pet_last_frame,
&mut pet_last_pos,
editor_cursor,
);
if (painted_pet || painted_media || painted_gfx) && let Some((cx, cy)) = editor_cursor {
let _ = terminal.set_cursor_position((cx, cy));
}
} let (running, had_event) = event::handle_events(app)?;
if had_event {
full_until = std::time::Instant::now() + std::time::Duration::from_millis(700);
}
if !running {
break;
}
if app.git_wb.poll_loading() {
if let Some(m) = app.git_wb.take_app_message() {
app.message = m;
app.refresh_git();
}
}
let _ = app.poll_git_refresh();
lsp_sync_tick = lsp_sync_tick.wrapping_add(1);
if lsp_sync_tick % 5 == 0 {
app.sync_lsp_document();
}
app.lsp.poll();
app.poll_call_hierarchy();
if app.pr_review.poll() {
app.message = app.pr_review.message.clone();
}
if let Some(m) = app.update.poll() {
app.message = m;
}
app.poll_hook_messages();
app.dap.poll();
if app.dap.location_dirty {
app.dap_apply_stopped_location();
}
let lsp_comps: Vec<_> = std::mem::take(&mut app.lsp.pending_completions);
if !lsp_comps.is_empty() && app.completions.active {
for item in lsp_comps {
let exists = app.completions.suggestions.iter().any(|s| s.label == item.label);
if !exists {
app.completions.suggestions.push(xei_core::completion::Suggestion {
label: item.label.clone(),
detail: item.detail.unwrap_or_else(|| "LSP".to_string()),
insert_text: item.label,
});
}
}
}
if let Some(loc) = app.lsp.pending_definition.take() {
let path_str = loc.path.clone();
let as_peek = app.lsp.definition_as_peek;
app.lsp.definition_as_peek = false;
let file_text = std::fs::read_to_string(&path_str).ok();
let line_for_col = file_text
.as_ref()
.and_then(|t| t.lines().nth(loc.row))
.unwrap_or("");
let col = xei_core::lsp::utf16_to_char_col(line_for_col, loc.col);
if as_peek {
app.open_peek_at(&path_str, loc.row, col);
} else {
app.open_new_tab(&path_str);
app.buffer.cursor.row = loc.row.min(app.buffer.line_count().saturating_sub(1));
let line = app.buffer.line(app.buffer.cursor.row);
app.buffer.cursor.col = xei_core::lsp::utf16_to_char_col(line, loc.col);
app.buffer.clamp_col();
app.update_scroll();
app.sync_split_from_active();
app.message = format!("Jumped to definition: {}:{}", path_str, loc.row + 1);
}
}
if !app.lsp.pending_symbols.is_empty() {
app.apply_pending_symbols();
}
if let Some(ref path) = app.filename.clone() {
let path_s = path.display().to_string();
if app.inlay_hints_enabled {
let end = app.buffer.line_count().saturating_sub(1);
app.lsp.maybe_request_inlays(&path_s, end);
}
if app.code_lens_enabled {
app.lsp.maybe_request_code_lens(&path_s);
}
}
if let Some(hover) = app.lsp.pending_hover.take() {
let short: String = hover.chars().take(800).collect();
app.hover_text = Some(short);
app.message = String::from("Hover (Esc to dismiss)");
}
if !app.lsp.pending_references.is_empty() {
let refs = std::mem::take(&mut app.lsp.pending_references);
if let Some(first) = refs.first() {
app.push_jump();
if app.filename.as_ref().map(|p| p.display().to_string()) != Some(first.path.clone())
{
app.open_new_tab(&first.path);
}
app.buffer.cursor.row = first.row.min(app.buffer.line_count().saturating_sub(1));
let line = app.buffer.line(app.buffer.cursor.row);
app.buffer.cursor.col = xei_core::lsp::utf16_to_char_col(line, first.col);
app.buffer.clamp_col();
app.update_scroll();
}
app.xlc.open = true;
app.xlc.add_output(&format!("=== {} reference(s) ===", refs.len()));
for (i, r) in refs.iter().enumerate() {
app.xlc.add_output(&format!(
" {}. {}:{}:{}",
i + 1,
r.path,
r.row + 1,
r.col + 1
));
}
app.message = format!("{} reference(s) — see XLC panel", refs.len());
}
if !app.lsp.pending_edits.is_empty() {
let edits = std::mem::take(&mut app.lsp.pending_edits);
app.apply_file_edits(edits);
}
if let Some(msg) = app.lsp.pending_workspace_edit.take() {
if !msg.starts_with("APPLY\n") {
app.message = msg;
}
}
if !app.lsp.pending_code_actions.is_empty() {
app.open_code_actions_palette();
}
if let Some(soft) = app.lsp.soft_error.take() {
if app.message.is_empty() || app.message.ends_with('…') {
app.message = soft;
}
}
if let Some(reg) = app.macros.recording {
if !app.message.contains("Recording") {
app.message = format!("Recording @{}…", reg);
}
}
app.check_external_change();
}
Ok(())
}