feat!: new virtual terminal backend (#3989)

This commit is contained in:
三咲雅 misaki masa 2026-05-22 16:31:28 +08:00 committed by sxyazi
parent 3f5cc47a48
commit c037855c4f
No known key found for this signature in database
148 changed files with 3434 additions and 1596 deletions

View file

@ -16,6 +16,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Bulk create ([#3793])
### Changed
- Rename `<BackTab>` to `<S-Tab>` ([#3989])
- Remove Legacy Console Mode on Windows ([#3989])
## [v26.5.6]
### Added
@ -1721,3 +1726,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3906]: https://github.com/sxyazi/yazi/pull/3906
[#3934]: https://github.com/sxyazi/yazi/pull/3934
[#3943]: https://github.com/sxyazi/yazi/pull/3943
[#3989]: https://github.com/sxyazi/yazi/pull/3989

697
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -43,14 +43,13 @@ bitflags = { version = "2.11.1", features = [ "serde" ] }
chrono = "0.4.44"
clap = { version = "4.6.1", features = [ "derive" ] }
core-foundation-sys = "0.8.7"
crossterm = { version = "0.29.0", features = [ "event-stream" ] }
dirs = "6.0.0"
dyn-clone = "1.0.20"
either = { version = "1.15.0" }
either = { version = "1.16.0" }
foldhash = "0.2.0"
futures = "0.3.32"
globset = "0.4.18"
hashbrown = { version = "0.17.0", features = [ "serde" ] }
hashbrown = { version = "0.17.1", features = [ "serde" ] }
indexmap = { version = "2.14.0", features = [ "serde" ] }
libc = "0.2.186"
lru = "0.18.0"
@ -61,13 +60,13 @@ parking_lot = "0.12.5"
paste = "1.0.15"
percent-encoding = "2.3.2"
rand = { version = "0.10.1", default-features = false, features = [ "std", "sys_rng" ] }
ratatui = { version = "0.30.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] }
ratatui = { version = "0.30.0", default-features = false, features = [ "layout-cache", "serde", "underline-color", "unstable-rendered-line-info", "unstable-widget-ref" ] }
regex = "1.12.3"
russh = { version = "0.60.2", default-features = false, features = [ "ring", "rsa" ] }
russh = { version = "0.61.0", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0"
serde = { version = "1.0.228", features = [ "derive" ] }
serde_json = "1.0.149"
serde_with = "3.19.0"
serde_with = "3.20.0"
strum = { version = "0.28.0", features = [ "derive" ] }
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
thiserror = "2.0.18"

View file

@ -1 +1 @@
{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee"],"version":"0.2","language":"en","flagWords":[]}
{"language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm"],"version":"0.2","flagWords":[]}

View file

@ -34,13 +34,13 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
yazi-tui = { path = "../yazi-tui", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies
anyhow = { workspace = true }
crossterm = { workspace = true }
either = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true }
@ -55,6 +55,3 @@ tracing = { workspace = true }
[target."cfg(unix)".dependencies]
libc = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -1,5 +1,4 @@
use anyhow::Result;
use crossterm::event::MouseEventKind;
use mlua::{ObjectLike, Table};
use tracing::error;
use yazi_actor::lives::Lives;
@ -8,6 +7,7 @@ use yazi_macro::succ;
use yazi_parser::app::MouseForm;
use yazi_plugin::LUA;
use yazi_shared::data::Data;
use yazi_term::event::MouseEventKind;
use crate::{Actor, Ctx};

View file

@ -1,9 +1,11 @@
use std::process;
use anyhow::Result;
use yazi_boot::ARGS;
use yazi_fs::provider::{Provider, local::Local};
use yazi_parser::app::QuitForm;
use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}};
use yazi_term::Term;
use yazi_tui::Raterm;
use crate::{Actor, Ctx};
@ -27,7 +29,8 @@ impl Actor for Quit {
);
});
Term::goodbye(|| opt.code);
Raterm::stop();
process::exit(opt.code);
}
}

View file

@ -1,28 +1,28 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::app::ResumeForm;
use yazi_parser::app::ReflowForm;
use yazi_shared::data::Data;
use yazi_term::Term;
use yazi_tui::Raterm;
use crate::{Actor, Ctx};
pub struct Resume;
impl Actor for Resume {
type Form = ResumeForm;
type Form = ReflowForm;
const NAME: &str = "resume";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
cx.active_mut().preview.reset();
*cx.term = Some(Term::start()?);
drop(cx.term.take());
*cx.term = Some(Raterm::start()?);
// While the app resumes, it's possible that the terminal size has changed.
// We need to trigger a resize, and render the UI based on the resized area.
act!(app:resize, cx, form.reflow)?;
form.tx.send((true, form.replier))?;
act!(app:title, cx).ok();
succ!(render!());
}

View file

@ -1,6 +1,6 @@
use anyhow::Result;
use yazi_macro::succ;
use yazi_parser::app::StopForm;
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
@ -8,20 +8,15 @@ use crate::{Actor, Ctx};
pub struct Stop;
impl Actor for Stop {
type Form = StopForm;
type Form = VoidForm;
const NAME: &str = "stop";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
cx.active_mut().preview.reset_image();
// We need to destroy the `term` first before stopping the `signals`
// to prevent any signal from triggering the term to render again
// while the app is being suspended.
*cx.term = None;
form.tx.send((false, form.replier))?;
succ!();
}
}

View file

@ -1,11 +1,11 @@
use anyhow::Result;
use crossterm::{execute, terminal::SetTitle};
use yazi_actor::Ctx;
use yazi_macro::succ;
use yazi_macro::{succ, writef};
use yazi_parser::{app::TitleForm, spark::SparkKind};
use yazi_shared::{Source, data::Data};
use yazi_term::TermState;
use yazi_term::sequence::SetTitle;
use yazi_tty::TTY;
use yazi_tui::RatermState;
use crate::Actor;
@ -18,9 +18,9 @@ impl Actor for Title {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let s = form.value.unwrap_or_else(|| format!("Yazi: {}", cx.tab().name()).into());
execute!(TTY.writer(), SetTitle(&s))?;
writef!(TTY.writer(), "{}", SetTitle(&s))?;
yazi_term::STATE.set(TermState { title: !s.is_empty(), ..yazi_term::STATE.get() });
yazi_tui::STATE.set(RatermState { title: !s.is_empty(), ..yazi_tui::STATE.get() });
succ!()
}

View file

@ -4,11 +4,11 @@ use anyhow::{Result, anyhow};
use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}};
use yazi_fs::File;
use yazi_shared::{Id, Source, event::Action, url::UrlBuf};
use yazi_term::Term;
use yazi_tui::Raterm;
pub struct Ctx<'a> {
pub core: &'a mut Core,
pub term: &'a mut Option<Term>,
pub term: &'a mut Option<Raterm>,
pub tab: usize,
pub level: usize,
pub source: Source,
@ -27,7 +27,7 @@ impl DerefMut for Ctx<'_> {
}
impl<'a> Ctx<'a> {
pub fn new(action: &Action, core: &'a mut Core, term: &'a mut Option<Term>) -> Result<Self> {
pub fn new(action: &Action, core: &'a mut Core, term: &'a mut Option<Raterm>) -> Result<Self> {
let tab = if let Ok(id) = action.get::<Id>("tab") {
core
.mgr
@ -63,7 +63,7 @@ impl<'a> Ctx<'a> {
}
}
pub fn active(core: &'a mut Core, term: &'a mut Option<Term>) -> Self {
pub fn active(core: &'a mut Core, term: &'a mut Option<Raterm>) -> Self {
let tab = core.mgr.tabs.cursor;
Self {
core,

View file

@ -5,13 +5,13 @@ use scopeguard::defer;
use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRule};
use yazi_fs::{File, FilesOp, Splatter, provider::{Provider, local::Local}};
use yazi_macro::succ;
use yazi_macro::{succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy};
use yazi_shared::{data::Data, strand::Strand, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}};
use yazi_shared::{data::Data, strand::Strand, url::{AsUrl, UrlBuf, UrlCow, UrlLike}};
use yazi_shim::path::CROSS_SEPARATOR;
use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_term::{YIELD_TO_SUBPROCESS, sequence::EraseScreen};
use yazi_tty::TTY;
use yazi_vfs::{VfsFile, provider};
use yazi_watcher::WATCHER;
@ -61,7 +61,7 @@ impl Actor for BulkCreate {
impl BulkCreate {
async fn r#do(cwd: UrlBuf, todo: Vec<Entry<'_>>) -> Result<()> {
terminal_clear(TTY.writer())?;
writef!(TTY.writer(), "{EraseScreen}\n")?;
if todo.is_empty() {
return Ok(());
} else if !Self::ask_continue(&todo, None)? {
@ -134,7 +134,7 @@ impl BulkCreate {
async fn output_failed(failed: Vec<(Entry<'_>, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout();
terminal_clear(&mut *stdout)?;
writeln!(stdout, "{EraseScreen}")?;
writeln!(stdout, "Failed to create:")?;
for (entry, err) in failed {
@ -161,7 +161,7 @@ impl<'a> Entry<'a> {
None => (s, false),
};
Some(Self { path: path.into(), is_dir }).filter(|_| !path.is_empty())
(!path.is_empty()).then_some(Self { path: path.into(), is_dir })
}
}

View file

@ -1,7 +1,6 @@
use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path, sync::Arc};
use anyhow::{Result, anyhow};
use crossterm::{execute, style::Print};
use hashbrown::HashMap;
use scopeguard::defer;
use tokio::io::AsyncWriteExt;
@ -9,12 +8,12 @@ use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRule};
use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider::{FileBuilder, Provider, local::{Gate, Local}}};
use yazi_macro::{err, succ};
use yazi_macro::{err, succ, writef};
use yazi_parser::VoidForm;
use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy};
use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}};
use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlCow, UrlLike}};
use yazi_term::{YIELD_TO_SUBPROCESS, sequence::EraseScreen};
use yazi_tty::TTY;
use yazi_vfs::{VfsFile, maybe_exists, provider};
use yazi_watcher::WATCHER;
@ -100,11 +99,10 @@ impl BulkRename {
selected: Vec<UrlBuf>,
decision: Option<bool>,
) -> Result<()> {
terminal_clear(TTY.writer())?;
writef!(TTY.writer(), "{EraseScreen}\n")?;
if old.len() != new.len() {
#[rustfmt::skip]
let s = format!("Number of new and old file names mismatch (New: {}, Old: {}).\nPress <Enter> to exit...", new.len(), old.len());
execute!(TTY.writer(), Print(s))?;
writef!(TTY.writer(), "Number of new and old file names mismatch (New: {}, Old: {}).\nPress <Enter> to exit...", new.len(), old.len())?;
TTY.reader().read_exact(&mut [0])?;
return Ok(());
@ -186,7 +184,7 @@ impl BulkRename {
async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout();
terminal_clear(&mut *stdout)?;
writeln!(stdout, "{EraseScreen}")?;
writeln!(stdout, "Failed to rename:")?;
for (old, new, err) in failed {

View file

@ -21,6 +21,6 @@ impl Actor for Close {
}
fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> {
Some(SparkKind::KeyClose).filter(|_| cx.source().is_key())
cx.source().is_key().then_some(SparkKind::KeyClose)
}
}

View file

@ -59,7 +59,7 @@ impl Actor for Quit {
}
fn hook(cx: &Ctx, _form: &Self::Form) -> Option<SparkKind> {
Some(SparkKind::KeyQuit).filter(|_| cx.source().is_key())
cx.source().is_key().then_some(SparkKind::KeyQuit)
}
}

View file

@ -3,11 +3,11 @@ use std::time::Duration;
use anyhow::Result;
use ratatui::layout::Rect;
use yazi_core::notify::Notify;
use yazi_emulator::Dimension;
use yazi_macro::{render, render_partial, succ};
use yazi_parser::notify::TickForm;
use yazi_proxy::NotifyProxy;
use yazi_shared::data::Data;
use yazi_term::{Dimension, TERM};
use crate::{Actor, Ctx};
@ -21,8 +21,8 @@ impl Actor for Tick {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
cx.notify.ticker.take().map(|h| h.abort());
let Dimension { rows, columns, .. } = Dimension::available();
let area = Notify::available(Rect { x: 0, y: 0, width: columns, height: rows });
let Dimension { rows, cols, .. } = TERM.dimension();
let area = Notify::available(Rect { x: 0, y: 0, width: cols, height: rows });
let limit = cx.notify.limit(area);
if limit == 0 {

View file

@ -1,15 +1,15 @@
use std::io::Write;
use anyhow::Result;
use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode}};
use scopeguard::defer;
use tokio::{io::{AsyncReadExt, stdin}, select, sync::mpsc, time};
use yazi_binding::Permit;
use yazi_macro::succ;
use yazi_term::TERM;
use yazi_macro::{succ, writef};
use yazi_parser::VoidForm;
use yazi_scheduler::AppProxy;
use yazi_shared::{data::Data, terminal_clear};
use yazi_term::YIELD_TO_SUBPROCESS;
use yazi_shared::data::Data;
use yazi_term::{YIELD_TO_SUBPROCESS, sequence::EraseScreen};
use yazi_tty::TTY;
use crate::{Actor, Ctx};
@ -39,24 +39,27 @@ impl Actor for Inspect {
task.logs.clone()
};
// Stop the app and clear the terminal
AppProxy::stop().await;
terminal_clear(TTY.writer()).ok();
writeln!(TTY.writer(), "{EraseScreen}").ok();
// Print the buffered logs
TTY.writer().write_all(buffered.as_bytes()).ok();
TTY.writer().flush().ok();
defer! { disable_raw_mode().ok(); }
enable_raw_mode().ok();
defer! { TERM.enter_cooked_mode().ok(); }
TERM.enter_raw_mode().ok();
let mut stdin = stdin(); // TODO: stdin
let mut answer = 0;
loop {
select! {
Some(line) = rx.recv() => {
execute!(TTY.writer(), crossterm::style::Print(line), crossterm::style::Print("\r\n")).ok();
writef!(TTY.writer(), "{line}\r\n").ok();
}
_ = time::sleep(time::Duration::from_millis(500)) => {
if !ongoing.lock().exists(id) {
execute!(TTY.writer(), crossterm::style::Print("Task finished, press `q` to quit\r\n")).ok();
writef!(TTY.writer(), "Task finished, press `q` to quit\r\n").ok();
break;
}
},

View file

@ -19,21 +19,18 @@ yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
# External dependencies
ansi-to-tui = { workspace = true }
anyhow = { workspace = true }
base64 = { workspace = true }
crossterm = { workspace = true }
image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] }
moxcms = "0.8.1"
palette = { version = "0.7.6", default-features = false }
quantette = { version = "0.5.1", default-features = false }
quantette = { version = "0.6.0", default-features = false }
ratatui = { workspace = true }
strum = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -2,10 +2,10 @@ use std::{io::Write, path::PathBuf, process::Stdio};
use ansi_to_tui::IntoText;
use anyhow::{Result, anyhow, bail};
use crossterm::{cursor::MoveTo, queue};
use ratatui::layout::Rect;
use tokio::process::Command;
use yazi_emulator::Emulator;
use yazi_term::sequence::MoveTo;
use crate::Adapter;
@ -62,7 +62,7 @@ impl Chafa {
Emulator::move_lock((max.x, max.y), |w| {
for (i, line) in lines.into_iter().enumerate() {
w.write_all(line)?;
queue!(w, MoveTo(max.x, max.y + i as u16 + 1))?;
write!(w, "{}", MoveTo(max.x, max.y + i as u16 + 1))?;
}
Ok(area)
})
@ -72,7 +72,7 @@ impl Chafa {
let s = " ".repeat(area.width as usize);
Emulator::move_lock((0, 0), |w| {
for y in area.top()..area.bottom() {
queue!(w, MoveTo(area.x, y))?;
write!(w, "{}", MoveTo(area.x, y))?;
write!(w, "{s}")?;
}
Ok(())

View file

@ -2,11 +2,11 @@ use std::{fmt::Write, io::Write as ioWrite, path::PathBuf};
use anyhow::Result;
use base64::{Engine, engine::{Config, general_purpose::STANDARD}};
use crossterm::{cursor::MoveTo, queue};
use image::{DynamicImage, ExtendedColorType, ImageEncoder, codecs::{jpeg::JpegEncoder, png::PngEncoder}};
use ratatui::layout::Rect;
use yazi_config::YAZI;
use yazi_emulator::{CLOSE, Emulator, START};
use yazi_term::sequence::MoveTo;
use crate::{Image, adapter::Adapter};
@ -30,7 +30,7 @@ impl Iip {
let s = " ".repeat(area.width as usize);
Emulator::move_lock((0, 0), |w| {
for y in area.top()..area.bottom() {
queue!(w, MoveTo(area.x, y))?;
write!(w, "{}", MoveTo(area.x, y))?;
write!(w, "{s}")?;
}
Ok(())

View file

@ -3,11 +3,11 @@ use std::{io::Write, path::PathBuf};
use anyhow::Result;
use base64::{Engine, engine::general_purpose};
use crossterm::{cursor::MoveTo, queue};
use image::DynamicImage;
use ratatui::layout::Rect;
use ratatui::{layout::Rect, style::Color};
use yazi_emulator::{CLOSE, ESCAPE, Emulator, START};
use yazi_shim::cell::SyncCell;
use yazi_term::sequence::{MoveTo, SetFg};
use crate::{adapter::Adapter, image::Image};
@ -334,7 +334,7 @@ impl Kgp {
let s = " ".repeat(area.width as usize);
Emulator::move_lock((0, 0), |w| {
for y in area.top()..area.bottom() {
queue!(w, MoveTo(area.x, y))?;
write!(w, "{}", MoveTo(area.x, y))?;
write!(w, "{s}")?;
}
@ -385,10 +385,10 @@ impl Kgp {
let id = Self::image_id();
let (r, g, b) = ((id >> 16) & 0xff, (id >> 8) & 0xff, id & 0xff);
write!(buf, "\x1b[38;2;{r};{g};{b}m")?;
write!(buf, "{}", SetFg(Color::Rgb(r as u8, g as u8, b as u8)))?;
for y in 0..area.height {
write!(buf, "\x1b[{};{}H", area.y + y + 1, area.x + 1)?;
write!(buf, "{}", MoveTo(area.x, area.y + y))?;
for x in 0..area.width {
write!(buf, "\u{10EEEE}")?;
write!(buf, "{}", *DIACRITICS.get(y as usize).unwrap_or(&DIACRITICS[0]))?;

View file

@ -1,12 +1,12 @@
use std::{io::Write, path::PathBuf};
use anyhow::{Result, bail};
use crossterm::{cursor::MoveTo, queue};
use image::{DynamicImage, GenericImageView, RgbImage};
use palette::{Srgb, cast::ComponentsAs};
use quantette::{PaletteSize, color_map::IndexedColorMap, wu::{BinnerU8x3, WuU8x3}};
use ratatui::layout::Rect;
use yazi_emulator::{CLOSE, ESCAPE, Emulator, START};
use yazi_term::sequence::MoveTo;
use crate::{Image, adapter::Adapter};
@ -35,7 +35,7 @@ impl Sixel {
let s = " ".repeat(area.width as usize);
Emulator::move_lock((0, 0), |w| {
for y in area.top()..area.bottom() {
queue!(w, MoveTo(area.x, y))?;
write!(w, "{}", MoveTo(area.x, y))?;
write!(w, "{s}")?;
}
Ok(())

View file

@ -24,13 +24,13 @@ yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies
ansi-to-tui = { workspace = true }
anyhow = { workspace = true }
crossterm = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true }
mlua = { workspace = true }
@ -45,8 +45,5 @@ unicode-width = { workspace = true }
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }
[target."cfg(windows)".dependencies]
windows-sys = { version = "0.61.2", features = [ "Win32_Foundation", "Win32_System_JobObjects" ] }

View file

@ -1,19 +1,19 @@
use std::ops::Deref;
use crossterm::event::MouseButton;
use mlua::{UserData, UserDataFields};
use yazi_term::event::{MouseButton, MouseEventKind};
#[derive(Clone, Copy)]
pub struct MouseEvent(crossterm::event::MouseEvent);
pub struct MouseEvent(yazi_term::event::MouseEvent);
impl Deref for MouseEvent {
type Target = crossterm::event::MouseEvent;
type Target = yazi_term::event::MouseEvent;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<crossterm::event::MouseEvent> for MouseEvent {
fn from(event: crossterm::event::MouseEvent) -> Self { Self(event) }
impl From<yazi_term::event::MouseEvent> for MouseEvent {
fn from(event: yazi_term::event::MouseEvent) -> Self { Self(event) }
}
impl UserData for MouseEvent {
@ -21,15 +21,15 @@ impl UserData for MouseEvent {
fields.add_field_method_get("x", |_, me| Ok(me.column));
fields.add_field_method_get("y", |_, me| Ok(me.row));
fields.add_field_method_get("is_left", |_, me| {
use crossterm::event::MouseEventKind as K;
use MouseEventKind as K;
Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Left))
});
fields.add_field_method_get("is_right", |_, me| {
use crossterm::event::MouseEventKind as K;
use MouseEventKind as K;
Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Right))
});
fields.add_field_method_get("is_middle", |_, me| {
use crossterm::event::MouseEventKind as K;
use MouseEventKind as K;
Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Middle))
});
}

View file

@ -20,6 +20,7 @@ yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
# External dependencies
@ -33,7 +34,7 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
# External dependencies
clap = { workspace = true }
clap_complete = "4.6.3"
clap_complete = "4.6.5"
clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.6.0"
vergen-gitcl = { version = "9.1.0", features = [ "build", "rustc" ] }

View file

@ -5,6 +5,7 @@ use yazi_config::{THEME, YAZI};
use yazi_emulator::Mux;
use yazi_fs::Xdg;
use yazi_shared::timestamp_us;
use yazi_term::TERM;
use super::Actions;
@ -39,7 +40,7 @@ impl Actions {
writeln!(s, "\nAdapter")?;
writeln!(s, " Adapter.matches : {:?}", yazi_adapter::ADAPTOR)?;
writeln!(s, " Dimension.available: {:?}", yazi_emulator::Dimension::available())?;
writeln!(s, " Dimension.available: {:?}", TERM.dimension())?;
writeln!(s, "\nDesktop")?;
writeln!(s, " XDG_SESSION_TYPE : {:?}", env::var_os("XDG_SESSION_TYPE"))?;

View file

@ -18,12 +18,11 @@ yazi-dds = { path = "../yazi-dds", version = "26.5.6" }
yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
# External dependencies
anyhow = { workspace = true }
clap = { workspace = true }
crossterm = { workspace = true }
hashbrown = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
@ -37,16 +36,13 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
# External build dependencies
anyhow = { workspace = true }
clap = { workspace = true }
clap_complete = "4.6.3"
clap_complete = "4.6.5"
clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.6.0"
serde = { workspace = true }
serde_json = { workspace = true }
vergen-gitcl = { version = "9.1.0", features = [ "build" ] }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }
[[bin]]
name = "ya"
path = "src/main.rs"

View file

@ -1,4 +1,4 @@
use std::{env, io::{self, BufWriter}, path::{Path, PathBuf}, str::FromStr};
use std::{env, io, path::{Path, PathBuf}, str::FromStr};
use anyhow::{Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
@ -48,20 +48,19 @@ impl Dependency {
pub(super) fn header(&self, s: &str) -> Result<()> {
use std::io::IsTerminal;
use crossterm::style::{Attribute, Print, SetAttributes};
use yazi_shim::crossterm::If;
use yazi_macro::writef;
use yazi_term::sequence::{If, SetSgr};
let ansi = env::var_os("YA_FORCE_ANSI").is_some_and(|v| v == "1") || io::stdout().is_terminal();
crossterm::execute!(
BufWriter::new(io::stdout()),
Print("\n"),
If(ansi, SetAttributes(Attribute::Reverse.into())),
If(ansi, SetAttributes(Attribute::Bold.into())),
Print(" "),
Print(s.replacen("{name}", &self.name, 1)),
Print(" "),
If(ansi, SetAttributes(Attribute::Reset.into())),
Print("\n\n"),
writef!(
io::stdout(),
"\n{}{}{} {} {}{}\n\n",
If(ansi, SetSgr::Reverse),
If(ansi, SetSgr::Bold),
" ",
s.replacen("{name}", &self.name, 1),
" ",
If(ansi, SetSgr::Reset),
)?;
Ok(())
}

View file

@ -18,13 +18,13 @@ yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
# External dependencies
anyhow = { workspace = true }
arc-swap = { workspace = true }
bitflags = { workspace = true }
crossterm = { workspace = true }
globset = { workspace = true }
hashbrown = { workspace = true }
indexmap = { workspace = true }
@ -36,6 +36,3 @@ strum = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -1,7 +1,7 @@
use std::{fmt::{Display, Write}, str::FromStr};
use anyhow::bail;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use yazi_term::event::{KeyCode, KeyEvent, Modifiers};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Key {
@ -30,29 +30,12 @@ impl Default for Key {
impl From<KeyEvent> for Key {
fn from(value: KeyEvent) -> Self {
// For alphabet:
// Unix : <S-a> => Char("A") + SHIFT
// Windows : <S-a> => Char("A") + SHIFT
//
// For non-alphabet:
// Unix : <S-`> => Char("~") + NULL
// Windows : <S-`> => Char("~") + SHIFT
//
// So we detect `Char("~") + SHIFT`, and change it to `Char("~") + NULL`
// for consistent behavior between OSs.
let shift = match (value.code, value.modifiers) {
(KeyCode::Char(c), _) => c.is_ascii_uppercase(),
(KeyCode::BackTab, _) => false,
(_, m) => m.contains(KeyModifiers::SHIFT),
};
Self {
code: value.code,
shift,
ctrl: value.modifiers.contains(KeyModifiers::CONTROL),
alt: value.modifiers.contains(KeyModifiers::ALT),
super_: value.modifiers.contains(KeyModifiers::SUPER),
code: value.code,
shift: value.modifiers.contains(Modifiers::SHIFT),
ctrl: value.modifiers.contains(Modifiers::CONTROL),
alt: value.modifiers.contains(Modifiers::ALT),
super_: value.modifiers.contains(Modifiers::SUPER),
}
}
}
@ -92,29 +75,28 @@ impl FromStr for Key {
"pageup" => key.code = KeyCode::PageUp,
"pagedown" => key.code = KeyCode::PageDown,
"tab" => key.code = KeyCode::Tab,
"backtab" => key.code = KeyCode::BackTab,
"delete" => key.code = KeyCode::Delete,
"insert" => key.code = KeyCode::Insert,
"f1" => key.code = KeyCode::F(1),
"f2" => key.code = KeyCode::F(2),
"f3" => key.code = KeyCode::F(3),
"f4" => key.code = KeyCode::F(4),
"f5" => key.code = KeyCode::F(5),
"f6" => key.code = KeyCode::F(6),
"f7" => key.code = KeyCode::F(7),
"f8" => key.code = KeyCode::F(8),
"f9" => key.code = KeyCode::F(9),
"f10" => key.code = KeyCode::F(10),
"f11" => key.code = KeyCode::F(11),
"f12" => key.code = KeyCode::F(12),
"f13" => key.code = KeyCode::F(13),
"f14" => key.code = KeyCode::F(14),
"f15" => key.code = KeyCode::F(15),
"f16" => key.code = KeyCode::F(16),
"f17" => key.code = KeyCode::F(17),
"f18" => key.code = KeyCode::F(18),
"f19" => key.code = KeyCode::F(19),
"esc" => key.code = KeyCode::Esc,
"f1" => key.code = KeyCode::Fn(1),
"f2" => key.code = KeyCode::Fn(2),
"f3" => key.code = KeyCode::Fn(3),
"f4" => key.code = KeyCode::Fn(4),
"f5" => key.code = KeyCode::Fn(5),
"f6" => key.code = KeyCode::Fn(6),
"f7" => key.code = KeyCode::Fn(7),
"f8" => key.code = KeyCode::Fn(8),
"f9" => key.code = KeyCode::Fn(9),
"f10" => key.code = KeyCode::Fn(10),
"f11" => key.code = KeyCode::Fn(11),
"f12" => key.code = KeyCode::Fn(12),
"f13" => key.code = KeyCode::Fn(13),
"f14" => key.code = KeyCode::Fn(14),
"f15" => key.code = KeyCode::Fn(15),
"f16" => key.code = KeyCode::Fn(16),
"f17" => key.code = KeyCode::Fn(17),
"f18" => key.code = KeyCode::Fn(18),
"f19" => key.code = KeyCode::Fn(19),
"esc" => key.code = KeyCode::Escape,
_ => match next {
s if it.peek().is_none() => {
@ -166,29 +148,28 @@ impl Display for Key {
KeyCode::PageUp => "PageUp",
KeyCode::PageDown => "PageDown",
KeyCode::Tab => "Tab",
KeyCode::BackTab => "BackTab",
KeyCode::Delete => "Delete",
KeyCode::Insert => "Insert",
KeyCode::F(1) => "F1",
KeyCode::F(2) => "F2",
KeyCode::F(3) => "F3",
KeyCode::F(4) => "F4",
KeyCode::F(5) => "F5",
KeyCode::F(6) => "F6",
KeyCode::F(7) => "F7",
KeyCode::F(8) => "F8",
KeyCode::F(9) => "F9",
KeyCode::F(10) => "F10",
KeyCode::F(11) => "F11",
KeyCode::F(12) => "F12",
KeyCode::F(13) => "F13",
KeyCode::F(14) => "F14",
KeyCode::F(15) => "F15",
KeyCode::F(16) => "F16",
KeyCode::F(17) => "F17",
KeyCode::F(18) => "F18",
KeyCode::F(19) => "F19",
KeyCode::Esc => "Esc",
KeyCode::Fn(1) => "F1",
KeyCode::Fn(2) => "F2",
KeyCode::Fn(3) => "F3",
KeyCode::Fn(4) => "F4",
KeyCode::Fn(5) => "F5",
KeyCode::Fn(6) => "F6",
KeyCode::Fn(7) => "F7",
KeyCode::Fn(8) => "F8",
KeyCode::Fn(9) => "F9",
KeyCode::Fn(10) => "F10",
KeyCode::Fn(11) => "F11",
KeyCode::Fn(12) => "F12",
KeyCode::Fn(13) => "F13",
KeyCode::Fn(14) => "F14",
KeyCode::Fn(15) => "F15",
KeyCode::Fn(16) => "F16",
KeyCode::Fn(17) => "F17",
KeyCode::Fn(18) => "F18",
KeyCode::Fn(19) => "F19",
KeyCode::Escape => "Esc",
KeyCode::Char(' ') => "Space",
KeyCode::Char(c) => {

View file

@ -5,6 +5,7 @@ yazi_macro::mod_flat!(icon layout mixing pattern platform preset priority select
use std::io::{Read, Write};
use yazi_shim::{cell::{RoCell, SyncCell}, toml::{DeserializeOver, DeserializeOverWith}};
use yazi_term::sequence::SetSgr;
use yazi_tty::TTY;
pub static YAZI: RoCell<yazi::Yazi> = RoCell::new();
@ -74,14 +75,12 @@ fn wait_for_key(e: anyhow::Error) -> anyhow::Result<()> {
writeln!(stdout, "\nCaused by:\n{src}")?;
}
use crossterm::style::{Attribute, Print, SetAttributes};
crossterm::execute!(
writeln!(
stdout,
SetAttributes(Attribute::Reverse.into()),
SetAttributes(Attribute::Bold.into()),
Print("Press <Enter> to continue with preset settings..."),
SetAttributes(Attribute::Reset.into()),
Print("\n"),
"{}{}Press <Enter> to continue with preset settings...{}",
SetSgr::Reverse,
SetSgr::Bold,
SetSgr::Reset
)?;
TTY.reader().read_exact(&mut [0])?;

View file

@ -1,7 +1,7 @@
use anyhow::{Result, bail};
use bitflags::bitflags;
use crossterm::event::MouseEventKind;
use serde::{Deserialize, Serialize};
use yazi_term::event::MouseEventKind;
bitflags! {
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
@ -45,8 +45,8 @@ impl From<MouseEvents> for Vec<String> {
}
}
impl From<crossterm::event::MouseEventKind> for MouseEvents {
fn from(value: crossterm::event::MouseEventKind) -> Self {
impl From<MouseEventKind> for MouseEvents {
fn from(value: MouseEventKind) -> Self {
match value {
MouseEventKind::Down(_) | MouseEventKind::Up(_) => Self::CLICK,
MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => Self::SCROLL,

View file

@ -1,5 +1,5 @@
use crossterm::terminal::WindowSize;
use ratatui::layout::Rect;
use yazi_term::Dimension;
use super::{Offset, Origin};
@ -12,15 +12,15 @@ pub struct Position {
impl Position {
pub const fn new(origin: Origin, offset: Offset) -> Self { Self { origin, offset } }
pub fn rect(&self, WindowSize { columns, rows, .. }: WindowSize) -> Rect {
pub fn rect(&self, Dimension { cols, rows, .. }: Dimension) -> Rect {
use Origin::*;
let Offset { x, y, width, height } = self.offset;
let max_x = columns.saturating_sub(width);
let max_x = cols.saturating_sub(width);
let new_x = match self.origin {
TopLeft | BottomLeft => x.clamp(0, max_x as i16) as u16,
TopCenter | BottomCenter | Center => {
(columns / 2).saturating_sub(width / 2).saturating_add_signed(x).clamp(0, max_x)
(cols / 2).saturating_sub(width / 2).saturating_add_signed(x).clamp(0, max_x)
}
TopRight | BottomRight => max_x.saturating_add_signed(x).clamp(0, max_x),
Hovered => unreachable!(),
@ -37,18 +37,18 @@ impl Position {
Rect {
x: new_x,
y: new_y,
width: width.min(columns.saturating_sub(new_x)),
width: width.min(cols.saturating_sub(new_x)),
height: height.min(rows.saturating_sub(new_y)),
}
}
pub fn sticky(WindowSize { columns, rows, .. }: WindowSize, base: Rect, offset: Offset) -> Rect {
pub fn sticky(Dimension { cols, rows, .. }: Dimension, base: Rect, offset: Offset) -> Rect {
let Offset { x, y, width, height } = offset;
let above =
base.y.saturating_add(base.height).saturating_add(height).saturating_add_signed(y) > rows;
let new_x = base.x.saturating_add_signed(x).clamp(0, columns.saturating_sub(width));
let new_x = base.x.saturating_add_signed(x).clamp(0, cols.saturating_sub(width));
let new_y = if above {
base.y.saturating_sub(height.saturating_sub(y.unsigned_abs()))
} else {
@ -58,7 +58,7 @@ impl Position {
Rect {
x: new_x,
y: new_y,
width: width.min(columns.saturating_sub(new_x)),
width: width.min(cols.saturating_sub(new_x)),
height: height.min(rows.saturating_sub(new_y)),
}
}

View file

@ -21,20 +21,19 @@ yazi-adapter = { path = "../yazi-adapter", version = "26.5.6" }
yazi-binding = { path = "../yazi-binding", version = "26.5.6" }
yazi-config = { path = "../yazi-config", version = "26.5.6" }
yazi-dds = { path = "../yazi-dds", version = "26.5.6" }
yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" }
yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-runner = { path = "../yazi-runner", version = "26.5.6" }
yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies
anyhow = { workspace = true }
crossterm = { workspace = true }
dyn-clone = { workspace = true }
hashbrown = { workspace = true }
indexmap = { workspace = true }
@ -51,6 +50,3 @@ tokio-util = { workspace = true }
tracing = { workspace = true }
unicode-width = { workspace = true }
yazi-prebuilt = "0.1.0"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -99,7 +99,7 @@ impl Clone for Box<dyn PluginCallback> {
fn clone(&self) -> Self { dyn_clone::clone_box(&**self) }
}
impl fmt::Debug for dyn PluginCallback {
impl Debug for dyn PluginCallback {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PluginCallback").finish_non_exhaustive()
}

View file

@ -1,6 +1,6 @@
use crossterm::cursor::SetCursorStyle;
use ratatui::layout::{Position, Rect};
use yazi_shared::Layer;
use yazi_term::CursorStyle;
use crate::{cmp::Cmp, confirm::Confirm, help::Help, input::Input, mgr::Mgr, notify::Notify, pick::Pick, tab::{Folder, Tab}, tasks::Tasks, which::Which};
@ -31,7 +31,7 @@ impl Core {
}
}
pub fn cursor(&self) -> Option<(Position, SetCursorStyle)> {
pub fn cursor(&self) -> Option<(Position, CursorStyle)> {
if self.input.visible {
let Rect { x, y, .. } = self.mgr.area(self.input.position);
return Some((

View file

@ -1,10 +1,9 @@
use anyhow::Result;
use crossterm::{cursor::SetCursorStyle, event::KeyCode};
use unicode_width::UnicodeWidthStr;
use yazi_config::{KEYMAP, YAZI, keymap::{Chord, Key}};
use yazi_emulator::Dimension;
use yazi_macro::{act, render, render_and};
use yazi_shared::Layer;
use yazi_term::{CursorStyle, TERM, event::KeyCode};
use yazi_widgets::Scrollable;
use crate::help::HELP_MARGIN;
@ -27,7 +26,7 @@ impl Help {
pub fn r#type(&mut self, key: &Key) -> Result<bool> {
let Some(input) = &mut self.in_filter else { return Ok(false) };
match key {
Key { code: KeyCode::Esc, shift: false, ctrl: false, alt: false, super_: false } => {
Key { code: KeyCode::Escape, shift: false, ctrl: false, alt: false, super_: false } => {
self.in_filter = None;
render!();
}
@ -85,26 +84,22 @@ impl Help {
return None;
}
if let Some(kw) = self.keyword() {
return Some((kw.width() as u16, Dimension::available().rows));
return Some((kw.width() as u16, TERM.dimension().rows));
}
None
}
pub fn rel_cursor(&self) -> usize { self.cursor - self.offset }
pub fn cursor_shape(&self) -> SetCursorStyle {
if YAZI.input.cursor_blink {
SetCursorStyle::BlinkingBlock
} else {
SetCursorStyle::SteadyBlock
}
pub fn cursor_shape(&self) -> CursorStyle {
if YAZI.input.cursor_blink { CursorStyle::BlinkingBlock } else { CursorStyle::SteadyBlock }
}
}
impl Scrollable for Help {
fn total(&self) -> usize { self.bindings.len() }
fn limit(&self) -> usize { Dimension::available().rows.saturating_sub(HELP_MARGIN) as usize }
fn limit(&self) -> usize { TERM.dimension().rows.saturating_sub(HELP_MARGIN) as usize }
fn cursor_mut(&mut self) -> &mut usize { &mut self.cursor }

View file

@ -2,9 +2,9 @@ use std::iter;
use ratatui::layout::Rect;
use yazi_config::popup::{Origin, Position};
use yazi_emulator::Dimension;
use yazi_fs::Splatable;
use yazi_shared::url::{AsUrl, Url, UrlBuf};
use yazi_term::TERM;
use yazi_watcher::Watcher;
use super::{Batcher, Mimetype, Tabs, Yanked};
@ -35,7 +35,7 @@ impl Mgr {
if pos.origin == Origin::Hovered {
self.active().hovered_rect_based(pos)
} else {
pos.rect(Dimension::available().into())
pos.rect(TERM.dimension())
}
}

View file

@ -4,9 +4,9 @@ use anyhow::Result;
use ratatui::layout::Rect;
use tokio::task::JoinHandle;
use yazi_config::{LAYOUT, popup::{Origin, Position}};
use yazi_emulator::Dimension;
use yazi_fs::File;
use yazi_shared::{Id, Ids, url::{UrlBuf, UrlLike}};
use yazi_term::TERM;
use super::{Backstack, Finder, Folder, History, Mode, Preference, Preview};
use crate::{spot::Spot, tab::Selected};
@ -90,11 +90,11 @@ impl Tab {
}
pub fn hovered_rect_based(&self, pos: Position) -> Rect {
let ws = Dimension::available().into();
let dim = TERM.dimension();
if let Some(r) = self.hovered_rect() {
Position::sticky(ws, r, pos.offset)
Position::sticky(dim, r, pos.offset)
} else {
Position::new(Origin::TopCenter, pos.offset).rect(ws)
Position::new(Origin::TopCenter, pos.offset).rect(dim)
}
}

View file

@ -1,8 +1,8 @@
use std::{sync::Arc, time::Duration};
use tokio::{task::JoinHandle, time::sleep};
use yazi_emulator::Dimension;
use yazi_scheduler::{Scheduler, TaskSnap, TaskSummary};
use yazi_term::TERM;
use super::{TASKS_BORDER, TASKS_PADDING, TASKS_PERCENT};
use crate::AppProxy;
@ -52,8 +52,8 @@ impl Tasks {
}
pub fn limit() -> usize {
((Dimension::available().rows * TASKS_PERCENT / 100)
.saturating_sub(TASKS_BORDER + TASKS_PADDING) as usize)
((TERM.dimension().rows * TASKS_PERCENT / 100).saturating_sub(TASKS_BORDER + TASKS_PADDING)
as usize)
/ 3
}

View file

@ -35,7 +35,7 @@ impl IntoLua for EmberCd<'_> {
lua
.create_table_from([
("tab", self.tab.get().into_lua(lua)?),
("url", Some(self.url).filter(|_| !self.dummy).map(yazi_binding::Url::new).into_lua(lua)?),
("url", (!self.dummy).then_some(self.url).map(yazi_binding::Url::new).into_lua(lua)?),
])?
.into_lua(lua)
}

View file

@ -16,15 +16,13 @@ workspace = true
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
# External dependencies
anyhow = { workspace = true }
crossterm = { workspace = true }
either = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }
anyhow = { workspace = true }
either = { workspace = true }
ratatui = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }

View file

@ -1,59 +1,16 @@
use std::mem;
use crossterm::terminal::WindowSize;
use yazi_term::TERM;
use crate::EMULATOR;
#[derive(Clone, Copy, Debug)]
pub struct Dimension {
pub rows: u16,
pub columns: u16,
pub width: u16,
pub height: u16,
}
impl From<WindowSize> for Dimension {
fn from(s: WindowSize) -> Self {
Self { rows: s.rows, columns: s.columns, width: s.width, height: s.height }
}
}
impl From<Dimension> for WindowSize {
fn from(d: Dimension) -> Self {
Self { rows: d.rows, columns: d.columns, width: d.width, height: d.height }
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Dimension;
impl Dimension {
pub fn available() -> Self {
let mut size = WindowSize { rows: 0, columns: 0, width: 0, height: 0 };
if let Ok(s) = crossterm::terminal::window_size() {
_ = mem::replace(&mut size, s);
}
if (size.columns == 0 || size.rows == 0)
&& let Ok((cols, rows)) = crossterm::terminal::size()
{
size.columns = cols;
size.rows = rows;
}
size.into()
}
pub fn ratio(self) -> Option<(f64, f64)> {
if self.rows == 0 || self.columns == 0 || self.width == 0 || self.height == 0 {
None
} else {
Some((self.width as f64 / self.columns as f64, self.height as f64 / self.rows as f64))
}
}
pub fn cell_size() -> Option<(f64, f64)> {
let emu = &*EMULATOR;
Some(if emu.force_16t {
(emu.csi_16t.0 as f64, emu.csi_16t.1 as f64)
} else if let Some(r) = Self::available().ratio() {
} else if let Some(r) = TERM.dimension().ratio() {
r
} else if emu.csi_16t.0 != 0 && emu.csi_16t.1 != 0 {
(emu.csi_16t.0 as f64, emu.csi_16t.1 as f64)

View file

@ -1,15 +1,17 @@
use std::{io::BufWriter, time::Duration};
use std::{io::{self, BufWriter, Write}, time::Duration};
use anyhow::Result;
use crossterm::{cursor::{RestorePosition, SavePosition}, execute, style::Print, terminal::{disable_raw_mode, enable_raw_mode}};
use either::Either;
use ratatui::style::Color;
use scopeguard::defer;
use tokio::time::sleep;
use tracing::{debug, error, warn};
use yazi_macro::writef;
use yazi_shim::cell::RoCell;
use yazi_term::{TERM, sequence::{HideCursor, If, KittyGraphicsQuery, MoveTo, RequestBgColor, RequestCellPixelSize, RequestDA1, RequestXtVersion, RestoreCursorPos, SaveCursorPos, SetFg, SetSgr, ShowCursor}};
use yazi_tty::{Handle, TTY};
use crate::{Brand, Dimension, Mux, TMUX, Unknown};
use crate::{Brand, Mux, TMUX, Unknown};
pub static EMULATOR: RoCell<Emulator> = RoCell::new();
@ -36,25 +38,18 @@ impl Default for Emulator {
impl Emulator {
pub fn detect() -> Result<Self> {
defer! { disable_raw_mode().ok(); }
enable_raw_mode()?;
defer! { TERM.enter_cooked_mode().ok(); }
TERM.enter_raw_mode()?;
let resort = Brand::from_env();
let kgp_seq = if resort.is_none() {
Mux::csi("\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\")
} else {
"".into()
};
execute!(
writef!(
TTY.writer(),
SavePosition,
Print(kgp_seq), // Detect KGP
Print(Mux::csi("\x1b[>q")), // Request terminal version
Print("\x1b[16t"), // Request cell size
Print("\x1b]11;?\x07"), // Request background color
Print(Mux::csi("\x1b[0c")), // Request device attributes
RestorePosition
"{SaveCursorPos}{}{}{}{}{}{RestoreCursorPos}",
If(resort.is_none(), Mux::wrap(KittyGraphicsQuery)),
Mux::wrap(RequestXtVersion),
RequestCellPixelSize,
RequestBgColor,
Mux::wrap(RequestDA1),
)?;
let resp = Self::read_until_da1();
@ -83,28 +78,26 @@ impl Emulator {
where
F: FnOnce(&mut BufWriter<Handle>) -> Result<T>,
{
use std::{io::Write, thread, time::Duration};
use crossterm::{cursor::{Hide, MoveTo, Show}, queue};
use std::{thread, time::Duration};
let mut w = TTY.lockout();
// I really don't want to add this,
// But tmux and ConPTY sometimes cause the cursor position to get out of sync.
if TMUX.get() || cfg!(windows) {
execute!(w, SavePosition, MoveTo(x, y), Show)?;
execute!(w, MoveTo(x, y), Show)?;
execute!(w, MoveTo(x, y), Show)?;
writef!(w, "{SaveCursorPos}{}{ShowCursor}", MoveTo(x, y))?;
writef!(w, "{}{ShowCursor}", MoveTo(x, y))?;
writef!(w, "{}{ShowCursor}", MoveTo(x, y))?;
thread::sleep(Duration::from_millis(1));
} else {
queue!(w, SavePosition, MoveTo(x, y))?;
write!(w, "{SaveCursorPos}{}", MoveTo(x, y))?;
}
let result = cb(&mut w);
if TMUX.get() || cfg!(windows) {
queue!(w, Hide, RestorePosition)?;
write!(w, "{HideCursor}{RestoreCursorPos}")?;
} else {
queue!(w, RestorePosition)?;
write!(w, "{RestoreCursorPos}")?;
}
w.flush()?;
@ -148,21 +141,13 @@ impl Emulator {
}
async fn error_to_user() {
use crossterm::style::{Attribute, Color, Print, ResetColor, SetAttributes, SetForegroundColor};
sleep(Duration::from_millis(400)).await;
_ = crossterm::execute!(
std::io::stderr(),
SetForegroundColor(Color::Red),
SetAttributes(Attribute::Bold.into()),
Print("\r\nTerminal response timeout: "),
ResetColor,
SetAttributes(Attribute::Reset.into()),
//
Print("The request sent by Yazi didn't receive a correct response.\r\n"),
Print(
"Please check your terminal environment as per: https://yazi-rs.github.io/docs/faq#trt\r\n"
),
_ = writef!(
io::stderr(),
"{}{}\r\nTerminal response timeout: {}The request sent by Yazi didn't receive a correct response.\r\nPlease check your terminal environment as per: https://yazi-rs.github.io/docs/faq#trt\r\n",
SetFg(Color::Red),
SetSgr::Bold,
SetSgr::Reset,
);
}
@ -206,8 +191,6 @@ impl Emulator {
return false;
}
Dimension::available()
.ratio()
.is_none_or(|(rw, rh)| rw.floor() as u16 != w || rh.floor() as u16 != h)
TERM.dimension().ratio().is_none_or(|(rw, rh)| rw.floor() as u16 != w || rh.floor() as u16 != h)
}
}

View file

@ -1,9 +1,10 @@
use std::borrow::Cow;
use std::fmt::{self, Display};
use anyhow::Result;
use tracing::error;
use yazi_macro::time;
use yazi_macro::{time, writef};
use yazi_shim::cell::SyncCell;
use yazi_term::sequence::RequestDeviceStatus;
use yazi_tty::TTY;
use crate::Emulator;
@ -16,15 +17,24 @@ pub static CLOSE: SyncCell<&'static str> = SyncCell::new("");
pub struct Mux;
impl Mux {
pub fn csi(s: &str) -> Cow<'_, str> {
if TMUX.get() {
Cow::Owned(format!(
"{START}{}{CLOSE}",
s.trim_start_matches('\x1b').replace('\x1b', ESCAPE.get()),
))
} else {
Cow::Borrowed(s)
pub fn wrap<T: Display>(s: T) -> impl Display {
struct Wrapper<T>(T);
impl<T: Display> Display for Wrapper<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !TMUX.get() {
return self.0.fmt(f);
}
write!(
f,
"{START}{}{CLOSE}",
self.0.to_string().trim_start_matches('\x1b').replace('\x1b', ESCAPE.get())
)
}
}
Wrapper(s)
}
pub fn tmux_passthrough() {
@ -56,7 +66,7 @@ impl Mux {
pub fn tmux_drain() -> Result<()> {
if TMUX.get() {
crossterm::execute!(TTY.writer(), crossterm::style::Print(Self::csi("\x1b[5n")))?;
writef!(TTY.writer(), "{}", Self::wrap(RequestDeviceStatus))?;
_ = Emulator::read_until_dsr();
}
Ok(())

View file

@ -24,7 +24,6 @@ yazi-boot = { path = "../yazi-boot", version = "26.5.6" }
yazi-config = { path = "../yazi-config", version = "26.5.6" }
yazi-core = { path = "../yazi-core", version = "26.5.6" }
yazi-dds = { path = "../yazi-dds", version = "26.5.6" }
yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" }
yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-parser = { path = "../yazi-parser", version = "26.5.6" }
@ -36,6 +35,7 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
yazi-tui = { path = "../yazi-tui", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
@ -43,7 +43,6 @@ yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies
anyhow = { workspace = true }
better-panic = "0.3.0"
crossterm = { workspace = true }
fdlimit = "0.3.0"
futures = { workspace = true }
mlua = { workspace = true }
@ -60,9 +59,6 @@ tracing-subscriber = { version = "0.3.23", features = [ "env-filter" ] }
libc = { workspace = true }
signal-hook-tokio = { version = "0.4.0", features = [ "futures-v0_3" ] }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }
[target.'cfg(all(not(target_os = "macos"), not(target_os = "windows")))'.dependencies]
tikv-jemallocator = "0.6.1"

View file

@ -6,14 +6,13 @@ use yazi_actor::Ctx;
use yazi_core::Core;
use yazi_macro::act;
use yazi_shared::{data::Data, event::{Event, NEED_RENDER}};
use yazi_term::Term;
use yazi_tui::Raterm;
use crate::{Dispatcher, Signals};
pub(crate) struct App {
pub(crate) core: Core,
pub(crate) term: Option<Term>,
pub(crate) signals: Signals,
pub(crate) core: Core,
pub(crate) term: Option<Raterm>,
need_render: u8,
pub(crate) last_render: Instant,
@ -21,11 +20,10 @@ pub(crate) struct App {
}
impl App {
fn make(term: Term, signals: Signals) -> Result<Self> {
fn make(term: Raterm) -> Result<Self> {
Ok(Self {
core: Core::make(),
term: Some(term),
signals,
need_render: 0,
last_render: Instant::now(),
@ -34,12 +32,13 @@ impl App {
}
pub(crate) async fn serve() -> Result<()> {
let term = Term::start()?;
let (mut rx, signals) = (Event::take(), Signals::start()?);
let term = Raterm::start()?;
Signals::start()?;
let mut app = Self::make(term, signals)?;
let mut app = Self::make(term)?;
app.bootstrap()?;
let mut rx = Event::take();
loop {
if let Some(t) = app.next_render.take() {
select! {

View file

@ -1,15 +1,16 @@
use std::{sync::atomic::{AtomicU8, Ordering}, time::Instant};
use std::{io::Write, sync::atomic::{AtomicU8, Ordering}, time::Instant};
use anyhow::Result;
use crossterm::{cursor::{MoveTo, SetCursorStyle, Show}, execute, queue, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}};
use ratatui::{CompletedFrame, backend::{Backend, CrosstermBackend}, buffer::Buffer, layout::Position};
use ratatui::{CompletedFrame, backend::Backend, buffer::Buffer, layout::Position};
use yazi_actor::{Ctx, lives::Lives};
use yazi_binding::runtime_scope;
use yazi_config::LAYOUT;
use yazi_macro::{act, succ};
use yazi_macro::{act, succ, writef};
use yazi_plugin::LUA;
use yazi_shared::{data::Data, event::NEED_RENDER};
use yazi_term::{CursorStyle, sequence::{BeginSyncUpdate, EndSyncUpdate, MoveTo, SetCursorStyle, ShowCursor}};
use yazi_tty::TTY;
use yazi_tui::RatermBackend;
use yazi_widgets::COLLISION;
use crate::{app::App, root::Root};
@ -90,10 +91,10 @@ impl App {
}
let patches = frame.buffer.diff(&new);
CrosstermBackend::new(&mut *TTY.lockout()).draw(patches.into_iter()).ok();
RatermBackend::new(&mut *TTY.lockout()).draw(patches.into_iter()).ok();
}
fn routine(push: bool, cursor: Option<(Position, SetCursorStyle)>) {
fn routine(push: bool, cursor: Option<(Position, CursorStyle)>) {
static COUNT: AtomicU8 = AtomicU8::new(0);
if push && COUNT.fetch_add(1, Ordering::Relaxed) != 0 {
return;
@ -101,12 +102,18 @@ impl App {
return;
}
_ = if push {
queue!(TTY.writer(), BeginSynchronizedUpdate)
if push {
write!(TTY.writer(), "{BeginSyncUpdate}").ok();
} else if let Some((Position { x, y }, shape)) = cursor {
execute!(TTY.writer(), shape, MoveTo(x, y), Show, EndSynchronizedUpdate)
writef!(
TTY.writer(),
"{}{}{ShowCursor}{EndSyncUpdate}",
SetCursorStyle(shape as u8),
MoveTo(x, y),
)
.ok();
} else {
execute!(TTY.writer(), EndSynchronizedUpdate)
writef!(TTY.writer(), "{EndSyncUpdate}").ok();
};
}
}

View file

@ -3,8 +3,8 @@ use std::path::MAIN_SEPARATOR_STR;
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, List, ListItem, Widget}};
use yazi_config::{THEME, popup::{Offset, Position}};
use yazi_core::Core;
use yazi_emulator::Dimension;
use yazi_shared::strand::StrandLike;
use yazi_term::TERM;
pub(crate) struct Cmp<'a> {
core: &'a Core,
@ -38,7 +38,7 @@ impl Widget for Cmp<'_> {
.collect();
let input_area = self.core.mgr.area(self.core.input.position);
let mut area = Position::sticky(Dimension::available().into(), input_area, Offset {
let mut area = Position::sticky(TERM.dimension(), input_area, Offset {
x: 1,
y: 0,
width: input_area.width.saturating_sub(2),

View file

@ -1,12 +1,12 @@
use std::sync::atomic::Ordering;
use anyhow::Result;
use crossterm::event::{KeyEvent, MouseEvent};
use tracing::warn;
use yazi_actor::Ctx;
use yazi_config::keymap::Key;
use yazi_macro::{act, emit};
use yazi_shared::event::{ActionCow, Event, NEED_RENDER};
use yazi_term::event::{Event as TermEvent, KeyEvent, MouseEvent};
use yazi_widgets::input::InputMode;
use crate::{Executor, Router, app::App};
@ -23,11 +23,12 @@ impl<'a> Dispatcher<'a> {
Event::Call(action) => Ok(self.dispatch_call(action)),
Event::Seq(actions) => Ok(self.dispatch_seq(actions)),
Event::Render(partial) => self.dispatch_render(partial),
Event::Key(key) => self.dispatch_key(key),
Event::Mouse(mouse) => self.dispatch_mouse(mouse),
Event::Resize => self.dispatch_resize(),
Event::Focus => self.dispatch_focus(),
Event::Paste(str) => self.dispatch_paste(str),
Event::Term(TermEvent::Key(key)) => self.dispatch_key(key),
Event::Term(TermEvent::Mouse(mouse)) => self.dispatch_mouse(mouse),
Event::Term(TermEvent::Resize(_)) => self.dispatch_resize(),
Event::Term(TermEvent::FocusIn) => self.dispatch_focus(),
Event::Term(TermEvent::FocusOut) => Ok(()),
Event::Term(TermEvent::Paste(str)) => self.dispatch_paste(str),
};
if let Err(e) = &result {

View file

@ -1,4 +1,4 @@
use anyhow::{Context, Result};
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_macro::{act, succ};
use yazi_shared::{Layer, data::Data, event::ActionCow};
@ -33,7 +33,7 @@ impl<'a> Executor<'a> {
fn null(&mut self, _action: ActionCow) -> Result<Data> { succ!() }
fn app(&mut self, mut action: ActionCow) -> Result<Data> {
fn app(&mut self, action: ActionCow) -> Result<Data> {
let cx = &mut Ctx::new(&action, &mut self.app.core, &mut self.app.term)?;
macro_rules! on {
@ -51,19 +51,12 @@ impl<'a> Executor<'a> {
on!(lua);
on!(deprecate);
on!(theme);
on!(stop);
on!(quit);
match &*action.name {
"resize" => act!(app:resize, cx, crate::Root::reflow as fn(_) -> _),
"resume" => act!(app:resume, cx, yazi_parser::app::ResumeForm {
tx: self.app.signals.tx.clone(),
reflow: crate::Root::reflow,
replier: action.take_replier().context("Missing replier in ResumeForm")?,
}),
"stop" => act!(app:stop, cx, yazi_parser::app::StopForm {
tx: self.app.signals.tx.clone(),
replier: action.take_replier().context("Missing replier in StopForm")?,
}),
"resume" => act!(app:resume, cx, crate::Root::reflow as fn(_) -> _),
_ => succ!(),
}
}

View file

@ -1,12 +1,14 @@
use std::fs::File;
use std::{fs::File, io};
use anyhow::Context;
use crossterm::style::{Color, Print, ResetColor, SetForegroundColor};
use ratatui::style::Color;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter;
use yazi_fs::Xdg;
use yazi_macro::writef;
use yazi_shared::LOG_LEVEL;
use yazi_shim::cell::RoCell;
use yazi_term::sequence::{SetFg, SetSgr};
static _GUARD: RoCell<WorkerGuard> = RoCell::new();
@ -36,11 +38,11 @@ impl Logs {
.init();
_GUARD.init(guard);
Ok(crossterm::execute!(
std::io::stderr(),
SetForegroundColor(Color::Yellow),
Print(format!("Running with log level `{level}`, logs are written to {log_path:?}\n")),
ResetColor
Ok(writef!(
io::stderr(),
"{}Running with log level `{level}`, logs are written to {log_path:?}\n{}",
SetFg(Color::Yellow),
SetSgr::Reset,
)?)
}
}

View file

@ -16,7 +16,7 @@ async fn main() -> anyhow::Result<()> {
yazi_tty::init();
yazi_term::init();
yazi_term::init()?;
yazi_fs::init();

View file

@ -1,4 +1,4 @@
use yazi_term::Term;
use yazi_tui::Raterm;
pub(super) struct Panic;
@ -8,10 +8,8 @@ impl Panic {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
Term::goodbye(|| {
hook(info);
1
});
Raterm::stop();
hook(info);
}));
}
}

View file

@ -1,24 +1,39 @@
use anyhow::Result;
use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind};
use futures::StreamExt;
use tokio::{select, sync::mpsc};
use yazi_config::YAZI;
use yazi_shared::event::{Event, Replier};
use tokio_stream::StreamExt;
pub(super) struct Signals {
pub(super) tx: mpsc::UnboundedSender<(bool, Replier)>,
}
pub(super) struct Signals;
impl Signals {
pub(super) fn start() -> Result<Self> {
let (tx, rx) = mpsc::unbounded_channel();
Self::spawn(rx)?;
pub(super) fn start() -> Result<()> {
#[cfg(unix)]
use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGTSTP};
Ok(Self { tx })
#[cfg(unix)]
let mut stream = signal_hook_tokio::Signals::new([
// Interrupt signals (Ctrl-C, Ctrl-\)
SIGINT, SIGQUIT, //
// Hangup signal (Terminal closed)
SIGHUP, //
// Termination signal (kill)
SIGTERM, //
// Job control signals (Ctrl-Z, fg/bg)
SIGTSTP, SIGCONT,
])?;
#[cfg(windows)]
let mut stream = tokio_stream::empty();
tokio::spawn(async move {
while let Some(n) = stream.next().await {
if !Self::handle(n).await {
break;
}
}
});
Ok(())
}
#[cfg(unix)]
fn handle_sys(n: libc::c_int) -> bool {
async fn handle(n: libc::c_int) -> bool {
use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGSTOP, SIGTERM, SIGTSTP};
use tracing::error;
use yazi_term::YIELD_TO_SUBPROCESS;
@ -30,16 +45,14 @@ impl Signals {
return false;
}
SIGTSTP => {
tokio::spawn(async move {
yazi_scheduler::AppProxy::stop().await;
if unsafe { libc::kill(0, SIGSTOP) } != 0 {
error!("Failed to stop the process:\n{}", std::io::Error::last_os_error());
yazi_proxy::AppProxy::quit(Default::default());
}
});
yazi_scheduler::AppProxy::stop().await;
if unsafe { libc::kill(0, SIGSTOP) } != 0 {
error!("Failed to stop the process:\n{}", std::io::Error::last_os_error());
yazi_proxy::AppProxy::quit(Default::default());
}
}
SIGCONT if YIELD_TO_SUBPROCESS.try_acquire().is_ok() => {
tokio::spawn(yazi_scheduler::AppProxy::resume());
yazi_scheduler::AppProxy::resume().await;
}
_ => {}
}
@ -47,69 +60,5 @@ impl Signals {
}
#[cfg(windows)]
#[inline]
fn handle_sys(_: ()) -> bool { unreachable!() }
fn handle_term(event: CrosstermEvent) {
match event {
CrosstermEvent::Key(key @ KeyEvent { kind: KeyEventKind::Press, .. }) => {
Event::Key(key).emit()
}
CrosstermEvent::Mouse(mouse) if YAZI.mgr.mouse_events.get().contains(mouse.kind.into()) => {
Event::Mouse(mouse).emit()
}
CrosstermEvent::Resize(..) => Event::Resize.emit(),
CrosstermEvent::FocusGained => Event::Focus.emit(),
CrosstermEvent::Paste(str) => Event::Paste(str).emit(),
_ => {}
}
}
fn spawn(mut rx: mpsc::UnboundedReceiver<(bool, Replier)>) -> Result<()> {
#[cfg(unix)]
use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGTSTP};
#[cfg(unix)]
let mut sys = signal_hook_tokio::Signals::new([
// Interrupt signals (Ctrl-C, Ctrl-\)
SIGINT, SIGQUIT, //
// Hangup signal (Terminal closed)
SIGHUP, //
// Termination signal (kill)
SIGTERM, //
// Job control signals (Ctrl-Z, fg/bg)
SIGTSTP, SIGCONT,
])?;
#[cfg(windows)]
let mut sys = tokio_stream::empty();
let mut term = Some(EventStream::new());
tokio::spawn(async move {
loop {
if let Some(t) = &mut term {
select! {
biased;
Some((state, replier)) = rx.recv() => {
term = term.filter(|_| state);
replier.send(Ok(().into())).ok();
},
Some(n) = sys.next() => if !Self::handle_sys(n) { return },
Some(Ok(e)) = t.next() => Self::handle_term(e)
}
} else {
select! {
biased;
Some((state, replier)) = rx.recv() => {
term = state.then(EventStream::new);
replier.send(Ok(().into())).ok();
},
Some(n) = sys.next() => if !Self::handle_sys(n) { return },
}
}
}
});
Ok(())
}
async fn handle(_: ()) -> bool { unreachable!() }
}

View file

@ -59,6 +59,7 @@ impl Cha {
where
U: AsUrl,
{
#[allow(unused_mut)]
let mut kind = ChaKind::DUMMY;
let mode = r#type.map(ChaMode::from_bare).unwrap_or_default();

View file

@ -52,9 +52,9 @@ impl TryFrom<Attrs> for std::fs::FileTimes {
impl TryFrom<Attrs> for std::fs::Permissions {
type Error = ();
fn try_from(value: Attrs) -> Result<Self, Self::Error> {
fn try_from(_value: Attrs) -> Result<Self, Self::Error> {
#[cfg(unix)]
if let Some(mode) = value.mode {
if let Some(mode) = _value.mode {
return Ok(mode.into());
}

View file

@ -13,3 +13,13 @@ macro_rules! errln {
writeln!(std::io::stderr(), $($tt)*)
}}
}
/// Like [`write!`] but immediately flushes the writer afterwards.
#[macro_export]
macro_rules! writef {
($dst:expr, $($arg:tt)*) => {{
use std::io::Write as _;
let dst = &mut $dst;
write!(dst, $($arg)*).and_then(|_| dst.flush())
}};
}

View file

@ -26,13 +26,13 @@ yazi-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies
anyhow = { workspace = true }
bitflags = { workspace = true }
crossterm = { workspace = true }
hashbrown = { workspace = true }
mlua = { workspace = true }
paste = { workspace = true }
@ -41,6 +41,3 @@ serde = { workspace = true }
serde_with = { workspace = true }
strum = { workspace = true }
tokio = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(deprecate lua mouse plugin quit reflow resume stop title update_progress);
yazi_macro::mod_flat!(deprecate lua mouse plugin quit reflow title update_progress);

View file

@ -1,5 +1,5 @@
use crossterm::event::MouseEvent;
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_term::event::MouseEvent;
#[derive(Debug)]
pub struct MouseForm {

View file

@ -1,19 +0,0 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, Value};
use ratatui::layout::Rect;
use tokio::sync::mpsc;
use yazi_shared::event::Replier;
#[derive(Debug)]
pub struct ResumeForm {
pub tx: mpsc::UnboundedSender<(bool, Replier)>,
pub reflow: fn(Rect) -> mlua::Result<Table>,
pub replier: Replier,
}
impl FromLua for ResumeForm {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for ResumeForm {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -1,17 +0,0 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use tokio::sync::mpsc;
use yazi_shared::event::Replier;
#[derive(Debug)]
pub struct StopForm {
pub tx: mpsc::UnboundedSender<(bool, Replier)>,
pub replier: Replier,
}
impl FromLua for StopForm {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for StopForm {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -19,8 +19,8 @@ pub enum Spark<'a> {
AppQuit(crate::app::QuitForm),
AppReflow(crate::app::ReflowForm),
AppResize(crate::app::ReflowForm),
AppResume(crate::app::ResumeForm),
AppStop(crate::app::StopForm),
AppResume(crate::app::ReflowForm),
AppStop(crate::VoidForm),
AppTheme(crate::VoidForm),
AppTitle(crate::app::TitleForm),
AppUpdateProgress(crate::app::UpdateProgressForm),
@ -352,6 +352,7 @@ try_from_spark!(
crate::VoidForm,
app:bootstrap,
app:focus,
app:stop,
app:theme,
mgr:back,
mgr:bulk_rename,
@ -379,9 +380,7 @@ try_from_spark!(crate::app::LuaForm, app:lua);
try_from_spark!(crate::app::MouseForm, app:mouse);
try_from_spark!(crate::app::PluginForm, app:plugin, app:plugin_do);
try_from_spark!(crate::app::QuitForm, app:quit, mgr:quit);
try_from_spark!(crate::app::ReflowForm, app:reflow, app:resize);
try_from_spark!(crate::app::ResumeForm, app:resume);
try_from_spark!(crate::app::StopForm, app:stop);
try_from_spark!(crate::app::ReflowForm, app:reflow, app:resize, app:resume);
try_from_spark!(crate::app::TitleForm, app:title);
try_from_spark!(crate::app::UpdateProgressForm, app:update_progress);
try_from_spark!(crate::cmp::CloseForm, cmp:close);

View file

@ -25,6 +25,7 @@ yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-runner = { path = "../yazi-runner", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tui = { path = "../yazi-tui", version = "26.5.6" }
yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
# External dependencies

View file

@ -15,10 +15,10 @@ workspace = true
[dependencies]
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
# External dependencies
anyhow = { workspace = true }
crossterm = { workspace = true }
dyn-clone = { workspace = true }
foldhash = { workspace = true }
futures = { workspace = true }
@ -41,6 +41,3 @@ uzers = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61.2", features = [ "Win32_UI_Shell" ] }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -1,4 +1,4 @@
use std::{any::Any, fmt};
use std::{any::Any, fmt::{self, Debug}};
use dyn_clone::DynClone;
@ -21,7 +21,7 @@ impl Clone for Box<dyn DataAny> {
fn clone(&self) -> Self { dyn_clone::clone_box(&**self) }
}
impl fmt::Debug for dyn DataAny {
impl Debug for dyn DataAny {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DataAny").finish_non_exhaustive()
}

View file

@ -1,4 +1,3 @@
use crossterm::event::{KeyEvent, MouseEvent};
use tokio::sync::mpsc;
use yazi_shim::cell::RoCell;
@ -11,12 +10,8 @@ static RX: RoCell<mpsc::UnboundedReceiver<Event>> = RoCell::new();
pub enum Event {
Call(ActionCow),
Seq(Vec<ActionCow>),
Term(yazi_term::event::Event),
Render(bool),
Key(KeyEvent),
Mouse(MouseEvent),
Resize,
Focus,
Paste(String),
}
impl Event {

View file

@ -1,6 +1,6 @@
yazi_macro::mod_pub!(data event loc path pool scheme shell strand translit url wtf8);
yazi_macro::mod_flat!(alias bytes chars completion_token condition debounce env id kebab_cased_string last_value layer localset natsort non_empty_string os predictor snake_cased_string source terminal tests throttle time utf8);
yazi_macro::mod_flat!(alias bytes chars completion_token condition debounce env id kebab_cased_string last_value layer localset natsort non_empty_string os predictor snake_cased_string source tests throttle time);
pub fn init() {
LOCAL_SET.with(tokio::task::LocalSet::new);

View file

@ -1,12 +0,0 @@
use std::io::Write;
use crossterm::execute;
#[inline]
pub fn terminal_clear(mut w: impl Write) -> std::io::Result<()> {
execute!(
w,
crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
crossterm::style::Print("\n")
)
}

View file

@ -368,10 +368,10 @@ impl<'a> Url<'a> {
(U::Regular(_), U::Regular(_)) => Ok(prefix),
(U::Search { .. }, U::Search { .. }) => Ok(prefix),
(U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => {
Some(prefix).filter(|_| a == b).ok_or(Exotic)
(a == b).then_some(prefix).ok_or(Exotic)
}
(U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => {
Some(prefix).filter(|_| a == b).ok_or(Exotic)
(a == b).then_some(prefix).ok_or(Exotic)
}
// Both are local files
@ -380,16 +380,16 @@ impl<'a> Url<'a> {
// Only the entry of archives is a local file
(U::Regular(_), U::Archive { .. }) => {
Some(prefix).filter(|_| base.uri().is_empty()).ok_or(NotPrefix)
base.uri().is_empty().then_some(prefix).ok_or(NotPrefix)
}
(U::Search { .. }, U::Archive { .. }) => {
Some(prefix).filter(|_| base.uri().is_empty()).ok_or(NotPrefix)
base.uri().is_empty().then_some(prefix).ok_or(NotPrefix)
}
(U::Archive { .. }, U::Regular(_)) => {
Some(prefix).filter(|_| self.uri().is_empty()).ok_or(NotPrefix)
self.uri().is_empty().then_some(prefix).ok_or(NotPrefix)
}
(U::Archive { .. }, U::Search { .. }) => {
Some(prefix).filter(|_| self.uri().is_empty()).ok_or(NotPrefix)
self.uri().is_empty().then_some(prefix).ok_or(NotPrefix)
}
// Independent virtual file space
@ -414,10 +414,10 @@ impl<'a> Url<'a> {
(U::Regular(_), U::Regular(_)) => Ok(suffix),
(U::Search { .. }, U::Search { .. }) => Ok(suffix),
(U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => {
Some(suffix).filter(|_| a == b).ok_or(Exotic)
(a == b).then_some(suffix).ok_or(Exotic)
}
(U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => {
Some(suffix).filter(|_| a == b).ok_or(Exotic)
(a == b).then_some(suffix).ok_or(Exotic)
}
// Both are local files
@ -426,16 +426,16 @@ impl<'a> Url<'a> {
// Only the entry of archives is a local file
(U::Regular(_), U::Archive { .. }) => {
Some(suffix).filter(|_| other.uri().is_empty()).ok_or(NotSuffix)
other.uri().is_empty().then_some(suffix).ok_or(NotSuffix)
}
(U::Search { .. }, U::Archive { .. }) => {
Some(suffix).filter(|_| other.uri().is_empty()).ok_or(NotSuffix)
other.uri().is_empty().then_some(suffix).ok_or(NotSuffix)
}
(U::Archive { .. }, U::Regular(_)) => {
Some(suffix).filter(|_| self.uri().is_empty()).ok_or(NotSuffix)
self.uri().is_empty().then_some(suffix).ok_or(NotSuffix)
}
(U::Archive { .. }, U::Search { .. }) => {
Some(suffix).filter(|_| self.uri().is_empty()).ok_or(NotSuffix)
self.uri().is_empty().then_some(suffix).ok_or(NotSuffix)
}
// Independent virtual file space

View file

@ -1,53 +0,0 @@
// https://tools.ietf.org/html/rfc3629
const UTF8_CHAR_WIDTH: &[u8; 256] = &[
// 1 2 3 4 5 6 7 8 9 A B C D E F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B
0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // D
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // E
4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F
];
/// Given a first byte, determines how many bytes are in this UTF-8 character.
#[must_use]
#[inline]
pub const fn utf8_char_width(b: u8) -> usize { UTF8_CHAR_WIDTH[b as usize] as usize }
/// Finds the closest `x` not exceeding `index` where [`is_char_boundary(x)`] is
/// `true`.
///
/// This method can help you truncate a string so that it's still valid UTF-8,
/// but doesn't exceed a given number of bytes. Note that this is done purely at
/// the character level and can still visually split graphemes, even though the
/// underlying characters aren't split. For example, the emoji 🧑‍🔬 (scientist)
/// could be split so that the string only includes 🧑 (person) instead.
#[inline]
pub fn floor_char_boundary(s: &str, index: usize) -> usize {
if index >= s.len() {
s.len()
} else {
let lower_bound = index.saturating_sub(3);
let new_index =
s.as_bytes()[lower_bound..=index].iter().rposition(|&b| is_utf8_char_boundary(b));
// SAFETY: we know that the character boundary will be within four bytes
unsafe { lower_bound + new_index.unwrap_unchecked() }
}
}
#[inline]
const fn is_utf8_char_boundary(b: u8) -> bool {
// This is bit magic equivalent to: b < 128 || b >= 192
(b as i8) >= -0x40
}

View file

@ -21,7 +21,6 @@ yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
# External dependencies
arc-swap = { workspace = true }
crossterm = { workspace = true }
hashbrown = { workspace = true }
mlua = { workspace = true }
ratatui = { workspace = true }
@ -33,6 +32,3 @@ unicode-width = { workspace = true }
[dependencies.unicode-segmentation]
version = "1.13.2"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -1,15 +0,0 @@
pub struct If<T: crossterm::Command>(pub bool, pub T);
impl<T: crossterm::Command> crossterm::Command for If<T> {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
if self.0 { self.1.write_ansi(f) } else { Ok(()) }
}
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
if self.0 { self.1.execute_winapi() } else { Ok(()) }
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool { self.1.is_ansi_code_supported() }
}

View file

@ -1 +0,0 @@
yazi_macro::mod_flat!(r#if restore_background restore_cursor set_background);

View file

@ -1,10 +0,0 @@
pub struct RestoreBackground;
impl crossterm::Command for RestoreBackground {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
write!(f, "\x1b]111\x1b\\")
}
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) }
}

View file

@ -1,29 +0,0 @@
use crossterm::cursor::SetCursorStyle;
pub struct RestoreCursor {
pub shape: u8,
pub blink: bool,
}
impl crossterm::Command for RestoreCursor {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
let (shape, shape_blink) = match self.shape {
u8::MAX => (0, None),
n => (n.max(1).div_ceil(2), Some(n.max(1) & 1 == 1)),
};
let blink = shape_blink.unwrap_or(self.blink);
Ok(match shape {
2 if blink => SetCursorStyle::BlinkingUnderScore.write_ansi(f)?,
2 if !blink => SetCursorStyle::SteadyUnderScore.write_ansi(f)?,
3 if blink => SetCursorStyle::BlinkingBar.write_ansi(f)?,
3 if !blink => SetCursorStyle::SteadyBar.write_ansi(f)?,
_ if blink => SetCursorStyle::DefaultUserShape.write_ansi(f)?,
_ if !blink => SetCursorStyle::SteadyBlock.write_ansi(f)?,
_ => unreachable!(),
})
}
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) }
}

View file

@ -1,10 +0,0 @@
pub struct SetBackground<'a>(pub &'a str);
impl crossterm::Command for SetBackground<'_> {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
if self.0.is_empty() { Ok(()) } else { write!(f, "\x1b]11;{}\x1b\\", self.0) }
}
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) }
}

View file

@ -1,3 +1,6 @@
yazi_macro::mod_pub!(arc_swap cell crossterm mlua path ratatui serde strum toml vec);
yazi_macro::mod_pub!(arc_swap cell mlua path ratatui serde strum toml vec);
yazi_macro::mod_flat!(twox);
yazi_macro::mod_flat!(twox utf8);
#[cfg(windows)]
yazi_macro::mod_flat!(win32);

25
yazi-shim/src/utf8.rs Normal file
View file

@ -0,0 +1,25 @@
// https://tools.ietf.org/html/rfc3629
const UTF8_CHAR_WIDTH: &[u8; 256] = &[
// 1 2 3 4 5 6 7 8 9 A B C D E F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B
0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // D
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // E
4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F
];
/// Given a first byte, determines how many bytes are in this UTF-8 character.
#[must_use]
#[inline]
pub const fn utf8_char_width(b: u8) -> u8 { UTF8_CHAR_WIDTH[b as usize] }

13
yazi-shim/src/win32.rs Normal file
View file

@ -0,0 +1,13 @@
use std::io;
/// Maps a Win32 BOOL return value to `io::Result<()>`; 0 becomes
/// `Err(last_os_error())`.
pub fn bool_ok(r: i32) -> io::Result<()> {
if r == 0 { Err(io::Error::last_os_error()) } else { Ok(()) }
}
/// Maps a Win32 non-zero-success return value to `io::Result<u32>`; 0 becomes
/// `Err(last_os_error())`.
pub fn nz_ok(r: u32) -> io::Result<u32> {
if r == 0 { Err(io::Error::last_os_error()) } else { Ok(r) }
}

View file

@ -1,6 +1,6 @@
[package]
name = "yazi-term"
description = "Yazi terminal extensions"
description = "Cross-platform virtual terminal"
version.workspace = true
edition.workspace = true
license.workspace = true
@ -13,17 +13,23 @@ rust-version.workspace = true
workspace = true
[dependencies]
yazi-config = { path = "../yazi-config", version = "26.5.6" }
yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
# External dependencies
anyhow = { workspace = true }
crossterm = { workspace = true }
ratatui = { workspace = true }
tokio = { workspace = true }
anyhow = { workspace = true }
base64 = { workspace = true }
bitflags = { workspace = true }
futures = { workspace = true }
parking_lot = { workspace = true }
ratatui = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }
[target.'cfg(unix)'.dependencies]
signal-hook = "0.4.4"
rustix = { version = "1.1.4" , default-features = false, features = [ "std", "stdio", "termios", "event" ] }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61.2", features = [ "Win32_Globalization", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Threading" ] }

22
yazi-term/LICENSE Normal file
View file

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2025 - Michael Davis
Copyright (c) 2026 - sxyazi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,5 +1,3 @@
# yazi-term
This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API.
[source]: https://github.com/sxyazi/yazi
A fork of [Termina](https://github.com/helix-editor/termina) used by Yazi.

View file

@ -0,0 +1,41 @@
use std::io;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Dimension {
pub cols: u16,
pub rows: u16,
pub width: u16,
pub height: u16,
}
#[cfg(unix)]
impl From<rustix::termios::Winsize> for Dimension {
fn from(size: rustix::termios::Winsize) -> Self {
Self {
cols: size.ws_col,
rows: size.ws_row,
width: size.ws_xpixel,
height: size.ws_ypixel,
}
}
}
impl Dimension {
pub fn checked(self) -> io::Result<Self> {
if self.cols == 0 || self.rows == 0 {
Err(io::Error::other(
"failed to get terminal dimension with both ioctl and $LINES/$COLUMNS variables",
))
} else {
Ok(self)
}
}
pub fn ratio(self) -> Option<(f64, f64)> {
if self.rows == 0 || self.cols == 0 || self.width == 0 || self.height == 0 {
None
} else {
Some((self.width as f64 / self.cols as f64, self.height as f64 / self.rows as f64))
}
}
}

21
yazi-term/src/error.rs Normal file
View file

@ -0,0 +1,21 @@
use std::str;
pub type Result<T, E = ParseError> = std::result::Result<T, E>;
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
/// Malformed or unrecognized byte sequence.
#[error("invalid or unrecognized byte sequence")]
Invalid,
/// Recognised sequence that should not be emitted as an event.
#[error("recognised sequence that should be ignored")]
Ignored,
/// The sequence is not yet complete; more bytes are needed before it can be
/// parsed.
#[error("incomplete sequence; more bytes are needed")]
Incomplete,
}
impl From<str::Utf8Error> for ParseError {
fn from(_: str::Utf8Error) -> Self { Self::Invalid }
}

View file

@ -0,0 +1,11 @@
use crate::{Dimension, event::{KeyEvent, MouseEvent}};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Event {
Key(KeyEvent),
Mouse(MouseEvent),
Resize(Dimension),
FocusIn,
FocusOut,
Paste(String),
}

View file

@ -0,0 +1,268 @@
use bitflags::bitflags;
use crate::{ParseError, Result, bail, event::Modifiers};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyEvent {
pub code: KeyCode,
pub kind: KeyEventKind,
pub modifiers: Modifiers,
pub state: KeyEventState,
}
impl KeyEvent {
pub const fn new(code: KeyCode, modifiers: Modifiers) -> Self {
Self { code, kind: KeyEventKind::Press, modifiers, state: KeyEventState::empty() }
}
}
impl From<KeyCode> for KeyEvent {
fn from(code: KeyCode) -> Self {
Self {
code,
kind: KeyEventKind::Press,
modifiers: Modifiers::empty(),
state: KeyEventState::empty(),
}
}
}
// --- Kind
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum KeyEventKind {
#[default]
Press,
Release,
Repeat,
}
impl KeyEventKind {
pub(crate) fn from_vt_code(code: u8) -> Self {
match code {
2 => Self::Repeat,
3 => Self::Release,
_ => Self::Press,
}
}
}
// --- State
bitflags! {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct KeyEventState: u8 {
const KEYPAD = 1;
const CAPS_LOCK = 2;
const NUM_LOCK = 4;
}
}
impl KeyEventState {
pub(crate) fn from_vt_mask(mask: u8) -> Self {
let m = mask.saturating_sub(1);
let mut state = Self::empty();
if m & 64 != 0 {
state |= Self::CAPS_LOCK;
}
if m & 128 != 0 {
state |= Self::NUM_LOCK;
}
state
}
}
// --- Code
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum KeyCode {
Char(char),
Enter,
Backspace,
Tab,
Escape,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
Insert,
Delete,
KeypadBegin,
CapsLock,
ScrollLock,
NumLock,
PrintScreen,
Pause,
Menu,
Null,
Fn(u8),
Modifier(ModifierKeyCode),
Media(MediaKeyCode),
}
impl KeyCode {
pub(crate) fn from_xterm_modifier(r#final: u8) -> Result<Self> {
Ok(match r#final {
b'A' => KeyCode::Up,
b'B' => KeyCode::Down,
b'C' => KeyCode::Right,
b'D' => KeyCode::Left,
b'F' => KeyCode::End,
b'H' => KeyCode::Home,
b'P' => KeyCode::Fn(1),
b'Q' => KeyCode::Fn(2),
b'R' => KeyCode::Fn(3),
b'S' => KeyCode::Fn(4),
_ => bail!(),
})
}
pub(crate) fn from_codepoint(codepoint: u32) -> Result<(Self, KeyEventState)> {
if let Ok(pair) = Self::from_kitty_codepoint(codepoint) {
return Ok(pair);
}
let code = match char::from_u32(codepoint).ok_or(ParseError::Invalid)? {
'\x1B' => Self::Escape,
'\r' => Self::Enter,
'\t' => Self::Tab,
'\x7F' => Self::Backspace,
c => Self::Char(c),
};
Ok((code, KeyEventState::empty()))
}
fn from_kitty_codepoint(codepoint: u32) -> Result<(Self, KeyEventState)> {
// Keypad keys
if (57399..=57427).contains(&codepoint) {
let code = match codepoint {
57399 => Self::Char('0'),
57400 => Self::Char('1'),
57401 => Self::Char('2'),
57402 => Self::Char('3'),
57403 => Self::Char('4'),
57404 => Self::Char('5'),
57405 => Self::Char('6'),
57406 => Self::Char('7'),
57407 => Self::Char('8'),
57408 => Self::Char('9'),
57409 => Self::Char('.'),
57410 => Self::Char('/'),
57411 => Self::Char('*'),
57412 => Self::Char('-'),
57413 => Self::Char('+'),
57414 => Self::Enter,
57415 => Self::Char('='),
57416 => Self::Char(','),
57417 => Self::Left,
57418 => Self::Right,
57419 => Self::Up,
57420 => Self::Down,
57421 => Self::PageUp,
57422 => Self::PageDown,
57423 => Self::Home,
57424 => Self::End,
57425 => Self::Insert,
57426 => Self::Delete,
57427 => Self::KeypadBegin,
_ => unreachable!(),
};
return Ok((code, KeyEventState::KEYPAD));
}
// F13F35 keys
if (57376..=57398).contains(&codepoint) {
return Ok((Self::Fn((codepoint - 57363) as u8), KeyEventState::empty()));
}
let code = match codepoint {
57358 => Self::CapsLock,
57359 => Self::ScrollLock,
57360 => Self::NumLock,
57361 => Self::PrintScreen,
57362 => Self::Pause,
57363 => Self::Menu,
57428 => Self::Media(MediaKeyCode::Play),
57429 => Self::Media(MediaKeyCode::Pause),
57430 => Self::Media(MediaKeyCode::PlayPause),
57431 => Self::Media(MediaKeyCode::Reverse),
57432 => Self::Media(MediaKeyCode::Stop),
57433 => Self::Media(MediaKeyCode::FastForward),
57434 => Self::Media(MediaKeyCode::Rewind),
57435 => Self::Media(MediaKeyCode::NextTrack),
57436 => Self::Media(MediaKeyCode::PreviousTrack),
57437 => Self::Media(MediaKeyCode::Record),
57438 => Self::Media(MediaKeyCode::LowerVolume),
57439 => Self::Media(MediaKeyCode::RaiseVolume),
57440 => Self::Media(MediaKeyCode::MuteVolume),
57441 => Self::Modifier(ModifierKeyCode::LeftShift),
57442 => Self::Modifier(ModifierKeyCode::LeftControl),
57443 => Self::Modifier(ModifierKeyCode::LeftAlt),
57444 => Self::Modifier(ModifierKeyCode::LeftSuper),
57445 => Self::Modifier(ModifierKeyCode::LeftHyper),
57446 => Self::Modifier(ModifierKeyCode::LeftMeta),
57447 => Self::Modifier(ModifierKeyCode::RightShift),
57448 => Self::Modifier(ModifierKeyCode::RightControl),
57449 => Self::Modifier(ModifierKeyCode::RightAlt),
57450 => Self::Modifier(ModifierKeyCode::RightSuper),
57451 => Self::Modifier(ModifierKeyCode::RightHyper),
57452 => Self::Modifier(ModifierKeyCode::RightMeta),
57453 => Self::Modifier(ModifierKeyCode::IsoLevel3Shift),
57454 => Self::Modifier(ModifierKeyCode::IsoLevel5Shift),
_ => bail!(),
};
Ok((code, KeyEventState::empty()))
}
}
// --- Modifier key
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ModifierKeyCode {
LeftShift,
LeftControl,
LeftAlt,
LeftSuper, // Command/Windows/Super key
LeftHyper,
LeftMeta,
RightShift,
RightControl,
RightAlt,
RightSuper, // Command/Windows/Super key
RightHyper,
RightMeta,
IsoLevel3Shift,
IsoLevel5Shift,
}
impl ModifierKeyCode {
pub(crate) fn to_modifier(self) -> Option<Modifiers> {
match self {
Self::LeftShift | Self::RightShift => Some(Modifiers::SHIFT),
Self::LeftControl | Self::RightControl => Some(Modifiers::CONTROL),
Self::LeftAlt | Self::RightAlt => Some(Modifiers::ALT),
Self::LeftSuper | Self::RightSuper => Some(Modifiers::SUPER),
Self::LeftHyper | Self::RightHyper => Some(Modifiers::HYPER),
Self::LeftMeta | Self::RightMeta => Some(Modifiers::META),
_ => None,
}
}
}
// --- Media key
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum MediaKeyCode {
Play,
Pause,
PlayPause,
Reverse,
Stop,
FastForward,
Rewind,
NextTrack,
PreviousTrack,
Record,
LowerVolume,
RaiseVolume,
MuteVolume,
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(event keyboard modifiers mouse);

View file

@ -0,0 +1,21 @@
use bitflags::bitflags;
bitflags! {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Modifiers: u8 {
const SHIFT = 1;
const ALT = 2;
const CONTROL = 4;
const SUPER = 8;
const HYPER = 16;
const META = 32;
}
}
impl Modifiers {
pub(crate) fn from_vt_mask(mask: u8) -> Self { Self::from_bits_truncate(mask.saturating_sub(1)) }
pub(crate) fn for_char(c: char) -> Self {
if c.is_uppercase() { Self::SHIFT } else { Self::empty() }
}
}

View file

@ -0,0 +1,76 @@
use crate::{Result, bail, event::Modifiers};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseEvent {
pub kind: MouseEventKind,
pub column: u16,
pub row: u16,
pub modifiers: Modifiers,
}
// --- Kind
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MouseEventKind {
Down(MouseButton),
Up(MouseButton),
Drag(MouseButton),
Moved,
ScrollDown,
ScrollUp,
ScrollLeft,
ScrollRight,
}
impl MouseEventKind {
/// Cb is the byte of a mouse input that contains the button being used, the
/// key modifiers being held and whether the mouse is dragging or not.
///
/// Bit layout of cb, from low to high:
/// - button number (bits 01)
/// - shift (bit 2)
/// - meta/alt (bit 3)
/// - control (bit 4)
/// - mouse is dragging (bit 5)
/// - button number (bits 67)
pub(crate) fn from_cb(cb: u8) -> Result<(Self, Modifiers)> {
let button = (cb & 0b0000_0011) | ((cb & 0b1100_0000) >> 4);
let dragging = cb & 0b0010_0000 == 0b0010_0000;
let kind = match (button, dragging) {
(0, false) => Self::Down(MouseButton::Left),
(1, false) => Self::Down(MouseButton::Middle),
(2, false) => Self::Down(MouseButton::Right),
(0, true) => Self::Drag(MouseButton::Left),
(1, true) => Self::Drag(MouseButton::Middle),
(2, true) => Self::Drag(MouseButton::Right),
(3, false) => Self::Up(MouseButton::Left),
(3, true) | (4, true) | (5, true) => Self::Moved,
(4, false) => Self::ScrollUp,
(5, false) => Self::ScrollDown,
(6, false) => Self::ScrollLeft,
(7, false) => Self::ScrollRight,
_ => bail!(),
};
let mut modifiers = Modifiers::empty();
if cb & 0b0000_0100 == 0b0000_0100 {
modifiers |= Modifiers::SHIFT;
}
if cb & 0b0000_1000 == 0b0000_1000 {
modifiers |= Modifiers::ALT;
}
if cb & 0b0001_0000 == 0b0001_0000 {
modifiers |= Modifiers::CONTROL;
}
Ok((kind, modifiers))
}
}
// --- Button
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MouseButton {
Left,
Right,
Middle,
}

View file

@ -1,3 +1,13 @@
yazi_macro::mod_flat!(option semaphore state term);
mod macros;
pub fn init() { YIELD_TO_SUBPROCESS.init(tokio::sync::Semaphore::new(1)); }
yazi_macro::mod_flat!(dimension error semaphore style term timeout);
yazi_macro::mod_pub!(event parser restorer sequence source stream terminal waker);
pub fn init() -> anyhow::Result<()> {
YIELD_TO_SUBPROCESS.init(tokio::sync::Semaphore::new(1));
TERM.init(terminal::Terminal::new(&yazi_tty::TTY)?);
Ok(())
}

6
yazi-term/src/macros.rs Normal file
View file

@ -0,0 +1,6 @@
#[macro_export]
macro_rules! bail {
() => {
return Err($crate::ParseError::Invalid)
};
}

279
yazi-term/src/parser/csi.rs Normal file
View file

@ -0,0 +1,279 @@
use std::str::{self, FromStr};
use super::parser::Parser;
use crate::{ParseError, Result, bail, event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, Modifiers, MouseEvent, MouseEventKind}};
impl Parser {
pub(super) fn parse_csi(&self) -> Result<Event> {
let seq = &self.seq;
debug_assert!(seq.starts_with(b"\x1B["));
if seq.len() == 2 {
return Err(ParseError::Incomplete);
}
let first = seq[2];
let second = seq.get(3);
let last = seq[seq.len() - 1];
Ok(match first {
b'[' => match second {
None => return Err(ParseError::Incomplete),
Some(b @ b'A'..=b'E') => Event::Key(KeyCode::Fn(1 + b - b'A').into()),
Some(_) => bail!(),
},
b'D' => Event::Key(KeyCode::Left.into()),
b'C' => Event::Key(KeyCode::Right.into()),
b'A' => Event::Key(KeyCode::Up.into()),
b'B' => Event::Key(KeyCode::Down.into()),
b'H' => Event::Key(KeyCode::Home.into()),
b'F' => Event::Key(KeyCode::End.into()),
b'Z' => Event::Key(KeyEvent::new(KeyCode::Tab, Modifiers::SHIFT)),
b'M' => return self.parse_csi_normal_mouse(),
b'<' => return self.parse_csi_sgr_mouse(),
b'I' => Event::FocusIn,
b'O' => Event::FocusOut,
b';' => return self.parse_csi_modifier_key(),
// P, Q, and S for compatibility with Kitty keyboard protocol,
// as the 1 in 'CSI 1 P' etc. must be omitted if there are no
// modifiers pressed:
// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys
b'P' => Event::Key(KeyCode::Fn(1).into()),
b'Q' => Event::Key(KeyCode::Fn(2).into()),
b'S' => Event::Key(KeyCode::Fn(4).into()),
b'?' => match last {
b'u' | b'c' | b'n' | b'y' => return Err(ParseError::Ignored),
_ => bail!(),
},
b'>' => match seq[seq.len() - 2..] {
[b' ', b'q'] => return Err(ParseError::Ignored),
_ => bail!(),
},
b'0'..=b'9' if !(64..=126).contains(&last) => return Err(ParseError::Incomplete),
b'0'..=b'9' => match last {
b'M' => return self.parse_csi_rxvt_mouse(),
b'~' => return self.parse_csi_special_key(),
b'u' => return self.parse_csi_u_key(),
b'R' => return Err(ParseError::Ignored),
_ if self.seq.contains(&b';') => return self.parse_csi_modifier_key(),
_ => return self.parse_csi_modifier_legacy_key(),
},
_ => bail!(),
})
}
pub(super) fn parse_csi_u_key(&self) -> Result<Event> {
let seq = &self.seq;
debug_assert!(seq.starts_with(b"\x1B[")); // CSI
debug_assert!(seq.ends_with(b"u"));
let s = str::from_utf8(&seq[2..seq.len() - 1])?;
let mut it = s.split(';');
// In `CSI u`, this is parsed as:
//
// CSI codepoint ; modifiers u
// codepoint: ASCII Dec value
//
// The Kitty Keyboard Protocol extends this with optional components that can be
// enabled progressively. The full sequence is parsed as:
//
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ;
// text-as-codepoints u
let mut codepoints = it.next().ok_or(ParseError::Invalid)?.split(':');
let (mut code, state_from_keycode) = KeyCode::from_codepoint(parse_next(&mut codepoints)?)?;
let (mut modifiers, kind, state_from_modifiers) = parse_mks(&mut it).unwrap_or_default();
if let KeyCode::Modifier(c) = code
&& let Some(m) = c.to_modifier()
{
modifiers |= m;
}
// When the "report alternate keys" flag is enabled in the Kitty Keyboard
// Protocol and the terminal sends a keyboard event containing shift, the
// sequence will contain an additional codepoint separated by a ':' character
// which contains the shifted character according to the keyboard layout.
if modifiers.contains(Modifiers::SHIFT)
&& let Ok(Some(shifted)) = parse_next(&mut codepoints).map(char::from_u32)
{
code = KeyCode::Char(shifted);
modifiers.remove(Modifiers::SHIFT);
}
Ok(Event::Key(KeyEvent {
code,
modifiers,
kind,
state: state_from_keycode | state_from_modifiers,
}))
}
/// Parses `CSI [1;] modifier[:kind] final` — sequences that carry a
/// semicolon, e.g. `\x1B[;2A` (Shift+Up, leading 1 omitted) or `\x1B[1;2A`
/// (Shift+Up).
pub(super) fn parse_csi_modifier_key(&self) -> Result<Event> {
let seq = &self.seq;
debug_assert!(seq.starts_with(b"\x1B[")); // CSI
let s = str::from_utf8(&seq[2..seq.len() - 1])?;
let mut it = s.split(';');
it.next(); // skip leading "1" or empty string
let (modifiers, kind, _) = parse_mks(&mut it).unwrap_or_default();
let code = KeyCode::from_xterm_modifier(seq[seq.len() - 1])?;
Ok(Event::Key(KeyEvent { code, modifiers, kind, state: KeyEventState::empty() }))
}
/// Parses legacy `CSI modifier final` - no semicolon, modifier digit
/// immediately before the final byte, e.g. `\x1B[2A` = Shift+Up.
pub(super) fn parse_csi_modifier_legacy_key(&self) -> Result<Event> {
let seq = &self.seq;
debug_assert!(seq.starts_with(b"\x1B[")); // CSI
let modifier = seq[seq.len() - 2];
if !modifier.is_ascii_digit() {
bail!();
}
Ok(Event::Key(KeyEvent {
code: KeyCode::from_xterm_modifier(seq[seq.len() - 1])?,
modifiers: Modifiers::from_vt_mask(modifier - b'0'),
kind: KeyEventKind::Press,
state: KeyEventState::empty(),
}))
}
pub(super) fn parse_csi_special_key(&self) -> Result<Event> {
let seq = &self.seq;
debug_assert!(seq.starts_with(b"\x1B[")); // CSI
debug_assert!(seq.ends_with(b"~"));
let s = str::from_utf8(&seq[2..seq.len() - 1])?;
let mut it = s.split(';');
// This CSI sequence can be a list of semicolon-separated numbers.
let first: u8 = parse_next(&mut it)?;
let (modifiers, kind, state) = parse_mks(&mut it).unwrap_or_default();
let code = match first {
1 | 7 => KeyCode::Home,
2 => KeyCode::Insert,
3 => KeyCode::Delete,
4 | 8 => KeyCode::End,
5 => KeyCode::PageUp,
6 => KeyCode::PageDown,
v @ 11..=15 => KeyCode::Fn(v - 10),
v @ 17..=21 => KeyCode::Fn(v - 11),
v @ 23..=26 => KeyCode::Fn(v - 12),
v @ 28..=29 => KeyCode::Fn(v - 15),
v @ 31..=34 => KeyCode::Fn(v - 17),
_ => bail!(),
};
let event = Event::Key(KeyEvent { code, modifiers, kind, state });
Ok(event)
}
// Parse rxvt mouse: CSI Cb ; Cx ; Cy ; M
pub(super) fn parse_csi_rxvt_mouse(&self) -> Result<Event> {
let seq = &self.seq;
debug_assert!(seq.starts_with(b"\x1B[")); // CSI
debug_assert!(seq.ends_with(b"M"));
let s = str::from_utf8(&seq[2..seq.len() - 1])?;
let mut it = s.split(';');
let cb = parse_next::<u8>(&mut it)?.checked_sub(32).ok_or(ParseError::Invalid)?;
let (kind, modifiers) = MouseEventKind::from_cb(cb)?;
let column = parse_next::<u16>(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?;
let row = parse_next::<u16>(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?;
Ok(Event::Mouse(MouseEvent { kind, column, row, modifiers }))
}
// Parse normal mouse: CSI M CB Cx Cy (6 characters only).
pub(super) fn parse_csi_normal_mouse(&self) -> Result<Event> {
let seq = &self.seq;
debug_assert!(seq.starts_with(b"\x1B[M")); // CSI M
if seq.len() < 6 {
return Err(ParseError::Incomplete);
}
let cb = seq[3].checked_sub(32).ok_or(ParseError::Invalid)?;
let (kind, modifiers) = MouseEventKind::from_cb(cb)?;
// See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
// Mouse positions are encoded as (value + 32), but the upper left
// character position on the terminal is denoted as 1,1.
// So, we need to subtract 32 + 1 (33) to keep it synced with the cursor.
let column = u16::from(seq[4].checked_sub(33).ok_or(ParseError::Invalid)?);
let row = u16::from(seq[5].checked_sub(33).ok_or(ParseError::Invalid)?);
Ok(Event::Mouse(MouseEvent { kind, column, row, modifiers }))
}
// Parse SGR mouse: CSI < Cb ; Cx ; Cy (;) (M or m)
pub(super) fn parse_csi_sgr_mouse(&self) -> Result<Event> {
let seq = &self.seq;
debug_assert!(seq.starts_with(b"\x1B[<")); // CSI <
if !seq.ends_with(b"m") && !seq.ends_with(b"M") {
return Err(ParseError::Ignored);
}
let s = str::from_utf8(&seq[3..seq.len() - 1])?;
let mut it = s.split(';');
let cb = parse_next(&mut it)?;
let (mut kind, modifiers) = MouseEventKind::from_cb(cb)?;
// See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
// The upper left character position on the terminal is denoted as 1,1.
// Subtract 1 to keep it synced with cursor
let column = parse_next::<u16>(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?;
let row = parse_next::<u16>(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?;
// When button 3 in Cb is used to represent mouse release, you can't tell which
// button was released. SGR mode solves this by having the sequence end with a
// lowercase m if it's a button release and an uppercase M if it's a button
// press.
//
// We've already checked that the last character is a lowercase or uppercase M
// at the start of this function, so we just need one if.
if seq.ends_with(b"m")
&& let MouseEventKind::Down(button) = kind
{
kind = MouseEventKind::Up(button);
}
Ok(Event::Mouse(MouseEvent { kind, column, row, modifiers }))
}
}
fn parse_next<'a, T>(iter: &mut impl Iterator<Item = &'a str>) -> Result<T>
where
T: FromStr,
{
iter.next().ok_or(ParseError::Invalid)?.parse::<T>().map_err(|_| ParseError::Invalid)
}
fn parse_mks<'a, I>(mut it: I) -> Option<(Modifiers, KeyEventKind, KeyEventState)>
where
I: Iterator<Item = &'a str>,
{
let mut it = it.next()?.split(':');
let mask: u8 = it.next()?.parse().ok()?;
let code: u8 = it.next().and_then(|s| s.parse().ok()).unwrap_or(1);
Some((
Modifiers::from_vt_mask(mask),
KeyEventKind::from_vt_code(code),
KeyEventState::from_vt_mask(mask),
))
}

View file

@ -0,0 +1,24 @@
use crate::{event::{KeyCode, KeyEvent, Modifiers}, parser::Parser};
impl Parser {
/// Maps a ground-state byte to a [`KeyEvent`], returning [`None`] for ESC
/// (`\x1B`), multi-byte UTF-8 lead bytes, and continuation / invalid bytes.
pub(super) fn parse_ground_key(b: u8) -> Option<KeyEvent> {
match b {
b'\r' => Some(KeyCode::Enter.into()),
b'\t' => Some(KeyCode::Tab.into()),
b'\x7F' => Some(KeyCode::Backspace.into()),
b'\0' => Some(KeyEvent::new(KeyCode::Char(' '), Modifiers::CONTROL)),
c @ b'\x01'..=b'\x1A' => {
Some(KeyEvent::new(KeyCode::Char((c - 0x1 + b'a') as char), Modifiers::CONTROL))
}
c @ b'\x1C'..=b'\x1F' => {
Some(KeyEvent::new(KeyCode::Char((c - 0x1c + b'4') as char), Modifiers::CONTROL))
}
b'\x20'..=b'\x7E' => {
Some(KeyEvent::new(KeyCode::Char(b as char), Modifiers::for_char(b as char)))
}
_ => None,
}
}
}

View file

@ -0,0 +1,4 @@
yazi_macro::mod_flat!(csi ground parser state);
#[cfg(windows)]
yazi_macro::mod_flat!(windows);

View file

@ -0,0 +1,260 @@
use std::{collections::VecDeque, num::NonZeroU8, str};
use yazi_shim::utf8_char_width;
use super::state::State;
use crate::event::{Event, KeyCode, KeyEvent, Modifiers};
#[derive(Debug)]
pub struct Parser {
state: State,
pub(super) seq: Vec<u8>,
pub(crate) events: VecDeque<Event>,
}
impl Default for Parser {
fn default() -> Self {
Self {
state: State::Ground,
seq: Vec::with_capacity(64),
events: VecDeque::with_capacity(32),
}
}
}
impl Parser {
pub fn parse(&mut self, bytes: &[u8]) {
for &b in bytes {
self.step(b);
}
}
fn step(&mut self, b: u8) {
match self.state {
State::Ground => self.on_ground(b),
State::Esc => self.on_esc(b),
State::EscO => self.on_esco(b),
State::Csi => self.on_csi(b),
State::NormalMouse => self.on_normal_mouse(b),
State::BracketedPaste => self.on_bracketed_paste(b),
State::Osc | State::OscSt => self.on_osc(b),
State::Dcs | State::DcsSt => self.on_dcs(b),
State::Utf8(n) => self.on_utf8(b, n),
State::AltUtf8(n) => self.on_alt_utf8(b, n),
}
}
/// Resolve any pending ambiguous state.
///
/// Call this when no more input bytes are immediately available. If the
/// parser is waiting in the [`State::Esc`] state (a lone `\x1B` has
/// been seen but no follow-up bytes arrived), this emits a bare
/// [`KeyCode::Escape`] event and resets to [`State::Ground`].
pub fn flush(&mut self) {
if self.state == State::Esc {
self.emit_key(KeyCode::Escape);
}
self.reset();
}
fn reset(&mut self) {
self.state = State::Ground;
self.seq.clear();
}
pub(crate) fn emit(&mut self, event: impl Into<Event>) { self.events.push_back(event.into()); }
fn emit_key(&mut self, event: impl Into<KeyEvent>) { self.emit(Event::Key(event.into())); }
pub fn pop(&mut self) -> Option<Event> { self.events.pop_front() }
fn on_ground(&mut self, b: u8) {
match b {
b'\x1B' => {
self.seq.clear();
self.seq.push(b'\x1B');
self.state = State::Esc;
}
_ if let Some(key) = Self::parse_ground_key(b) => {
self.emit_key(key);
}
_ if let w @ 2..=4 = utf8_char_width(b) => {
self.seq.clear();
self.seq.push(b);
self.state = State::Utf8(NonZeroU8::new(w - 1).unwrap());
}
_ => {}
}
}
fn on_esc(&mut self, b: u8) {
self.seq.push(b);
match b {
b'[' => self.state = State::Csi,
b']' => self.state = State::Osc,
b'P' => self.state = State::Dcs,
b'O' => self.state = State::EscO,
// ESC ESC: emit Escape for the first byte, the second is consumed.
b'\x1B' => {
self.emit_key(KeyCode::Escape);
self.reset();
}
// Alt + <key>
_ if let Some(mut key) = Self::parse_ground_key(b) => {
key.modifiers |= Modifiers::ALT;
self.emit_key(key);
self.reset();
}
// seq = [ESC, b], collect w-1 more continuation bytes.
_ if let w @ 2..=4 = utf8_char_width(b) => {
self.state = State::AltUtf8(NonZeroU8::new(w - 1).unwrap());
}
// invalid byte — discard
_ => self.reset(),
}
}
fn on_esco(&mut self, b: u8) {
self.seq.push(b);
let event = match b {
b'A' => Event::Key(KeyCode::Up.into()),
b'B' => Event::Key(KeyCode::Down.into()),
b'C' => Event::Key(KeyCode::Right.into()),
b'D' => Event::Key(KeyCode::Left.into()),
b'F' => Event::Key(KeyCode::End.into()),
b'H' => Event::Key(KeyCode::Home.into()),
v @ b'P'..=b'S' => Event::Key(KeyCode::Fn(1 + v - b'P').into()),
_ => {
self.reset();
return;
}
};
self.emit(event);
self.reset();
}
fn on_csi(&mut self, b: u8) {
self.seq.push(b);
// Bytes below 0x40 are parameter or intermediate bytes; keep accumulating.
if !(0x40..=0x7e).contains(&b) {
return;
}
// The Linux console sends `\x1B[[x` for F1F5. The inner `[` (0x5B) is
// technically in the final-byte range but must not trigger early dispatch.
if b == b'[' && self.seq.len() == 3 {
return;
}
// `\x1B[M` (no params) = X10 normal-mouse encoding; 3 raw bytes follow.
if b == b'M' && self.seq.len() == 3 {
self.state = State::NormalMouse;
return;
}
// `\x1B[200~` = start of bracketed-paste mode.
if self.seq == b"\x1B[200~" {
self.state = State::BracketedPaste;
return;
}
if let Ok(e) = self.parse_csi() {
self.emit(e);
}
self.reset();
}
fn on_normal_mouse(&mut self, b: u8) {
self.seq.push(b);
// Need `\x1B[M` + 3 raw bytes = 6 bytes total.
if self.seq.len() < 6 {
return;
}
if let Ok(e) = self.parse_csi_normal_mouse() {
self.emit(e);
}
self.reset();
}
fn on_bracketed_paste(&mut self, b: u8) {
self.seq.push(b);
if self.seq.ends_with(b"\x1b[201~") {
// seq = b"\x1B[200~" + paste_content + b"\x1B[201~"
let paste = String::from_utf8_lossy(&self.seq[6..self.seq.len() - 6]).into_owned();
self.emit(Event::Paste(paste));
self.reset();
}
}
fn on_osc(&mut self, b: u8) {
self.seq.push(b);
match (self.state, b) {
(State::Osc, b'\x07') => self.reset(), // BEL — OSC complete (discard)
(State::Osc, b'\x1B') => self.state = State::OscSt,
(State::Osc, _) => {} // keep accumulating
(State::OscSt, b'\\') => self.reset(), // ST (`\x1B\\`) — OSC complete (discard)
(State::OscSt, b'\x1B') => {} // another ESC — stay in OscSt
(State::OscSt, _) => self.state = State::Osc, // not ST — resume OSC
_ => unreachable!(),
}
}
fn on_dcs(&mut self, b: u8) {
self.seq.push(b);
match (self.state, b) {
(State::Dcs, b'\x1B') => self.state = State::DcsSt,
(State::Dcs, _) => {}
(State::DcsSt, b'\\') => self.reset(), // ST — DCS complete (discard)
(State::DcsSt, b'\x1B') => {} // another ESC — stay in DcsSt
(State::DcsSt, _) => self.state = State::Dcs, // not ST — resume DCS
_ => unreachable!(),
}
}
fn on_utf8(&mut self, b: u8, remaining: NonZeroU8) {
if b & 0xc0 != 0x80 {
self.reset();
self.on_ground(b);
return; // Not a continuation byte — abandon the sequence and retry from ground.
}
self.seq.push(b);
if remaining.get() != 1 {
return self.state = State::Utf8(NonZeroU8::new(remaining.get() - 1).unwrap());
}
if let Ok(s) = str::from_utf8(&self.seq)
&& let Some(c) = s.chars().next()
{
self.emit_key(KeyEvent::new(KeyCode::Char(c), Modifiers::for_char(c)));
}
self.reset();
}
fn on_alt_utf8(&mut self, b: u8, remaining: NonZeroU8) {
if b & 0xc0 != 0x80 {
self.reset();
self.on_ground(b);
return; // Not a continuation byte — abandon Alt+UTF-8 and retry from ground.
}
self.seq.push(b);
if remaining.get() != 1 {
return self.state = State::AltUtf8(NonZeroU8::new(remaining.get() - 1).unwrap());
}
// seq = [ESC, …utf8_bytes…]
if let Ok(s) = str::from_utf8(&self.seq[1..])
&& let Some(c) = s.chars().next()
{
self.emit_key(KeyEvent::new(KeyCode::Char(c), Modifiers::ALT | Modifiers::for_char(c)));
}
self.reset();
}
}

View file

@ -0,0 +1,29 @@
use std::num::NonZeroU8;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum State {
/// Normal ground state.
Ground,
/// Saw `\x1B`, waiting for the next byte.
Esc,
/// Saw `\x1B O`, waiting for the SS3 key byte.
EscO,
/// Inside a CSI sequence (`\x1B[` … final-byte in 0x400x7E).
Csi,
/// Saw `\x1B[M`, collecting 3 raw mouse bytes (CB Cx Cy).
NormalMouse,
/// Inside bracketed paste (`\x1B[200~` … `\x1B[201~`).
BracketedPaste,
/// Inside an OSC sequence (`\x1B]` … BEL or ST).
Osc,
/// Inside OSC, just saw `\x1B` (potential start of ST = `\x1B\\`).
OscSt,
/// Inside a DCS sequence (`\x1BP` … ST).
Dcs,
/// Inside DCS, just saw `\x1B` (potential start of ST).
DcsSt,
/// Mid-UTF-8 character in ground: `n` continuation bytes still needed.
Utf8(NonZeroU8),
/// `\x1B` + mid-UTF-8 character: `n` continuation bytes still needed.
AltUtf8(NonZeroU8),
}

View file

@ -0,0 +1,36 @@
use windows_sys::Win32::System::Console;
use crate::{Dimension, event::Event, parser::Parser};
impl Parser {
pub(crate) fn parse_input_records(&mut self, records: &[Console::INPUT_RECORD]) {
for record in records {
match record.EventType as u32 {
Console::KEY_EVENT => {
let event = unsafe { record.Event.KeyEvent };
let ch = unsafe { event.uChar.AsciiChar } as u8;
if event.bKeyDown != 0 && ch != 0 {
self.parse(&[ch]);
}
}
Console::WINDOW_BUFFER_SIZE_EVENT => {
let event = unsafe { record.Event.WindowBufferSizeEvent };
if event.dwSize.X <= 0 || event.dwSize.Y <= 0 {
continue;
}
self.events.push_back(Event::Resize(Dimension {
rows: event.dwSize.Y as u16,
cols: event.dwSize.X as u16,
width: 0,
height: 0,
}));
}
_ => {}
}
}
self.flush();
}
}

View file

@ -0,0 +1,5 @@
#[cfg(unix)]
yazi_macro::mod_flat!(unix);
#[cfg(windows)]
yazi_macro::mod_flat!(windows);

Some files were not shown because too many files have changed in this diff Show more