refactor: move several UI-related APIs from the ya namespace to ui (#2939)

This commit is contained in:
三咲雅 misaki masa 2025-07-02 23:26:13 +08:00 committed by sxyazi
parent 4ff7dae721
commit ffdd74b6ab
No known key found for this signature in database
15 changed files with 205 additions and 96 deletions

View file

@ -150,7 +150,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Setup LXD - name: Setup LXD
uses: canonical/setup-lxd@v0.1.2 uses: canonical/setup-lxd@v0.1.3
- name: Setup snapcraft - name: Setup snapcraft
run: sudo snap install --classic snapcraft run: sudo snap install --classic snapcraft

17
Cargo.lock generated
View file

@ -1224,6 +1224,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "io-uring"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"libc",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -2666,17 +2677,19 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.45.1" version = "1.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
"io-uring",
"libc", "libc",
"mio", "mio",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"slab",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.52.0",

View file

@ -45,7 +45,7 @@ scopeguard = "1.2.0"
serde = { version = "1.0.219", features = [ "derive" ] } serde = { version = "1.0.219", features = [ "derive" ] }
serde_json = "1.0.140" serde_json = "1.0.140"
syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
tokio = { version = "1.45.1", features = [ "full" ] } tokio = { version = "1.46.0", features = [ "full" ] }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
tokio-util = "0.7.15" tokio-util = "0.7.15"
toml = { version = "0.8.23" } toml = { version = "0.8.23" }

View file

@ -28,7 +28,7 @@ function Header:cwd()
end end
local s = ya.readable_path(tostring(self._current.cwd)) .. self:flags() local s = ya.readable_path(tostring(self._current.cwd)) .. self:flags()
return ui.Span(ya.truncate(s, { max = max, rtl = true })):style(th.mgr.cwd) return ui.Span(ui.truncate(s, { max = max, rtl = true })):style(th.mgr.cwd)
end end
function Header:flags() function Header:flags()

View file

@ -23,7 +23,7 @@ function Tabs:redraw()
local pos = lines[1]:width() local pos = lines[1]:width()
local max = math.floor(self:inner_width() / #cx.tabs) local max = math.floor(self:inner_width() / #cx.tabs)
for i = 1, #cx.tabs do for i = 1, #cx.tabs do
local name = ya.truncate(string.format(" %d %s ", i, cx.tabs[i].name), { max = max }) local name = ui.truncate(string.format(" %d %s ", i, cx.tabs[i].name), { max = max })
if i == cx.tabs.idx then if i == cx.tabs.idx then
lines[#lines + 1] = ui.Line { lines[#lines + 1] = ui.Line {
ui.Span(th.tabs.sep_inner.open):style(th.tabs.inactive), ui.Span(th.tabs.sep_inner.open):style(th.tabs.inactive),

View file

@ -33,7 +33,7 @@ function M:peek(job)
left[#left] = ui.Line { left[#left] = ui.Line {
left[#left], left[#left],
ya.truncate(f.path, { ui.truncate(f.path, {
rtl = true, rtl = true,
max = math.max(0, job.area.w - ui.width(left[#left]) - ui.width(right[#right])), max = math.max(0, job.area.w - ui.width(left[#left]) - ui.width(right[#right])),
}), }),

View file

@ -11,7 +11,7 @@ end)
function M:entry() function M:entry()
ya.emit("escape", { visual = true }) ya.emit("escape", { visual = true })
local _permit = ya.hide() local _permit = ui.hide()
local cwd, selected = state() local cwd, selected = state()
local output, err = M.run_with(cwd, selected) local output, err = M.run_with(cwd, selected)

View file

@ -87,7 +87,7 @@ local function entry()
return fail("No directory history found, check Zoxide's doc to set it up and restart Yazi.") return fail("No directory history found, check Zoxide's doc to set it up and restart Yazi.")
end end
local _permit = ya.hide() local _permit = ui.hide()
local child, err1 = Command("zoxide") local child, err1 = Command("zoxide")
:arg({ "query", "-i", "--exclude", st.cwd }) :arg({ "query", "-i", "--exclude", st.cwd })
:env("SHELL", "sh") :env("SHELL", "sh")

View file

@ -28,8 +28,11 @@ pub fn compose(lua: &Lua) -> mlua::Result<Value> {
b"Wrap" => super::Wrap::compose(lua)?, b"Wrap" => super::Wrap::compose(lua)?,
b"area" => super::Utils::area(lua)?, b"area" => super::Utils::area(lua)?,
b"hide" => super::Utils::hide(lua)?,
b"width" => super::Utils::width(lua)?, b"width" => super::Utils::width(lua)?,
b"redraw" => super::Utils::redraw(lua)?, b"redraw" => super::Utils::redraw(lua)?,
b"render" => super::Utils::render(lua)?,
b"truncate" => super::Utils::truncate(lua)?,
_ => return Ok(Value::Nil), _ => return Ok(Value::Nil),
} }
.into_lua(lua) .into_lua(lua)

View file

@ -1,9 +1,12 @@
use mlua::{ExternalError, IntoLua, Lua, ObjectLike, Table, Value}; use mlua::{AnyUserData, ExternalError, IntoLua, Lua, ObjectLike, Table, Value};
use tracing::error; use tracing::error;
use unicode_width::UnicodeWidthStr; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use yazi_config::LAYOUT; use yazi_config::LAYOUT;
use yazi_macro::render;
use yazi_proxy::{AppProxy, HIDER};
use super::{Line, Rect, Span}; use super::{Line, Rect, Span};
use crate::bindings::{Permit, PermitRef};
pub(super) struct Utils; pub(super) struct Utils;
@ -22,6 +25,22 @@ impl Utils {
f.into_lua(lua) f.into_lua(lua)
} }
pub(super) fn hide(lua: &Lua) -> mlua::Result<Value> {
let f = lua.create_async_function(|lua, ()| async move {
if lua.named_registry_value::<PermitRef<fn()>>("HIDE_PERMIT").is_ok_and(|h| h.is_some()) {
return Err("Cannot hide while already hidden".into_lua_err());
}
let permit = HIDER.acquire().await.unwrap();
AppProxy::stop().await;
lua.set_named_registry_value("HIDE_PERMIT", Permit::new(permit, AppProxy::resume as fn()))?;
lua.named_registry_value::<AnyUserData>("HIDE_PERMIT")
})?;
f.into_lua(lua)
}
pub(super) fn width(lua: &Lua) -> mlua::Result<Value> { pub(super) fn width(lua: &Lua) -> mlua::Result<Value> {
let f = lua.create_function(|_, v: Value| match v { let f = lua.create_function(|_, v: Value| match v {
Value::String(s) => { Value::String(s) => {
@ -77,4 +96,149 @@ impl Utils {
f.into_lua(lua) f.into_lua(lua)
} }
pub(super) fn render(lua: &Lua) -> mlua::Result<Value> {
let f = lua.create_function(|_, ()| {
render!();
Ok(())
})?;
f.into_lua(lua)
}
pub(super) fn truncate(lua: &Lua) -> mlua::Result<Value> {
fn traverse(
it: impl Iterator<Item = (usize, char)>,
max: usize,
) -> (Option<usize>, usize, bool) {
let (mut adv, mut last) = (0, 0);
let idx = it
.take_while(|&(_, c)| {
(last, adv) = (adv, adv + c.width().unwrap_or(0));
adv <= max
})
.map(|(i, _)| i)
.last();
(idx, last, adv > max)
}
let f = lua.create_function(|lua, (s, t): (mlua::String, Table)| {
let b = s.as_bytes();
if b.is_empty() {
return Ok(s);
}
let max = t.raw_get("max")?;
if b.len() <= max {
return Ok(s);
} else if max < 1 {
return lua.create_string("");
}
let lossy = String::from_utf8_lossy(&b);
let rtl = t.raw_get("rtl").unwrap_or(false);
let (idx, width, remain) = if rtl {
traverse(lossy.char_indices().rev(), max)
} else {
traverse(lossy.char_indices(), max)
};
let Some(idx) = idx else { return lua.create_string("") };
if !remain {
return Ok(s);
}
let result: Vec<_> = match (rtl, width == max) {
(false, false) => {
let len = lossy[idx..].chars().next().map_or(0, |c| c.len_utf8());
lossy[..idx + len].bytes().chain("".bytes()).collect()
}
(false, true) => lossy[..idx].bytes().chain("".bytes()).collect(),
(true, false) => "".bytes().chain(lossy[idx..].bytes()).collect(),
(true, true) => {
let len = lossy[idx..].chars().next().map_or(0, |c| c.len_utf8());
"".bytes().chain(lossy[idx + len..].bytes()).collect()
}
};
lua.create_string(result)
})?;
f.into_lua(lua)
}
}
#[cfg(test)]
mod tests {
use mlua::chunk;
use super::*;
fn truncate(s: &str, max: usize, rtl: bool) -> String {
let lua = Lua::new();
let f = Utils::truncate(&lua).unwrap();
lua
.load(chunk! {
return $f($s, { max = $max, rtl = $rtl })
})
.call(())
.unwrap()
}
#[test]
fn test_truncate() {
assert_eq!(truncate("你好world", 0, false), "");
assert_eq!(truncate("你好world", 1, false), "");
assert_eq!(truncate("你好world", 2, false), "");
assert_eq!(truncate("你好,世界", 3, false), "你…");
assert_eq!(truncate("你好,世界", 4, false), "你…");
assert_eq!(truncate("你好,世界", 5, false), "你好…");
assert_eq!(truncate("Hello, world", 5, false), "Hell…");
assert_eq!(truncate("Ni好世界", 3, false), "Ni…");
}
#[test]
fn test_truncate_rtl() {
assert_eq!(truncate("world你好", 0, true), "");
assert_eq!(truncate("world你好", 1, true), "");
assert_eq!(truncate("world你好", 2, true), "");
assert_eq!(truncate("你好,世界", 3, true), "…界");
assert_eq!(truncate("你好,世界", 4, true), "…界");
assert_eq!(truncate("你好,世界", 5, true), "…世界");
assert_eq!(truncate("Hello, world", 5, true), "…orld");
assert_eq!(truncate("你好Shi界", 3, true), "…界");
}
#[test]
fn test_truncate_oboe() {
assert_eq!(truncate("Hello, world", 11, false), "Hello, wor…");
assert_eq!(truncate("你好,世界", 9, false), "你好,世…");
assert_eq!(truncate("你好世Jie", 9, false), "你好,世…");
assert_eq!(truncate("Hello, world", 11, true), "…llo, world");
assert_eq!(truncate("你好,世界", 9, true), "…好,世界");
assert_eq!(truncate("Ni好世界", 9, true), "…好,世界");
}
#[test]
fn test_truncate_exact() {
assert_eq!(truncate("Hello, world", 12, false), "Hello, world");
assert_eq!(truncate("你好,世界", 10, false), "你好,世界");
assert_eq!(truncate("Hello, world", 12, true), "Hello, world");
assert_eq!(truncate("你好,世界", 10, true), "你好,世界");
}
#[test]
fn test_truncate_overflow() {
assert_eq!(truncate("Hello, world", 13, false), "Hello, world");
assert_eq!(truncate("你好,世界", 11, false), "你好,世界");
assert_eq!(truncate("Hello, world", 13, true), "Hello, world");
assert_eq!(truncate("你好,世界", 11, true), "你好,世界");
}
} }

View file

@ -3,7 +3,7 @@ use yazi_binding::Id;
use yazi_proxy::{AppProxy, HIDER}; use yazi_proxy::{AppProxy, HIDER};
use super::Utils; use super::Utils;
use crate::bindings::{Permit, PermitRef}; use crate::{bindings::{Permit, PermitRef}, deprecate};
impl Utils { impl Utils {
pub(super) fn id(lua: &Lua) -> mlua::Result<Function> { pub(super) fn id(lua: &Lua) -> mlua::Result<Function> {
@ -18,6 +18,8 @@ impl Utils {
pub(super) fn hide(lua: &Lua) -> mlua::Result<Function> { pub(super) fn hide(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, ()| async move { lua.create_async_function(|lua, ()| async move {
deprecate!(lua, "`ya.hide()` is deprecated, use `ui.hide()` instead, in your {}\nSee #2939 for more details: https://github.com/sxyazi/yazi/pull/2939");
if lua.named_registry_value::<PermitRef<fn()>>("HIDE_PERMIT").is_ok_and(|h| h.is_some()) { if lua.named_registry_value::<PermitRef<fn()>>("HIDE_PERMIT").is_ok_and(|h| h.is_some()) {
return Err("Cannot hide while already hidden".into_lua_err()); return Err("Cannot hide while already hidden".into_lua_err());
} }

View file

@ -4,10 +4,13 @@ use yazi_macro::{emit, render};
use yazi_shared::{Layer, event::Cmd}; use yazi_shared::{Layer, event::Cmd};
use super::Utils; use super::Utils;
use crate::deprecate;
impl Utils { impl Utils {
pub(super) fn render(lua: &Lua) -> mlua::Result<Function> { pub(super) fn render(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|_, ()| { lua.create_function(|lua, ()| {
deprecate!(lua, "`ya.render()` is deprecated, use `ui.render()` instead, in your {}\nSee #2939 for more details: https://github.com/sxyazi/yazi/pull/2939");
render!(); render!();
Ok(()) Ok(())
}) })

View file

@ -44,10 +44,7 @@ impl Utils {
if pos.is_nil() { if pos.is_nil() {
pos = t.raw_get("position")?; pos = t.raw_get("position")?;
if !pos.is_nil() { if !pos.is_nil() {
deprecate!( deprecate!(lua, "The `position` property of `ya.input()` is deprecated, use `pos` instead in your {}\nSee #2921 for more details: https://github.com/sxyazi/yazi/pull/2921");
lua,
"The `position` property of `ya.input()` is deprecated, use `pos` instead in your {}\nSee #2921 for more details: https://github.com/sxyazi/yazi/pull/2921"
);
} }
} }

View file

@ -3,7 +3,7 @@ use twox_hash::XxHash3_128;
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
use super::Utils; use super::Utils;
use crate::CLIPBOARD; use crate::{CLIPBOARD, deprecate};
impl Utils { impl Utils {
pub(super) fn hash(lua: &Lua) -> mlua::Result<Function> { pub(super) fn hash(lua: &Lua) -> mlua::Result<Function> {
@ -41,6 +41,8 @@ impl Utils {
} }
lua.create_function(|lua, (s, t): (mlua::String, Table)| { lua.create_function(|lua, (s, t): (mlua::String, Table)| {
deprecate!(lua, "`ya.truncate()` is deprecated, use `ui.truncate()` instead, in your {}\nSee #2939 for more details: https://github.com/sxyazi/yazi/pull/2939");
let b = s.as_bytes(); let b = s.as_bytes();
if b.is_empty() { if b.is_empty() {
return Ok(s); return Ok(s);
@ -93,79 +95,3 @@ impl Utils {
}) })
} }
} }
#[cfg(test)]
mod tests {
use mlua::chunk;
use super::*;
fn truncate(s: &str, max: usize, rtl: bool) -> String {
let lua = Lua::new();
let f = Utils::truncate(&lua).unwrap();
lua
.load(chunk! {
return $f($s, { max = $max, rtl = $rtl })
})
.call(())
.unwrap()
}
#[test]
fn test_truncate() {
assert_eq!(truncate("你好world", 0, false), "");
assert_eq!(truncate("你好world", 1, false), "");
assert_eq!(truncate("你好world", 2, false), "");
assert_eq!(truncate("你好,世界", 3, false), "你…");
assert_eq!(truncate("你好,世界", 4, false), "你…");
assert_eq!(truncate("你好,世界", 5, false), "你好…");
assert_eq!(truncate("Hello, world", 5, false), "Hell…");
assert_eq!(truncate("Ni好世界", 3, false), "Ni…");
}
#[test]
fn test_truncate_rtl() {
assert_eq!(truncate("world你好", 0, true), "");
assert_eq!(truncate("world你好", 1, true), "");
assert_eq!(truncate("world你好", 2, true), "");
assert_eq!(truncate("你好,世界", 3, true), "…界");
assert_eq!(truncate("你好,世界", 4, true), "…界");
assert_eq!(truncate("你好,世界", 5, true), "…世界");
assert_eq!(truncate("Hello, world", 5, true), "…orld");
assert_eq!(truncate("你好Shi界", 3, true), "…界");
}
#[test]
fn test_truncate_oboe() {
assert_eq!(truncate("Hello, world", 11, false), "Hello, wor…");
assert_eq!(truncate("你好,世界", 9, false), "你好,世…");
assert_eq!(truncate("你好世Jie", 9, false), "你好,世…");
assert_eq!(truncate("Hello, world", 11, true), "…llo, world");
assert_eq!(truncate("你好,世界", 9, true), "…好,世界");
assert_eq!(truncate("Ni好世界", 9, true), "…好,世界");
}
#[test]
fn test_truncate_exact() {
assert_eq!(truncate("Hello, world", 12, false), "Hello, world");
assert_eq!(truncate("你好,世界", 10, false), "你好,世界");
assert_eq!(truncate("Hello, world", 12, true), "Hello, world");
assert_eq!(truncate("你好,世界", 10, true), "你好,世界");
}
#[test]
fn test_truncate_overflow() {
assert_eq!(truncate("Hello, world", 13, false), "Hello, world");
assert_eq!(truncate("你好,世界", 11, false), "你好,世界");
assert_eq!(truncate("Hello, world", 13, true), "Hello, world");
assert_eq!(truncate("你好,世界", 11, true), "你好,世界");
}
}

View file

@ -30,8 +30,9 @@ impl Input {
return; return;
} }
let (o_cur, n_cur) = (snap.cursor, opt.step.cursor(snap)); let o_cur = snap.cursor;
render!(self.handle_op(n_cur, false)); render!(self.handle_op(opt.step.cursor(snap), false));
let n_cur = self.snap().cursor;
let (limit, snap) = (self.limit, self.snap_mut()); let (limit, snap) = (self.limit, self.snap_mut());
if snap.value.is_empty() { if snap.value.is_empty() {