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]) - Bulk create ([#3793])
### Changed
- Rename `<BackTab>` to `<S-Tab>` ([#3989])
- Remove Legacy Console Mode on Windows ([#3989])
## [v26.5.6] ## [v26.5.6]
### Added ### 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 [#3906]: https://github.com/sxyazi/yazi/pull/3906
[#3934]: https://github.com/sxyazi/yazi/pull/3934 [#3934]: https://github.com/sxyazi/yazi/pull/3934
[#3943]: https://github.com/sxyazi/yazi/pull/3943 [#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" chrono = "0.4.44"
clap = { version = "4.6.1", features = [ "derive" ] } clap = { version = "4.6.1", features = [ "derive" ] }
core-foundation-sys = "0.8.7" core-foundation-sys = "0.8.7"
crossterm = { version = "0.29.0", features = [ "event-stream" ] }
dirs = "6.0.0" dirs = "6.0.0"
dyn-clone = "1.0.20" dyn-clone = "1.0.20"
either = { version = "1.15.0" } either = { version = "1.16.0" }
foldhash = "0.2.0" foldhash = "0.2.0"
futures = "0.3.32" futures = "0.3.32"
globset = "0.4.18" 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" ] } indexmap = { version = "2.14.0", features = [ "serde" ] }
libc = "0.2.186" libc = "0.2.186"
lru = "0.18.0" lru = "0.18.0"
@ -61,13 +60,13 @@ parking_lot = "0.12.5"
paste = "1.0.15" paste = "1.0.15"
percent-encoding = "2.3.2" percent-encoding = "2.3.2"
rand = { version = "0.10.1", default-features = false, features = [ "std", "sys_rng" ] } 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" 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" scopeguard = "1.2.0"
serde = { version = "1.0.228", features = [ "derive" ] } serde = { version = "1.0.228", features = [ "derive" ] }
serde_json = "1.0.149" serde_json = "1.0.149"
serde_with = "3.19.0" serde_with = "3.20.0"
strum = { version = "0.28.0", features = [ "derive" ] } strum = { version = "0.28.0", features = [ "derive" ] }
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
thiserror = "2.0.18" 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-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" } 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-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" } yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
crossterm = { workspace = true }
either = { workspace = true } either = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
@ -55,6 +55,3 @@ tracing = { workspace = true }
[target."cfg(unix)".dependencies] [target."cfg(unix)".dependencies]
libc = { workspace = true } 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 anyhow::Result;
use crossterm::event::MouseEventKind;
use mlua::{ObjectLike, Table}; use mlua::{ObjectLike, Table};
use tracing::error; use tracing::error;
use yazi_actor::lives::Lives; use yazi_actor::lives::Lives;
@ -8,6 +7,7 @@ use yazi_macro::succ;
use yazi_parser::app::MouseForm; use yazi_parser::app::MouseForm;
use yazi_plugin::LUA; use yazi_plugin::LUA;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_term::event::MouseEventKind;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};

View file

@ -1,9 +1,11 @@
use std::process;
use anyhow::Result; use anyhow::Result;
use yazi_boot::ARGS; use yazi_boot::ARGS;
use yazi_fs::provider::{Provider, local::Local}; use yazi_fs::provider::{Provider, local::Local};
use yazi_parser::app::QuitForm; use yazi_parser::app::QuitForm;
use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}}; use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}};
use yazi_term::Term; use yazi_tui::Raterm;
use crate::{Actor, Ctx}; 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 anyhow::Result;
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::app::ResumeForm; use yazi_parser::app::ReflowForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_term::Term; use yazi_tui::Raterm;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
pub struct Resume; pub struct Resume;
impl Actor for Resume { impl Actor for Resume {
type Form = ResumeForm; type Form = ReflowForm;
const NAME: &str = "resume"; const NAME: &str = "resume";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
cx.active_mut().preview.reset(); 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. // 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. // We need to trigger a resize, and render the UI based on the resized area.
act!(app:resize, cx, form.reflow)?; act!(app:resize, cx, form.reflow)?;
form.tx.send((true, form.replier))?;
act!(app:title, cx).ok(); act!(app:title, cx).ok();
succ!(render!()); succ!(render!());
} }

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_macro::succ; use yazi_macro::succ;
use yazi_parser::app::StopForm; use yazi_parser::VoidForm;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -8,20 +8,15 @@ use crate::{Actor, Ctx};
pub struct Stop; pub struct Stop;
impl Actor for Stop { impl Actor for Stop {
type Form = StopForm; type Form = VoidForm;
const NAME: &str = "stop"; 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(); 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; *cx.term = None;
form.tx.send((false, form.replier))?;
succ!(); succ!();
} }
} }

View file

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

View file

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

View file

@ -5,13 +5,13 @@ use scopeguard::defer;
use yazi_binding::Permit; use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRule}; use yazi_config::{YAZI, opener::OpenerRule};
use yazi_fs::{File, FilesOp, Splatter, provider::{Provider, local::Local}}; 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_parser::VoidForm;
use yazi_proxy::TasksProxy; use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy}; 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_shim::path::CROSS_SEPARATOR;
use yazi_term::YIELD_TO_SUBPROCESS; use yazi_term::{YIELD_TO_SUBPROCESS, sequence::EraseScreen};
use yazi_tty::TTY; use yazi_tty::TTY;
use yazi_vfs::{VfsFile, provider}; use yazi_vfs::{VfsFile, provider};
use yazi_watcher::WATCHER; use yazi_watcher::WATCHER;
@ -61,7 +61,7 @@ impl Actor for BulkCreate {
impl BulkCreate { impl BulkCreate {
async fn r#do(cwd: UrlBuf, todo: Vec<Entry<'_>>) -> Result<()> { async fn r#do(cwd: UrlBuf, todo: Vec<Entry<'_>>) -> Result<()> {
terminal_clear(TTY.writer())?; writef!(TTY.writer(), "{EraseScreen}\n")?;
if todo.is_empty() { if todo.is_empty() {
return Ok(()); return Ok(());
} else if !Self::ask_continue(&todo, None)? { } else if !Self::ask_continue(&todo, None)? {
@ -134,7 +134,7 @@ impl BulkCreate {
async fn output_failed(failed: Vec<(Entry<'_>, anyhow::Error)>) -> Result<()> { async fn output_failed(failed: Vec<(Entry<'_>, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout(); let mut stdout = TTY.lockout();
terminal_clear(&mut *stdout)?; writeln!(stdout, "{EraseScreen}")?;
writeln!(stdout, "Failed to create:")?; writeln!(stdout, "Failed to create:")?;
for (entry, err) in failed { for (entry, err) in failed {
@ -161,7 +161,7 @@ impl<'a> Entry<'a> {
None => (s, false), 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 std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path, sync::Arc};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use crossterm::{execute, style::Print};
use hashbrown::HashMap; use hashbrown::HashMap;
use scopeguard::defer; use scopeguard::defer;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
@ -9,12 +8,12 @@ use yazi_binding::Permit;
use yazi_config::{YAZI, opener::OpenerRule}; use yazi_config::{YAZI, opener::OpenerRule};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider::{FileBuilder, Provider, local::{Gate, Local}}}; 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_parser::VoidForm;
use yazi_proxy::TasksProxy; use yazi_proxy::TasksProxy;
use yazi_scheduler::{AppProxy, NotifyProxy}; 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_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlCow, UrlLike}};
use yazi_term::YIELD_TO_SUBPROCESS; use yazi_term::{YIELD_TO_SUBPROCESS, sequence::EraseScreen};
use yazi_tty::TTY; use yazi_tty::TTY;
use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_vfs::{VfsFile, maybe_exists, provider};
use yazi_watcher::WATCHER; use yazi_watcher::WATCHER;
@ -100,11 +99,10 @@ impl BulkRename {
selected: Vec<UrlBuf>, selected: Vec<UrlBuf>,
decision: Option<bool>, decision: Option<bool>,
) -> Result<()> { ) -> Result<()> {
terminal_clear(TTY.writer())?; writef!(TTY.writer(), "{EraseScreen}\n")?;
if old.len() != new.len() { if old.len() != new.len() {
#[rustfmt::skip] #[rustfmt::skip]
let s = format!("Number of new and old file names mismatch (New: {}, Old: {}).\nPress <Enter> to exit...", new.len(), old.len()); writef!(TTY.writer(), "Number of new and old file names mismatch (New: {}, Old: {}).\nPress <Enter> to exit...", new.len(), old.len())?;
execute!(TTY.writer(), Print(s))?;
TTY.reader().read_exact(&mut [0])?; TTY.reader().read_exact(&mut [0])?;
return Ok(()); return Ok(());
@ -186,7 +184,7 @@ impl BulkRename {
async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> { async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> {
let mut stdout = TTY.lockout(); let mut stdout = TTY.lockout();
terminal_clear(&mut *stdout)?; writeln!(stdout, "{EraseScreen}")?;
writeln!(stdout, "Failed to rename:")?; writeln!(stdout, "Failed to rename:")?;
for (old, new, err) in failed { 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> { 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> { 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 anyhow::Result;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use yazi_core::notify::Notify; use yazi_core::notify::Notify;
use yazi_emulator::Dimension;
use yazi_macro::{render, render_partial, succ}; use yazi_macro::{render, render_partial, succ};
use yazi_parser::notify::TickForm; use yazi_parser::notify::TickForm;
use yazi_proxy::NotifyProxy; use yazi_proxy::NotifyProxy;
use yazi_shared::data::Data; use yazi_shared::data::Data;
use yazi_term::{Dimension, TERM};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -21,8 +21,8 @@ impl Actor for Tick {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
cx.notify.ticker.take().map(|h| h.abort()); cx.notify.ticker.take().map(|h| h.abort());
let Dimension { rows, columns, .. } = Dimension::available(); let Dimension { rows, cols, .. } = TERM.dimension();
let area = Notify::available(Rect { x: 0, y: 0, width: columns, height: rows }); let area = Notify::available(Rect { x: 0, y: 0, width: cols, height: rows });
let limit = cx.notify.limit(area); let limit = cx.notify.limit(area);
if limit == 0 { if limit == 0 {

View file

@ -1,15 +1,15 @@
use std::io::Write; use std::io::Write;
use anyhow::Result; use anyhow::Result;
use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode}};
use scopeguard::defer; use scopeguard::defer;
use tokio::{io::{AsyncReadExt, stdin}, select, sync::mpsc, time}; use tokio::{io::{AsyncReadExt, stdin}, select, sync::mpsc, time};
use yazi_binding::Permit; use yazi_binding::Permit;
use yazi_macro::succ; use yazi_term::TERM;
use yazi_macro::{succ, writef};
use yazi_parser::VoidForm; use yazi_parser::VoidForm;
use yazi_scheduler::AppProxy; use yazi_scheduler::AppProxy;
use yazi_shared::{data::Data, terminal_clear}; use yazi_shared::data::Data;
use yazi_term::YIELD_TO_SUBPROCESS; use yazi_term::{YIELD_TO_SUBPROCESS, sequence::EraseScreen};
use yazi_tty::TTY; use yazi_tty::TTY;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -39,24 +39,27 @@ impl Actor for Inspect {
task.logs.clone() task.logs.clone()
}; };
// Stop the app and clear the terminal
AppProxy::stop().await; 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().write_all(buffered.as_bytes()).ok();
TTY.writer().flush().ok(); TTY.writer().flush().ok();
defer! { disable_raw_mode().ok(); } defer! { TERM.enter_cooked_mode().ok(); }
enable_raw_mode().ok(); TERM.enter_raw_mode().ok();
let mut stdin = stdin(); // TODO: stdin let mut stdin = stdin(); // TODO: stdin
let mut answer = 0; let mut answer = 0;
loop { loop {
select! { select! {
Some(line) = rx.recv() => { 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)) => { _ = time::sleep(time::Duration::from_millis(500)) => {
if !ongoing.lock().exists(id) { 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; 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-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", 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-tty = { path = "../yazi-tty", version = "26.5.9" }
# External dependencies # External dependencies
ansi-to-tui = { workspace = true } ansi-to-tui = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
base64 = { 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" ] } 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" moxcms = "0.8.1"
palette = { version = "0.7.6", default-features = false } 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 } ratatui = { workspace = true }
strum = { workspace = true } strum = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tracing = { 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 ansi_to_tui::IntoText;
use anyhow::{Result, anyhow, bail}; use anyhow::{Result, anyhow, bail};
use crossterm::{cursor::MoveTo, queue};
use ratatui::layout::Rect; use ratatui::layout::Rect;
use tokio::process::Command; use tokio::process::Command;
use yazi_emulator::Emulator; use yazi_emulator::Emulator;
use yazi_term::sequence::MoveTo;
use crate::Adapter; use crate::Adapter;
@ -62,7 +62,7 @@ impl Chafa {
Emulator::move_lock((max.x, max.y), |w| { Emulator::move_lock((max.x, max.y), |w| {
for (i, line) in lines.into_iter().enumerate() { for (i, line) in lines.into_iter().enumerate() {
w.write_all(line)?; 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) Ok(area)
}) })
@ -72,7 +72,7 @@ impl Chafa {
let s = " ".repeat(area.width as usize); let s = " ".repeat(area.width as usize);
Emulator::move_lock((0, 0), |w| { Emulator::move_lock((0, 0), |w| {
for y in area.top()..area.bottom() { for y in area.top()..area.bottom() {
queue!(w, MoveTo(area.x, y))?; write!(w, "{}", MoveTo(area.x, y))?;
write!(w, "{s}")?; write!(w, "{s}")?;
} }
Ok(()) Ok(())

View file

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

View file

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

View file

@ -1,12 +1,12 @@
use std::{io::Write, path::PathBuf}; use std::{io::Write, path::PathBuf};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use crossterm::{cursor::MoveTo, queue};
use image::{DynamicImage, GenericImageView, RgbImage}; use image::{DynamicImage, GenericImageView, RgbImage};
use palette::{Srgb, cast::ComponentsAs}; use palette::{Srgb, cast::ComponentsAs};
use quantette::{PaletteSize, color_map::IndexedColorMap, wu::{BinnerU8x3, WuU8x3}}; use quantette::{PaletteSize, color_map::IndexedColorMap, wu::{BinnerU8x3, WuU8x3}};
use ratatui::layout::Rect; use ratatui::layout::Rect;
use yazi_emulator::{CLOSE, ESCAPE, Emulator, START}; use yazi_emulator::{CLOSE, ESCAPE, Emulator, START};
use yazi_term::sequence::MoveTo;
use crate::{Image, adapter::Adapter}; use crate::{Image, adapter::Adapter};
@ -35,7 +35,7 @@ impl Sixel {
let s = " ".repeat(area.width as usize); let s = " ".repeat(area.width as usize);
Emulator::move_lock((0, 0), |w| { Emulator::move_lock((0, 0), |w| {
for y in area.top()..area.bottom() { for y in area.top()..area.bottom() {
queue!(w, MoveTo(area.x, y))?; write!(w, "{}", MoveTo(area.x, y))?;
write!(w, "{s}")?; write!(w, "{s}")?;
} }
Ok(()) 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-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", 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-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies # External dependencies
ansi-to-tui = { workspace = true } ansi-to-tui = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
crossterm = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
@ -45,8 +45,5 @@ unicode-width = { workspace = true }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = { workspace = true } libc = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }
[target."cfg(windows)".dependencies] [target."cfg(windows)".dependencies]
windows-sys = { version = "0.61.2", features = [ "Win32_Foundation", "Win32_System_JobObjects" ] } windows-sys = { version = "0.61.2", features = [ "Win32_Foundation", "Win32_System_JobObjects" ] }

View file

@ -1,19 +1,19 @@
use std::ops::Deref; use std::ops::Deref;
use crossterm::event::MouseButton;
use mlua::{UserData, UserDataFields}; use mlua::{UserData, UserDataFields};
use yazi_term::event::{MouseButton, MouseEventKind};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct MouseEvent(crossterm::event::MouseEvent); pub struct MouseEvent(yazi_term::event::MouseEvent);
impl Deref for MouseEvent { impl Deref for MouseEvent {
type Target = crossterm::event::MouseEvent; type Target = yazi_term::event::MouseEvent;
fn deref(&self) -> &Self::Target { &self.0 } fn deref(&self) -> &Self::Target { &self.0 }
} }
impl From<crossterm::event::MouseEvent> for MouseEvent { impl From<yazi_term::event::MouseEvent> for MouseEvent {
fn from(event: crossterm::event::MouseEvent) -> Self { Self(event) } fn from(event: yazi_term::event::MouseEvent) -> Self { Self(event) }
} }
impl UserData for MouseEvent { 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("x", |_, me| Ok(me.column));
fields.add_field_method_get("y", |_, me| Ok(me.row)); fields.add_field_method_get("y", |_, me| Ok(me.row));
fields.add_field_method_get("is_left", |_, me| { 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)) 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| { 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)) 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| { 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)) 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-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", 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-vfs = { path = "../yazi-vfs", version = "26.5.6" }
# External dependencies # External dependencies
@ -33,7 +34,7 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
# External dependencies # External dependencies
clap = { workspace = true } clap = { workspace = true }
clap_complete = "4.6.3" clap_complete = "4.6.5"
clap_complete_fig = "4.5.2" clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.6.0" clap_complete_nushell = "4.6.0"
vergen-gitcl = { version = "9.1.0", features = [ "build", "rustc" ] } 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_emulator::Mux;
use yazi_fs::Xdg; use yazi_fs::Xdg;
use yazi_shared::timestamp_us; use yazi_shared::timestamp_us;
use yazi_term::TERM;
use super::Actions; use super::Actions;
@ -39,7 +40,7 @@ impl Actions {
writeln!(s, "\nAdapter")?; writeln!(s, "\nAdapter")?;
writeln!(s, " Adapter.matches : {:?}", yazi_adapter::ADAPTOR)?; 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, "\nDesktop")?;
writeln!(s, " XDG_SESSION_TYPE : {:?}", env::var_os("XDG_SESSION_TYPE"))?; 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-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", 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 # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
crossterm = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
@ -37,16 +36,13 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
# External build dependencies # External build dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
clap_complete = "4.6.3" clap_complete = "4.6.5"
clap_complete_fig = "4.5.2" clap_complete_fig = "4.5.2"
clap_complete_nushell = "4.6.0" clap_complete_nushell = "4.6.0"
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
vergen-gitcl = { version = "9.1.0", features = [ "build" ] } vergen-gitcl = { version = "9.1.0", features = [ "build" ] }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }
[[bin]] [[bin]]
name = "ya" name = "ya"
path = "src/main.rs" 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 anyhow::{Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
@ -48,20 +48,19 @@ impl Dependency {
pub(super) fn header(&self, s: &str) -> Result<()> { pub(super) fn header(&self, s: &str) -> Result<()> {
use std::io::IsTerminal; use std::io::IsTerminal;
use crossterm::style::{Attribute, Print, SetAttributes}; use yazi_macro::writef;
use yazi_shim::crossterm::If; use yazi_term::sequence::{If, SetSgr};
let ansi = env::var_os("YA_FORCE_ANSI").is_some_and(|v| v == "1") || io::stdout().is_terminal(); let ansi = env::var_os("YA_FORCE_ANSI").is_some_and(|v| v == "1") || io::stdout().is_terminal();
crossterm::execute!( writef!(
BufWriter::new(io::stdout()), io::stdout(),
Print("\n"), "\n{}{}{} {} {}{}\n\n",
If(ansi, SetAttributes(Attribute::Reverse.into())), If(ansi, SetSgr::Reverse),
If(ansi, SetAttributes(Attribute::Bold.into())), If(ansi, SetSgr::Bold),
Print(" "), " ",
Print(s.replacen("{name}", &self.name, 1)), s.replacen("{name}", &self.name, 1),
Print(" "), " ",
If(ansi, SetAttributes(Attribute::Reset.into())), If(ansi, SetSgr::Reset),
Print("\n\n"),
)?; )?;
Ok(()) 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-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", 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-tty = { path = "../yazi-tty", version = "26.5.9" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
arc-swap = { workspace = true } arc-swap = { workspace = true }
bitflags = { workspace = true } bitflags = { workspace = true }
crossterm = { workspace = true }
globset = { workspace = true } globset = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
@ -36,6 +36,3 @@ strum = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
tracing = { 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 std::{fmt::{Display, Write}, str::FromStr};
use anyhow::bail; use anyhow::bail;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use yazi_term::event::{KeyCode, KeyEvent, Modifiers};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Key { pub struct Key {
@ -30,29 +30,12 @@ impl Default for Key {
impl From<KeyEvent> for Key { impl From<KeyEvent> for Key {
fn from(value: KeyEvent) -> Self { 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 { Self {
code: value.code, code: value.code,
shift, shift: value.modifiers.contains(Modifiers::SHIFT),
ctrl: value.modifiers.contains(KeyModifiers::CONTROL), ctrl: value.modifiers.contains(Modifiers::CONTROL),
alt: value.modifiers.contains(KeyModifiers::ALT), alt: value.modifiers.contains(Modifiers::ALT),
super_: value.modifiers.contains(KeyModifiers::SUPER), super_: value.modifiers.contains(Modifiers::SUPER),
} }
} }
} }
@ -92,29 +75,28 @@ impl FromStr for Key {
"pageup" => key.code = KeyCode::PageUp, "pageup" => key.code = KeyCode::PageUp,
"pagedown" => key.code = KeyCode::PageDown, "pagedown" => key.code = KeyCode::PageDown,
"tab" => key.code = KeyCode::Tab, "tab" => key.code = KeyCode::Tab,
"backtab" => key.code = KeyCode::BackTab,
"delete" => key.code = KeyCode::Delete, "delete" => key.code = KeyCode::Delete,
"insert" => key.code = KeyCode::Insert, "insert" => key.code = KeyCode::Insert,
"f1" => key.code = KeyCode::F(1), "f1" => key.code = KeyCode::Fn(1),
"f2" => key.code = KeyCode::F(2), "f2" => key.code = KeyCode::Fn(2),
"f3" => key.code = KeyCode::F(3), "f3" => key.code = KeyCode::Fn(3),
"f4" => key.code = KeyCode::F(4), "f4" => key.code = KeyCode::Fn(4),
"f5" => key.code = KeyCode::F(5), "f5" => key.code = KeyCode::Fn(5),
"f6" => key.code = KeyCode::F(6), "f6" => key.code = KeyCode::Fn(6),
"f7" => key.code = KeyCode::F(7), "f7" => key.code = KeyCode::Fn(7),
"f8" => key.code = KeyCode::F(8), "f8" => key.code = KeyCode::Fn(8),
"f9" => key.code = KeyCode::F(9), "f9" => key.code = KeyCode::Fn(9),
"f10" => key.code = KeyCode::F(10), "f10" => key.code = KeyCode::Fn(10),
"f11" => key.code = KeyCode::F(11), "f11" => key.code = KeyCode::Fn(11),
"f12" => key.code = KeyCode::F(12), "f12" => key.code = KeyCode::Fn(12),
"f13" => key.code = KeyCode::F(13), "f13" => key.code = KeyCode::Fn(13),
"f14" => key.code = KeyCode::F(14), "f14" => key.code = KeyCode::Fn(14),
"f15" => key.code = KeyCode::F(15), "f15" => key.code = KeyCode::Fn(15),
"f16" => key.code = KeyCode::F(16), "f16" => key.code = KeyCode::Fn(16),
"f17" => key.code = KeyCode::F(17), "f17" => key.code = KeyCode::Fn(17),
"f18" => key.code = KeyCode::F(18), "f18" => key.code = KeyCode::Fn(18),
"f19" => key.code = KeyCode::F(19), "f19" => key.code = KeyCode::Fn(19),
"esc" => key.code = KeyCode::Esc, "esc" => key.code = KeyCode::Escape,
_ => match next { _ => match next {
s if it.peek().is_none() => { s if it.peek().is_none() => {
@ -166,29 +148,28 @@ impl Display for Key {
KeyCode::PageUp => "PageUp", KeyCode::PageUp => "PageUp",
KeyCode::PageDown => "PageDown", KeyCode::PageDown => "PageDown",
KeyCode::Tab => "Tab", KeyCode::Tab => "Tab",
KeyCode::BackTab => "BackTab",
KeyCode::Delete => "Delete", KeyCode::Delete => "Delete",
KeyCode::Insert => "Insert", KeyCode::Insert => "Insert",
KeyCode::F(1) => "F1", KeyCode::Fn(1) => "F1",
KeyCode::F(2) => "F2", KeyCode::Fn(2) => "F2",
KeyCode::F(3) => "F3", KeyCode::Fn(3) => "F3",
KeyCode::F(4) => "F4", KeyCode::Fn(4) => "F4",
KeyCode::F(5) => "F5", KeyCode::Fn(5) => "F5",
KeyCode::F(6) => "F6", KeyCode::Fn(6) => "F6",
KeyCode::F(7) => "F7", KeyCode::Fn(7) => "F7",
KeyCode::F(8) => "F8", KeyCode::Fn(8) => "F8",
KeyCode::F(9) => "F9", KeyCode::Fn(9) => "F9",
KeyCode::F(10) => "F10", KeyCode::Fn(10) => "F10",
KeyCode::F(11) => "F11", KeyCode::Fn(11) => "F11",
KeyCode::F(12) => "F12", KeyCode::Fn(12) => "F12",
KeyCode::F(13) => "F13", KeyCode::Fn(13) => "F13",
KeyCode::F(14) => "F14", KeyCode::Fn(14) => "F14",
KeyCode::F(15) => "F15", KeyCode::Fn(15) => "F15",
KeyCode::F(16) => "F16", KeyCode::Fn(16) => "F16",
KeyCode::F(17) => "F17", KeyCode::Fn(17) => "F17",
KeyCode::F(18) => "F18", KeyCode::Fn(18) => "F18",
KeyCode::F(19) => "F19", KeyCode::Fn(19) => "F19",
KeyCode::Esc => "Esc", KeyCode::Escape => "Esc",
KeyCode::Char(' ') => "Space", KeyCode::Char(' ') => "Space",
KeyCode::Char(c) => { 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 std::io::{Read, Write};
use yazi_shim::{cell::{RoCell, SyncCell}, toml::{DeserializeOver, DeserializeOverWith}}; use yazi_shim::{cell::{RoCell, SyncCell}, toml::{DeserializeOver, DeserializeOverWith}};
use yazi_term::sequence::SetSgr;
use yazi_tty::TTY; use yazi_tty::TTY;
pub static YAZI: RoCell<yazi::Yazi> = RoCell::new(); 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}")?; writeln!(stdout, "\nCaused by:\n{src}")?;
} }
use crossterm::style::{Attribute, Print, SetAttributes}; writeln!(
crossterm::execute!(
stdout, stdout,
SetAttributes(Attribute::Reverse.into()), "{}{}Press <Enter> to continue with preset settings...{}",
SetAttributes(Attribute::Bold.into()), SetSgr::Reverse,
Print("Press <Enter> to continue with preset settings..."), SetSgr::Bold,
SetAttributes(Attribute::Reset.into()), SetSgr::Reset
Print("\n"),
)?; )?;
TTY.reader().read_exact(&mut [0])?; TTY.reader().read_exact(&mut [0])?;

View file

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

View file

@ -1,5 +1,5 @@
use crossterm::terminal::WindowSize;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use yazi_term::Dimension;
use super::{Offset, Origin}; use super::{Offset, Origin};
@ -12,15 +12,15 @@ pub struct Position {
impl Position { impl Position {
pub const fn new(origin: Origin, offset: Offset) -> Self { Self { origin, offset } } 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::*; use Origin::*;
let Offset { x, y, width, height } = self.offset; 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 { let new_x = match self.origin {
TopLeft | BottomLeft => x.clamp(0, max_x as i16) as u16, TopLeft | BottomLeft => x.clamp(0, max_x as i16) as u16,
TopCenter | BottomCenter | Center => { 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), TopRight | BottomRight => max_x.saturating_add_signed(x).clamp(0, max_x),
Hovered => unreachable!(), Hovered => unreachable!(),
@ -37,18 +37,18 @@ impl Position {
Rect { Rect {
x: new_x, x: new_x,
y: new_y, 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)), 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 Offset { x, y, width, height } = offset;
let above = let above =
base.y.saturating_add(base.height).saturating_add(height).saturating_add_signed(y) > rows; 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 { let new_y = if above {
base.y.saturating_sub(height.saturating_sub(y.unsigned_abs())) base.y.saturating_sub(height.saturating_sub(y.unsigned_abs()))
} else { } else {
@ -58,7 +58,7 @@ impl Position {
Rect { Rect {
x: new_x, x: new_x,
y: new_y, 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)), 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-binding = { path = "../yazi-binding", version = "26.5.6" }
yazi-config = { path = "../yazi-config", version = "26.5.6" } yazi-config = { path = "../yazi-config", version = "26.5.6" }
yazi-dds = { path = "../yazi-dds", 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-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-runner = { path = "../yazi-runner", version = "26.5.6" } yazi-runner = { path = "../yazi-runner", version = "26.5.6" }
yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" } yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", 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-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" } yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
crossterm = { workspace = true }
dyn-clone = { workspace = true } dyn-clone = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
@ -51,6 +50,3 @@ tokio-util = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
unicode-width = { workspace = true } unicode-width = { workspace = true }
yazi-prebuilt = "0.1.0" 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) } 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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PluginCallback").finish_non_exhaustive() f.debug_struct("PluginCallback").finish_non_exhaustive()
} }

View file

@ -1,6 +1,6 @@
use crossterm::cursor::SetCursorStyle;
use ratatui::layout::{Position, Rect}; use ratatui::layout::{Position, Rect};
use yazi_shared::Layer; 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}; 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 { if self.input.visible {
let Rect { x, y, .. } = self.mgr.area(self.input.position); let Rect { x, y, .. } = self.mgr.area(self.input.position);
return Some(( return Some((

View file

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

View file

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

View file

@ -4,9 +4,9 @@ use anyhow::Result;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use yazi_config::{LAYOUT, popup::{Origin, Position}}; use yazi_config::{LAYOUT, popup::{Origin, Position}};
use yazi_emulator::Dimension;
use yazi_fs::File; use yazi_fs::File;
use yazi_shared::{Id, Ids, url::{UrlBuf, UrlLike}}; use yazi_shared::{Id, Ids, url::{UrlBuf, UrlLike}};
use yazi_term::TERM;
use super::{Backstack, Finder, Folder, History, Mode, Preference, Preview}; use super::{Backstack, Finder, Folder, History, Mode, Preference, Preview};
use crate::{spot::Spot, tab::Selected}; use crate::{spot::Spot, tab::Selected};
@ -90,11 +90,11 @@ impl Tab {
} }
pub fn hovered_rect_based(&self, pos: Position) -> Rect { 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() { if let Some(r) = self.hovered_rect() {
Position::sticky(ws, r, pos.offset) Position::sticky(dim, r, pos.offset)
} else { } 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 std::{sync::Arc, time::Duration};
use tokio::{task::JoinHandle, time::sleep}; use tokio::{task::JoinHandle, time::sleep};
use yazi_emulator::Dimension;
use yazi_scheduler::{Scheduler, TaskSnap, TaskSummary}; use yazi_scheduler::{Scheduler, TaskSnap, TaskSummary};
use yazi_term::TERM;
use super::{TASKS_BORDER, TASKS_PADDING, TASKS_PERCENT}; use super::{TASKS_BORDER, TASKS_PADDING, TASKS_PERCENT};
use crate::AppProxy; use crate::AppProxy;
@ -52,8 +52,8 @@ impl Tasks {
} }
pub fn limit() -> usize { pub fn limit() -> usize {
((Dimension::available().rows * TASKS_PERCENT / 100) ((TERM.dimension().rows * TASKS_PERCENT / 100).saturating_sub(TASKS_BORDER + TASKS_PADDING)
.saturating_sub(TASKS_BORDER + TASKS_PADDING) as usize) as usize)
/ 3 / 3
} }

View file

@ -35,7 +35,7 @@ impl IntoLua for EmberCd<'_> {
lua lua
.create_table_from([ .create_table_from([
("tab", self.tab.get().into_lua(lua)?), ("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) .into_lua(lua)
} }

View file

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

View file

@ -1,59 +1,16 @@
use std::mem; use yazi_term::TERM;
use crossterm::terminal::WindowSize;
use crate::EMULATOR; use crate::EMULATOR;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, Default)]
pub struct Dimension { 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 }
}
}
impl 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)> { pub fn cell_size() -> Option<(f64, f64)> {
let emu = &*EMULATOR; let emu = &*EMULATOR;
Some(if emu.force_16t { Some(if emu.force_16t {
(emu.csi_16t.0 as f64, emu.csi_16t.1 as f64) (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 r
} else if emu.csi_16t.0 != 0 && emu.csi_16t.1 != 0 { } else if emu.csi_16t.0 != 0 && emu.csi_16t.1 != 0 {
(emu.csi_16t.0 as f64, emu.csi_16t.1 as f64) (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 anyhow::Result;
use crossterm::{cursor::{RestorePosition, SavePosition}, execute, style::Print, terminal::{disable_raw_mode, enable_raw_mode}};
use either::Either; use either::Either;
use ratatui::style::Color;
use scopeguard::defer; use scopeguard::defer;
use tokio::time::sleep; use tokio::time::sleep;
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use yazi_macro::writef;
use yazi_shim::cell::RoCell; 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 yazi_tty::{Handle, TTY};
use crate::{Brand, Dimension, Mux, TMUX, Unknown}; use crate::{Brand, Mux, TMUX, Unknown};
pub static EMULATOR: RoCell<Emulator> = RoCell::new(); pub static EMULATOR: RoCell<Emulator> = RoCell::new();
@ -36,25 +38,18 @@ impl Default for Emulator {
impl Emulator { impl Emulator {
pub fn detect() -> Result<Self> { pub fn detect() -> Result<Self> {
defer! { disable_raw_mode().ok(); } defer! { TERM.enter_cooked_mode().ok(); }
enable_raw_mode()?; TERM.enter_raw_mode()?;
let resort = Brand::from_env(); let resort = Brand::from_env();
let kgp_seq = if resort.is_none() { writef!(
Mux::csi("\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\")
} else {
"".into()
};
execute!(
TTY.writer(), TTY.writer(),
SavePosition, "{SaveCursorPos}{}{}{}{}{}{RestoreCursorPos}",
Print(kgp_seq), // Detect KGP If(resort.is_none(), Mux::wrap(KittyGraphicsQuery)),
Print(Mux::csi("\x1b[>q")), // Request terminal version Mux::wrap(RequestXtVersion),
Print("\x1b[16t"), // Request cell size RequestCellPixelSize,
Print("\x1b]11;?\x07"), // Request background color RequestBgColor,
Print(Mux::csi("\x1b[0c")), // Request device attributes Mux::wrap(RequestDA1),
RestorePosition
)?; )?;
let resp = Self::read_until_da1(); let resp = Self::read_until_da1();
@ -83,28 +78,26 @@ impl Emulator {
where where
F: FnOnce(&mut BufWriter<Handle>) -> Result<T>, F: FnOnce(&mut BufWriter<Handle>) -> Result<T>,
{ {
use std::{io::Write, thread, time::Duration}; use std::{thread, time::Duration};
use crossterm::{cursor::{Hide, MoveTo, Show}, queue};
let mut w = TTY.lockout(); let mut w = TTY.lockout();
// I really don't want to add this, // I really don't want to add this,
// But tmux and ConPTY sometimes cause the cursor position to get out of sync. // But tmux and ConPTY sometimes cause the cursor position to get out of sync.
if TMUX.get() || cfg!(windows) { if TMUX.get() || cfg!(windows) {
execute!(w, SavePosition, MoveTo(x, y), Show)?; writef!(w, "{SaveCursorPos}{}{ShowCursor}", MoveTo(x, y))?;
execute!(w, MoveTo(x, y), Show)?; writef!(w, "{}{ShowCursor}", MoveTo(x, y))?;
execute!(w, MoveTo(x, y), Show)?; writef!(w, "{}{ShowCursor}", MoveTo(x, y))?;
thread::sleep(Duration::from_millis(1)); thread::sleep(Duration::from_millis(1));
} else { } else {
queue!(w, SavePosition, MoveTo(x, y))?; write!(w, "{SaveCursorPos}{}", MoveTo(x, y))?;
} }
let result = cb(&mut w); let result = cb(&mut w);
if TMUX.get() || cfg!(windows) { if TMUX.get() || cfg!(windows) {
queue!(w, Hide, RestorePosition)?; write!(w, "{HideCursor}{RestoreCursorPos}")?;
} else { } else {
queue!(w, RestorePosition)?; write!(w, "{RestoreCursorPos}")?;
} }
w.flush()?; w.flush()?;
@ -148,21 +141,13 @@ impl Emulator {
} }
async fn error_to_user() { async fn error_to_user() {
use crossterm::style::{Attribute, Color, Print, ResetColor, SetAttributes, SetForegroundColor};
sleep(Duration::from_millis(400)).await; sleep(Duration::from_millis(400)).await;
_ = crossterm::execute!( _ = writef!(
std::io::stderr(), io::stderr(),
SetForegroundColor(Color::Red), "{}{}\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",
SetAttributes(Attribute::Bold.into()), SetFg(Color::Red),
Print("\r\nTerminal response timeout: "), SetSgr::Bold,
ResetColor, SetSgr::Reset,
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"
),
); );
} }
@ -206,8 +191,6 @@ impl Emulator {
return false; return false;
} }
Dimension::available() TERM.dimension().ratio().is_none_or(|(rw, rh)| rw.floor() as u16 != w || rh.floor() as u16 != h)
.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 anyhow::Result;
use tracing::error; use tracing::error;
use yazi_macro::time; use yazi_macro::{time, writef};
use yazi_shim::cell::SyncCell; use yazi_shim::cell::SyncCell;
use yazi_term::sequence::RequestDeviceStatus;
use yazi_tty::TTY; use yazi_tty::TTY;
use crate::Emulator; use crate::Emulator;
@ -16,15 +17,24 @@ pub static CLOSE: SyncCell<&'static str> = SyncCell::new("");
pub struct Mux; pub struct Mux;
impl Mux { impl Mux {
pub fn csi(s: &str) -> Cow<'_, str> { pub fn wrap<T: Display>(s: T) -> impl Display {
if TMUX.get() { struct Wrapper<T>(T);
Cow::Owned(format!(
"{START}{}{CLOSE}", impl<T: Display> Display for Wrapper<T> {
s.trim_start_matches('\x1b').replace('\x1b', ESCAPE.get()), fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
)) if !TMUX.get() {
} else { return self.0.fmt(f);
Cow::Borrowed(s) }
write!(
f,
"{START}{}{CLOSE}",
self.0.to_string().trim_start_matches('\x1b').replace('\x1b', ESCAPE.get())
)
}
} }
Wrapper(s)
} }
pub fn tmux_passthrough() { pub fn tmux_passthrough() {
@ -56,7 +66,7 @@ impl Mux {
pub fn tmux_drain() -> Result<()> { pub fn tmux_drain() -> Result<()> {
if TMUX.get() { if TMUX.get() {
crossterm::execute!(TTY.writer(), crossterm::style::Print(Self::csi("\x1b[5n")))?; writef!(TTY.writer(), "{}", Self::wrap(RequestDeviceStatus))?;
_ = Emulator::read_until_dsr(); _ = Emulator::read_until_dsr();
} }
Ok(()) 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-config = { path = "../yazi-config", version = "26.5.6" }
yazi-core = { path = "../yazi-core", version = "26.5.6" } yazi-core = { path = "../yazi-core", version = "26.5.6" }
yazi-dds = { path = "../yazi-dds", 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-fs = { path = "../yazi-fs", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-parser = { path = "../yazi-parser", 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-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-term = { path = "../yazi-term", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" } 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-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" } yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", 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 # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
better-panic = "0.3.0" better-panic = "0.3.0"
crossterm = { workspace = true }
fdlimit = "0.3.0" fdlimit = "0.3.0"
futures = { workspace = true } futures = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
@ -60,9 +59,6 @@ tracing-subscriber = { version = "0.3.23", features = [ "env-filter" ] }
libc = { workspace = true } libc = { workspace = true }
signal-hook-tokio = { version = "0.4.0", features = [ "futures-v0_3" ] } 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] [target.'cfg(all(not(target_os = "macos"), not(target_os = "windows")))'.dependencies]
tikv-jemallocator = "0.6.1" tikv-jemallocator = "0.6.1"

View file

@ -6,14 +6,13 @@ use yazi_actor::Ctx;
use yazi_core::Core; use yazi_core::Core;
use yazi_macro::act; use yazi_macro::act;
use yazi_shared::{data::Data, event::{Event, NEED_RENDER}}; use yazi_shared::{data::Data, event::{Event, NEED_RENDER}};
use yazi_term::Term; use yazi_tui::Raterm;
use crate::{Dispatcher, Signals}; use crate::{Dispatcher, Signals};
pub(crate) struct App { pub(crate) struct App {
pub(crate) core: Core, pub(crate) core: Core,
pub(crate) term: Option<Term>, pub(crate) term: Option<Raterm>,
pub(crate) signals: Signals,
need_render: u8, need_render: u8,
pub(crate) last_render: Instant, pub(crate) last_render: Instant,
@ -21,11 +20,10 @@ pub(crate) struct App {
} }
impl App { impl App {
fn make(term: Term, signals: Signals) -> Result<Self> { fn make(term: Raterm) -> Result<Self> {
Ok(Self { Ok(Self {
core: Core::make(), core: Core::make(),
term: Some(term), term: Some(term),
signals,
need_render: 0, need_render: 0,
last_render: Instant::now(), last_render: Instant::now(),
@ -34,12 +32,13 @@ impl App {
} }
pub(crate) async fn serve() -> Result<()> { pub(crate) async fn serve() -> Result<()> {
let term = Term::start()?; let term = Raterm::start()?;
let (mut rx, signals) = (Event::take(), Signals::start()?); Signals::start()?;
let mut app = Self::make(term, signals)?; let mut app = Self::make(term)?;
app.bootstrap()?; app.bootstrap()?;
let mut rx = Event::take();
loop { loop {
if let Some(t) = app.next_render.take() { if let Some(t) = app.next_render.take() {
select! { 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 anyhow::Result;
use crossterm::{cursor::{MoveTo, SetCursorStyle, Show}, execute, queue, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; use ratatui::{CompletedFrame, backend::Backend, buffer::Buffer, layout::Position};
use ratatui::{CompletedFrame, backend::{Backend, CrosstermBackend}, buffer::Buffer, layout::Position};
use yazi_actor::{Ctx, lives::Lives}; use yazi_actor::{Ctx, lives::Lives};
use yazi_binding::runtime_scope; use yazi_binding::runtime_scope;
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
use yazi_macro::{act, succ}; use yazi_macro::{act, succ, writef};
use yazi_plugin::LUA; use yazi_plugin::LUA;
use yazi_shared::{data::Data, event::NEED_RENDER}; use yazi_shared::{data::Data, event::NEED_RENDER};
use yazi_term::{CursorStyle, sequence::{BeginSyncUpdate, EndSyncUpdate, MoveTo, SetCursorStyle, ShowCursor}};
use yazi_tty::TTY; use yazi_tty::TTY;
use yazi_tui::RatermBackend;
use yazi_widgets::COLLISION; use yazi_widgets::COLLISION;
use crate::{app::App, root::Root}; use crate::{app::App, root::Root};
@ -90,10 +91,10 @@ impl App {
} }
let patches = frame.buffer.diff(&new); 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); static COUNT: AtomicU8 = AtomicU8::new(0);
if push && COUNT.fetch_add(1, Ordering::Relaxed) != 0 { if push && COUNT.fetch_add(1, Ordering::Relaxed) != 0 {
return; return;
@ -101,12 +102,18 @@ impl App {
return; return;
} }
_ = if push { if push {
queue!(TTY.writer(), BeginSynchronizedUpdate) write!(TTY.writer(), "{BeginSyncUpdate}").ok();
} else if let Some((Position { x, y }, shape)) = cursor { } 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 { } 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 ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, List, ListItem, Widget}};
use yazi_config::{THEME, popup::{Offset, Position}}; use yazi_config::{THEME, popup::{Offset, Position}};
use yazi_core::Core; use yazi_core::Core;
use yazi_emulator::Dimension;
use yazi_shared::strand::StrandLike; use yazi_shared::strand::StrandLike;
use yazi_term::TERM;
pub(crate) struct Cmp<'a> { pub(crate) struct Cmp<'a> {
core: &'a Core, core: &'a Core,
@ -38,7 +38,7 @@ impl Widget for Cmp<'_> {
.collect(); .collect();
let input_area = self.core.mgr.area(self.core.input.position); 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, x: 1,
y: 0, y: 0,
width: input_area.width.saturating_sub(2), width: input_area.width.saturating_sub(2),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,24 +1,39 @@
use anyhow::Result; use anyhow::Result;
use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind}; use tokio_stream::StreamExt;
use futures::StreamExt;
use tokio::{select, sync::mpsc};
use yazi_config::YAZI;
use yazi_shared::event::{Event, Replier};
pub(super) struct Signals { pub(super) struct Signals;
pub(super) tx: mpsc::UnboundedSender<(bool, Replier)>,
}
impl Signals { impl Signals {
pub(super) fn start() -> Result<Self> { pub(super) fn start() -> Result<()> {
let (tx, rx) = mpsc::unbounded_channel(); #[cfg(unix)]
Self::spawn(rx)?; 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)] #[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 libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGSTOP, SIGTERM, SIGTSTP};
use tracing::error; use tracing::error;
use yazi_term::YIELD_TO_SUBPROCESS; use yazi_term::YIELD_TO_SUBPROCESS;
@ -30,16 +45,14 @@ impl Signals {
return false; return false;
} }
SIGTSTP => { SIGTSTP => {
tokio::spawn(async move { yazi_scheduler::AppProxy::stop().await;
yazi_scheduler::AppProxy::stop().await; if unsafe { libc::kill(0, SIGSTOP) } != 0 {
if unsafe { libc::kill(0, SIGSTOP) } != 0 { error!("Failed to stop the process:\n{}", std::io::Error::last_os_error());
error!("Failed to stop the process:\n{}", std::io::Error::last_os_error()); yazi_proxy::AppProxy::quit(Default::default());
yazi_proxy::AppProxy::quit(Default::default()); }
}
});
} }
SIGCONT if YIELD_TO_SUBPROCESS.try_acquire().is_ok() => { 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)] #[cfg(windows)]
#[inline] async fn handle(_: ()) -> bool { unreachable!() }
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(())
}
} }

View file

@ -59,6 +59,7 @@ impl Cha {
where where
U: AsUrl, U: AsUrl,
{ {
#[allow(unused_mut)]
let mut kind = ChaKind::DUMMY; let mut kind = ChaKind::DUMMY;
let mode = r#type.map(ChaMode::from_bare).unwrap_or_default(); 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 { impl TryFrom<Attrs> for std::fs::Permissions {
type Error = (); type Error = ();
fn try_from(value: Attrs) -> Result<Self, Self::Error> { fn try_from(_value: Attrs) -> Result<Self, Self::Error> {
#[cfg(unix)] #[cfg(unix)]
if let Some(mode) = value.mode { if let Some(mode) = _value.mode {
return Ok(mode.into()); return Ok(mode.into());
} }

View file

@ -13,3 +13,13 @@ macro_rules! errln {
writeln!(std::io::stderr(), $($tt)*) 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-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" } yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", 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-vfs = { path = "../yazi-vfs", version = "26.5.6" }
yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
bitflags = { workspace = true } bitflags = { workspace = true }
crossterm = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
paste = { workspace = true } paste = { workspace = true }
@ -41,6 +41,3 @@ serde = { workspace = true }
serde_with = { workspace = true } serde_with = { workspace = true }
strum = { workspace = true } strum = { workspace = true }
tokio = { 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 mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_term::event::MouseEvent;
#[derive(Debug)] #[derive(Debug)]
pub struct MouseForm { 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), AppQuit(crate::app::QuitForm),
AppReflow(crate::app::ReflowForm), AppReflow(crate::app::ReflowForm),
AppResize(crate::app::ReflowForm), AppResize(crate::app::ReflowForm),
AppResume(crate::app::ResumeForm), AppResume(crate::app::ReflowForm),
AppStop(crate::app::StopForm), AppStop(crate::VoidForm),
AppTheme(crate::VoidForm), AppTheme(crate::VoidForm),
AppTitle(crate::app::TitleForm), AppTitle(crate::app::TitleForm),
AppUpdateProgress(crate::app::UpdateProgressForm), AppUpdateProgress(crate::app::UpdateProgressForm),
@ -352,6 +352,7 @@ try_from_spark!(
crate::VoidForm, crate::VoidForm,
app:bootstrap, app:bootstrap,
app:focus, app:focus,
app:stop,
app:theme, app:theme,
mgr:back, mgr:back,
mgr:bulk_rename, 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::MouseForm, app:mouse);
try_from_spark!(crate::app::PluginForm, app:plugin, app:plugin_do); 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::QuitForm, app:quit, mgr:quit);
try_from_spark!(crate::app::ReflowForm, app:reflow, app:resize); try_from_spark!(crate::app::ReflowForm, app:reflow, app:resize, app:resume);
try_from_spark!(crate::app::ResumeForm, app:resume);
try_from_spark!(crate::app::StopForm, app:stop);
try_from_spark!(crate::app::TitleForm, app:title); try_from_spark!(crate::app::TitleForm, app:title);
try_from_spark!(crate::app::UpdateProgressForm, app:update_progress); try_from_spark!(crate::app::UpdateProgressForm, app:update_progress);
try_from_spark!(crate::cmp::CloseForm, cmp:close); 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-runner = { path = "../yazi-runner", version = "26.5.6" }
yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" }
yazi-term = { path = "../yazi-term", 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" } yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" }
# External dependencies # External dependencies

View file

@ -15,10 +15,10 @@ workspace = true
[dependencies] [dependencies]
yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-term = { path = "../yazi-term", version = "26.5.6" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
crossterm = { workspace = true }
dyn-clone = { workspace = true } dyn-clone = { workspace = true }
foldhash = { workspace = true } foldhash = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
@ -41,6 +41,3 @@ uzers = { workspace = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61.2", features = [ "Win32_UI_Shell" ] } 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; use dyn_clone::DynClone;
@ -21,7 +21,7 @@ impl Clone for Box<dyn DataAny> {
fn clone(&self) -> Self { dyn_clone::clone_box(&**self) } 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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DataAny").finish_non_exhaustive() f.debug_struct("DataAny").finish_non_exhaustive()
} }

View file

@ -1,4 +1,3 @@
use crossterm::event::{KeyEvent, MouseEvent};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_shim::cell::RoCell; use yazi_shim::cell::RoCell;
@ -11,12 +10,8 @@ static RX: RoCell<mpsc::UnboundedReceiver<Event>> = RoCell::new();
pub enum Event { pub enum Event {
Call(ActionCow), Call(ActionCow),
Seq(Vec<ActionCow>), Seq(Vec<ActionCow>),
Term(yazi_term::event::Event),
Render(bool), Render(bool),
Key(KeyEvent),
Mouse(MouseEvent),
Resize,
Focus,
Paste(String),
} }
impl Event { 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_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() { pub fn init() {
LOCAL_SET.with(tokio::task::LocalSet::new); 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::Regular(_), U::Regular(_)) => Ok(prefix),
(U::Search { .. }, U::Search { .. }) => Ok(prefix), (U::Search { .. }, U::Search { .. }) => Ok(prefix),
(U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => { (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, .. }) => { (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 // Both are local files
@ -380,16 +380,16 @@ impl<'a> Url<'a> {
// Only the entry of archives is a local file // Only the entry of archives is a local file
(U::Regular(_), U::Archive { .. }) => { (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 { .. }) => { (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(_)) => { (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 { .. }) => { (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 // Independent virtual file space
@ -414,10 +414,10 @@ impl<'a> Url<'a> {
(U::Regular(_), U::Regular(_)) => Ok(suffix), (U::Regular(_), U::Regular(_)) => Ok(suffix),
(U::Search { .. }, U::Search { .. }) => Ok(suffix), (U::Search { .. }, U::Search { .. }) => Ok(suffix),
(U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => { (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, .. }) => { (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 // Both are local files
@ -426,16 +426,16 @@ impl<'a> Url<'a> {
// Only the entry of archives is a local file // Only the entry of archives is a local file
(U::Regular(_), U::Archive { .. }) => { (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 { .. }) => { (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(_)) => { (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 { .. }) => { (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 // 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 # External dependencies
arc-swap = { workspace = true } arc-swap = { workspace = true }
crossterm = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
ratatui = { workspace = true } ratatui = { workspace = true }
@ -33,6 +32,3 @@ unicode-width = { workspace = true }
[dependencies.unicode-segmentation] [dependencies.unicode-segmentation]
version = "1.13.2" 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] [package]
name = "yazi-term" name = "yazi-term"
description = "Yazi terminal extensions" description = "Cross-platform virtual terminal"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -13,17 +13,23 @@ rust-version.workspace = true
workspace = true workspace = true
[dependencies] [dependencies]
yazi-config = { path = "../yazi-config", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" }
yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
yazi-shim = { path = "../yazi-shim", version = "26.5.6" }
yazi-tty = { path = "../yazi-tty", version = "26.5.9" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
crossterm = { workspace = true } base64 = { workspace = true }
ratatui = { workspace = true } bitflags = { workspace = true }
tokio = { workspace = true } futures = { workspace = true }
parking_lot = { workspace = true }
ratatui = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(unix)'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } 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 # 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. A fork of [Termina](https://github.com/helix-editor/termina) used by Yazi.
[source]: https://github.com/sxyazi/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