feat: new stash command to let users customize back and forward command behavior (#3380)

This commit is contained in:
三咲雅 misaki masa 2025-11-29 05:27:17 +08:00 committed by GitHub
parent 3561867b99
commit a8f2498471
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 702 additions and 238 deletions

View file

@ -118,13 +118,5 @@ impl<'a> Ctx<'a> {
pub fn tasks(&self) -> &Tasks { &self.tasks }
#[inline]
pub fn source(&self) -> Source {
use Source::*;
match self.source {
Key if self.level != 1 => Ind,
Emit if self.level != 1 => EmitInd,
Relay if self.level != 1 => RelayInd,
s => s,
}
}
pub fn source(&self) -> Source { if self.level != 1 { Source::Ind } else { self.source } }
}

View file

@ -14,7 +14,7 @@ impl Actor for Back {
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
if let Some(u) = cx.tab_mut().backstack.shift_backward().cloned() {
return act!(mgr:cd, cx, (u, CdSource::Back));
act!(mgr:cd, cx, (u, CdSource::Back))?;
}
succ!();
}

View file

@ -9,7 +9,7 @@ use yazi_fs::{File, FilesOp, path::expand_url};
use yazi_macro::{act, err, render, succ};
use yazi_parser::mgr::CdOpt;
use yazi_proxy::{CmpProxy, InputProxy, MgrProxy};
use yazi_shared::{Debounce, data::Data, errors::InputError, url::{AsUrl, UrlBuf, UrlLike}};
use yazi_shared::{Debounce, data::Data, errors::InputError, url::{UrlBuf, UrlLike}};
use yazi_vfs::VfsFile;
use crate::{Actor, Ctx};
@ -33,18 +33,8 @@ impl Actor for Cd {
}
// Take parent to history
if let Some(rep) = tab.parent.take() {
tab.history.insert(rep.url.to_owned(), rep);
}
// Backstack
if opt.source.big_jump() {
if tab.current.url.is_regular() {
tab.backstack.push(tab.current.url.as_url());
}
if opt.target.is_regular() {
tab.backstack.push(opt.target.as_url());
}
if let Some(t) = tab.parent.take() {
tab.history.insert(t.url.to_owned(), t);
}
// Current
@ -58,10 +48,12 @@ impl Actor for Cd {
}
err!(Pubsub::pub_after_cd(tab.id, tab.cwd()));
act!(mgr:displace, cx)?;
act!(mgr:hidden, cx)?;
act!(mgr:sort, cx)?;
act!(mgr:hover, cx)?;
act!(mgr:refresh, cx)?;
act!(mgr:stash, cx, opt).ok();
succ!(render!());
}
}

View file

@ -0,0 +1,59 @@
use anyhow::{Result, bail};
use yazi_macro::{act, succ};
use yazi_parser::{VoidOpt, mgr::{CdSource, DisplaceDoOpt}};
use yazi_proxy::MgrProxy;
use yazi_shared::{data::Data, url::UrlLike};
use yazi_vfs::provider;
use crate::{Actor, Ctx};
pub struct Displace;
impl Actor for Displace {
type Options = VoidOpt;
const NAME: &str = "displace";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
if cx.cwd().is_absolute() {
succ!();
}
let tab = cx.tab().id;
let from = cx.cwd().to_owned();
tokio::spawn(async move {
if let Ok(to) = provider::absolute(&from).await
&& to.is_owned()
{
MgrProxy::displace_do(tab, DisplaceDoOpt { to: to.into(), from });
}
});
succ!();
}
}
// --- Do
pub struct DisplaceDo;
impl Actor for DisplaceDo {
type Options = DisplaceDoOpt;
const NAME: &str = "displace_do";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if !opt.to.is_absolute() {
bail!("Target URL must be absolute");
}
if cx.cwd() != opt.from {
succ!()
} else if let Some(hovered) = cx.hovered()
&& let Ok(url) = opt.to.try_join(hovered.urn())
{
act!(mgr:reveal, cx, (url, CdSource::Displace))
} else {
act!(mgr:cd, cx, (opt.to, CdSource::Displace))
}
}
}

View file

@ -13,9 +13,9 @@ impl Actor for Forward {
const NAME: &str = "forward";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
match cx.tab_mut().backstack.shift_forward().cloned() {
Some(u) => act!(mgr:cd, cx, (u, CdSource::Forward)),
None => succ!(),
if let Some(u) = cx.tab_mut().backstack.shift_forward().cloned() {
act!(mgr:cd, cx, (u, CdSource::Forward))?;
}
succ!()
}
}

View file

@ -6,6 +6,7 @@ yazi_macro::mod_flat!(
close
copy
create
displace
download
enter
escape
@ -36,6 +37,7 @@ yazi_macro::mod_flat!(
shell
sort
spot
stash
suspend
tab_close
tab_create

View file

@ -1,4 +1,4 @@
use std::{borrow::Cow, mem, time::Duration};
use std::{borrow::Cow, time::Duration};
use anyhow::Result;
use tokio::pin;
@ -6,7 +6,7 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use yazi_config::popup::InputCfg;
use yazi_fs::{FilesOp, cha::Cha};
use yazi_macro::{act, succ};
use yazi_parser::{VoidOpt, mgr::{SearchOpt, SearchOptVia}};
use yazi_parser::{VoidOpt, mgr::{CdSource, SearchOpt, SearchOptVia}};
use yazi_plugin::external;
use yazi_proxy::{InputProxy, MgrProxy};
use yazi_shared::data::Data;
@ -107,15 +107,10 @@ impl Actor for SearchStop {
handle.abort();
}
if !tab.cwd().is_search() {
succ!();
if tab.cwd().is_search() {
act!(mgr:cd, cx, (tab.cwd().to_regular()?, CdSource::Escape))?;
}
let rep = tab.history.remove_or(tab.cwd().to_regular()?);
drop(mem::replace(&mut tab.current, rep));
act!(mgr:hidden, cx)?;
act!(mgr:sort, cx)?;
act!(mgr:refresh, cx)
succ!();
}
}

View file

@ -0,0 +1,31 @@
use anyhow::Result;
use yazi_dds::spark::SparkKind;
use yazi_macro::succ;
use yazi_parser::mgr::StashOpt;
use yazi_shared::{Source, data::Data, url::AsUrl};
use crate::{Actor, Ctx};
pub struct Stash;
impl Actor for Stash {
type Options = StashOpt;
const NAME: &str = "stash";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if opt.target.is_regular() {
cx.tab_mut().backstack.push(opt.target.as_url());
}
succ!()
}
fn hook(cx: &Ctx, _opt: &Self::Options) -> Option<SparkKind> {
match cx.source() {
Source::Relay => Some(SparkKind::RelayStash),
Source::Ind => Some(SparkKind::IndStash),
_ => None,
}
}
}

View file

@ -49,6 +49,10 @@ impl From<Url> for yazi_shared::url::UrlBufCov {
fn from(value: Url) -> Self { Self(value.inner) }
}
impl From<Url> for yazi_shared::url::UrlCow<'_> {
fn from(value: Url) -> Self { value.inner.into() }
}
impl TryFrom<&[u8]> for Url {
type Error = mlua::Error;

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum SortBy {
#[default]

View file

@ -67,12 +67,10 @@ impl Ember<'static> {
| "trash"
| "delete"
| "mount"
) || kind.starts_with("emit-")
|| kind.starts_with("emit-ind-")
) || kind.starts_with("key-")
|| kind.starts_with("ind-")
|| kind.starts_with("key-")
|| kind.starts_with("emit-")
|| kind.starts_with("relay-")
|| kind.starts_with("relay-ind-")
{
bail!("Cannot construct system event");
}

View file

@ -2,13 +2,17 @@ use std::fmt::Display;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SparkKind {
IndStash,
KeyQuit,
RelayStash,
}
impl AsRef<str> for SparkKind {
fn as_ref(&self) -> &str {
match self {
Self::IndStash => "ind-stash",
Self::KeyQuit => "key-quit",
Self::RelayStash => "relay-stash",
}
}
}

View file

@ -15,6 +15,8 @@ pub enum Spark<'a> {
Close(yazi_parser::mgr::CloseOpt),
Copy(yazi_parser::mgr::CopyOpt),
Create(yazi_parser::mgr::CreateOpt),
Displace(yazi_parser::VoidOpt),
DisplaceDo(yazi_parser::mgr::DisplaceDoOpt),
Download(yazi_parser::mgr::DownloadOpt),
Enter(yazi_parser::VoidOpt),
Escape(yazi_parser::mgr::EscapeOpt),
@ -53,6 +55,7 @@ pub enum Spark<'a> {
Shell(yazi_parser::mgr::ShellOpt),
Sort(yazi_parser::mgr::SortOpt),
Spot(yazi_parser::mgr::SpotOpt),
Stash(yazi_parser::mgr::StashOpt),
Suspend(yazi_parser::VoidOpt),
TabClose(yazi_parser::mgr::TabCloseOpt),
TabCreate(yazi_parser::mgr::TabCreateOpt),
@ -119,7 +122,9 @@ pub enum Spark<'a> {
impl<'a> Spark<'a> {
pub fn from_lua(lua: &Lua, kind: SparkKind, value: Value) -> mlua::Result<Self> {
Ok(match kind {
SparkKind::IndStash => Self::Stash(<_>::from_lua(value, lua)?),
SparkKind::KeyQuit => Self::Quit(<_>::from_lua(value, lua)?),
SparkKind::RelayStash => Self::Stash(<_>::from_lua(value, lua)?),
})
}
}
@ -138,6 +143,8 @@ impl<'a> IntoLua for Spark<'a> {
Self::Close(b) => b.into_lua(lua),
Self::Copy(b) => b.into_lua(lua),
Self::Create(b) => b.into_lua(lua),
Self::Displace(b) => b.into_lua(lua),
Self::DisplaceDo(b) => b.into_lua(lua),
Self::Download(b) => b.into_lua(lua),
Self::Enter(b) => b.into_lua(lua),
Self::Escape(b) => b.into_lua(lua),
@ -176,6 +183,7 @@ impl<'a> IntoLua for Spark<'a> {
Self::Shell(b) => b.into_lua(lua),
Self::Sort(b) => b.into_lua(lua),
Self::Spot(b) => b.into_lua(lua),
Self::Stash(b) => b.into_lua(lua),
Self::Suspend(b) => b.into_lua(lua),
Self::TabClose(b) => b.into_lua(lua),
Self::TabCreate(b) => b.into_lua(lua),
@ -282,6 +290,7 @@ try_from_spark!(mgr::CdOpt, mgr:cd);
try_from_spark!(mgr::CloseOpt, mgr:close);
try_from_spark!(mgr::CopyOpt, mgr:copy);
try_from_spark!(mgr::CreateOpt, mgr:create);
try_from_spark!(mgr::DisplaceDoOpt, mgr:displace_do);
try_from_spark!(mgr::DownloadOpt, mgr:download);
try_from_spark!(mgr::EscapeOpt, mgr:escape);
try_from_spark!(mgr::FilterOpt, mgr:filter, mgr:filter_do);
@ -306,6 +315,7 @@ try_from_spark!(mgr::SeekOpt, mgr:seek);
try_from_spark!(mgr::ShellOpt, mgr:shell);
try_from_spark!(mgr::SortOpt, mgr:sort);
try_from_spark!(mgr::SpotOpt, mgr:spot);
try_from_spark!(mgr::StashOpt, mgr:stash);
try_from_spark!(mgr::TabCloseOpt, mgr:tab_close);
try_from_spark!(mgr::TabCreateOpt, mgr:tab_create);
try_from_spark!(mgr::TabSwitchOpt, mgr:tab_switch);

View file

@ -137,6 +137,7 @@ impl<'a> Executor<'a> {
// VFS
on!(download);
on!(upload);
on!(displace_do);
match cmd.name.as_ref() {
// Help

View file

@ -92,7 +92,7 @@ fn absolute_url_impl<'a>(url: UrlCow<'a>) -> UrlCow<'a> {
if url.has_base() { 0 } else { 2 },
if url.has_trail() { 0 } else { 2 },
)
.expect("Failed to create Loc from drive letter")
.expect("Loc from drive letter")
} else if let Ok(rest) = path.strip_prefix("~/")
&& let Some(home) = dirs::home_dir()
&& home.is_absolute()
@ -103,7 +103,7 @@ fn absolute_url_impl<'a>(url: UrlCow<'a>) -> UrlCow<'a> {
url.uri().components().count() + if url.has_base() { 0 } else { add },
url.urn().components().count() + if url.has_trail() { 0 } else { add },
)
.expect("Failed to create Loc from home directory")
.expect("Loc from home directory")
} else if !url.is_absolute() {
let cwd = CWD.path();
LocBuf::<PathBuf>::with(
@ -111,7 +111,7 @@ fn absolute_url_impl<'a>(url: UrlCow<'a>) -> UrlCow<'a> {
url.uri().components().count(),
url.urn().components().count(),
)
.expect("Failed to create Loc from relative path")
.expect("Loc from relative path")
} else {
return url;
};

View file

@ -1,4 +1,5 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use serde::{Deserialize, Serialize};
use yazi_fs::path::expand_url;
use yazi_shared::{event::CmdCow, url::{Url, UrlBuf, UrlCow}};
@ -46,7 +47,8 @@ impl IntoLua for CdOpt {
}
// --- Source
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum CdSource {
Tab,
Cd,
@ -55,9 +57,6 @@ pub enum CdSource {
Leave,
Forward,
Back,
}
impl CdSource {
#[inline]
pub fn big_jump(self) -> bool { self == Self::Cd || self == Self::Reveal }
Escape,
Displace,
}

View file

@ -0,0 +1,29 @@
use anyhow::bail;
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{event::CmdCow, url::UrlBuf};
#[derive(Debug)]
pub struct DisplaceDoOpt {
pub to: UrlBuf,
pub from: UrlBuf,
}
impl TryFrom<CmdCow> for DisplaceDoOpt {
type Error = anyhow::Error;
fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {
if let Some(opt) = c.take_any2("opt") {
opt
} else {
bail!("'opt' is required for DisplaceDoOpt");
}
}
}
impl FromLua for DisplaceDoOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for DisplaceDoOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -3,6 +3,7 @@ yazi_macro::mod_flat!(
close
copy
create
displace_do
download
escape
filter
@ -27,6 +28,7 @@ yazi_macro::mod_flat!(
shell
sort
spot
stash
tab_close
tab_create
tab_switch

View file

@ -0,0 +1,44 @@
use anyhow::bail;
use mlua::{ExternalError, FromLua, IntoLua, Lua, LuaSerdeExt, Value};
use serde::{Deserialize, Serialize};
use yazi_binding::Url;
use yazi_shared::{event::CmdCow, url::UrlCow};
use crate::mgr::{CdOpt, CdSource};
#[derive(Debug, Deserialize, Serialize)]
pub struct StashOpt {
pub target: UrlCow<'static>,
pub source: CdSource,
}
impl TryFrom<CmdCow> for StashOpt {
type Error = anyhow::Error;
fn try_from(_: CmdCow) -> Result<Self, Self::Error> { bail!("unsupported") }
}
impl From<CdOpt> for StashOpt {
fn from(opt: CdOpt) -> Self { Self { target: opt.target, source: opt.source } }
}
impl FromLua for StashOpt {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let tbl = value.as_table().ok_or_else(|| "expected table".into_lua_err())?;
Ok(Self {
target: tbl.get::<Url>("target")?.into(),
source: lua.from_value(tbl.get("source")?)?,
})
}
}
impl IntoLua for StashOpt {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
lua
.create_table_from([
("target", Url::new(self.target).into_lua(lua)?),
("source", lua.to_value(&self.source)?),
])?
.into_lua(lua)
}
}

View file

@ -1,4 +1,4 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use mlua::{FromLua, IntoLua, Lua, Value};
use yazi_shared::event::CmdCow;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@ -13,9 +13,9 @@ impl From<()> for VoidOpt {
}
impl FromLua for VoidOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Ok(Self) }
}
impl IntoLua for VoidOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> { lua.create_table()?.into_lua(lua) }
}

View file

@ -44,7 +44,7 @@ function M:fetch(job)
if file.cache then
urls[i], paths[i] = tostring(file.url), tostring(file.cache)
else
paths[i] = tostring(file.url)
paths[i] = tostring(file.url.path)
end
end

View file

@ -1,22 +1,30 @@
use std::borrow::Cow;
use yazi_macro::{emit, relay};
use yazi_parser::mgr::{FilterOpt, FindDoOpt, OpenDoOpt, OpenOpt, SearchOpt, UpdatePeekedOpt, UpdateSpottedOpt};
use yazi_shared::{SStr, url::UrlBuf};
use yazi_parser::mgr::{DisplaceDoOpt, FilterOpt, FindDoOpt, OpenDoOpt, OpenOpt, SearchOpt, UpdatePeekedOpt, UpdateSpottedOpt};
use yazi_shared::{Id, SStr, url::UrlBuf};
pub struct MgrProxy;
impl MgrProxy {
pub fn arrow(step: impl Into<SStr>) {
emit!(Call(relay!(mgr:arrow, [step.into()])));
}
pub fn cd(target: &UrlBuf) {
emit!(Call(relay!(mgr:cd, [target]).with("raw", true)));
}
pub fn reveal(target: &UrlBuf) {
emit!(Call(relay!(mgr:reveal, [target]).with("raw", true).with("no-dummy", true)));
pub fn displace_do(tab: Id, opt: DisplaceDoOpt) {
emit!(Call(relay!(mgr:displace_do).with("tab", tab).with_any("opt", opt)));
}
pub fn arrow(step: impl Into<SStr>) {
emit!(Call(relay!(mgr:arrow, [step.into()])));
pub fn filter_do(opt: FilterOpt) {
emit!(Call(relay!(mgr:filter_do).with_any("opt", opt)));
}
pub fn find_do(opt: FindDoOpt) {
emit!(Call(relay!(mgr:find_do).with_any("opt", opt)));
}
pub fn open(opt: OpenOpt) {
@ -33,12 +41,8 @@ impl MgrProxy {
));
}
pub fn find_do(opt: FindDoOpt) {
emit!(Call(relay!(mgr:find_do).with_any("opt", opt)));
}
pub fn filter_do(opt: FilterOpt) {
emit!(Call(relay!(mgr:filter_do).with_any("opt", opt)));
pub fn reveal(target: &UrlBuf) {
emit!(Call(relay!(mgr:reveal, [target]).with("raw", true).with("no-dummy", true)));
}
pub fn search_do(opt: SearchOpt) {
@ -50,8 +54,8 @@ impl MgrProxy {
));
}
pub fn watch() {
emit!(Call(relay!(mgr:watch)));
pub fn update_paged_by(page: usize, only_if: &UrlBuf) {
emit!(Call(relay!(mgr:update_paged, [page]).with("only-if", only_if)));
}
pub fn update_peeked(opt: UpdatePeekedOpt) {
@ -62,14 +66,14 @@ impl MgrProxy {
emit!(Call(relay!(mgr:update_spotted).with_any("opt", opt)));
}
pub fn update_paged_by(page: usize, only_if: &UrlBuf) {
emit!(Call(relay!(mgr:update_paged, [page]).with("only-if", only_if)));
}
pub fn upload<I>(urls: I)
where
I: IntoIterator<Item = UrlBuf>,
{
emit!(Call(relay!(mgr:upload).with_seq(urls)));
}
pub fn watch() {
emit!(Call(relay!(mgr:watch)));
}
}

View file

@ -28,40 +28,6 @@ pub enum Data {
Any(Box<dyn Any + Send + Sync>),
}
impl Data {
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
pub fn into_string(self) -> Option<SStr> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
pub fn into_any<T: 'static>(self) -> Option<T> {
match self {
Self::Any(b) => b.downcast::<T>().ok().map(|b| *b),
_ => None,
}
}
// FIXME: find a better name
pub fn into_any2<T: 'static>(self) -> Result<T> {
if let Self::Any(b) = self
&& let Ok(t) = b.downcast::<T>()
{
Ok(*t)
} else {
bail!("Failed to downcast Data into {}", std::any::type_name::<T>())
}
}
}
impl From<()> for Data {
fn from(_: ()) -> Self { Self::Nil }
}
@ -201,13 +167,291 @@ impl<'a> TryFrom<&'a Data> for UrlCow<'a> {
}
}
impl<'a> TryFrom<&'a Data> for &'a [u8] {
type Error = anyhow::Error;
fn try_from(value: &'a Data) -> Result<Self, Self::Error> {
match value {
Data::Bytes(b) => Ok(b),
_ => bail!("not bytes"),
}
}
}
impl PartialEq<bool> for Data {
fn eq(&self, other: &bool) -> bool { self.try_into().is_ok_and(|b| *other == b) }
}
impl Data {
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
pub fn into_string(self) -> Option<SStr> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
pub fn into_any<T: 'static>(self) -> Option<T> {
match self {
Self::Any(b) => b.downcast::<T>().ok().map(|b| *b),
_ => None,
}
}
// FIXME: find a better name
pub fn into_any2<T: 'static>(self) -> Result<T> {
if let Self::Any(b) = self
&& let Ok(t) = b.downcast::<T>()
{
Ok(*t)
} else {
bail!("Failed to downcast Data into {}", std::any::type_name::<T>())
}
}
}
impl<'de> serde::Deserializer<'de> for &Data {
type Error = serde::de::value::Error;
fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("any not supported"))
}
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_bool(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_i8(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_i16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_i16(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_i32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_i32(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_i64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_i64(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_u8(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_u16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_u16(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_u32(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_u64(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_f32(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_f64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_f64(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_char<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("not a char"))
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_str(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_string<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("string not supported"))
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_bytes(self.try_into().map_err(serde::de::Error::custom)?)
}
fn deserialize_byte_buf<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("byte buf not supported"))
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
match self {
Data::Nil => visitor.visit_none(),
_ => visitor.visit_some(self),
}
}
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_unit_struct<V>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_newtype_struct<V>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_newtype_struct(self)
}
fn deserialize_seq<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("seq not supported"))
}
fn deserialize_tuple<V>(self, _len: usize, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("tuple not supported"))
}
fn deserialize_tuple_struct<V>(
self,
_name: &'static str,
_len: usize,
_visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("tuple struct not supported"))
}
fn deserialize_map<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("map not supported"))
}
fn deserialize_struct<V>(
self,
_name: &'static str,
_fields: &'static [&'static str],
_visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("struct not supported"))
}
fn deserialize_enum<V>(
self,
_name: &'static str,
_variants: &'static [&'static str],
_visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("enum not supported"))
}
fn deserialize_identifier<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("identifier not supported"))
}
fn deserialize_ignored_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(serde::de::Error::custom("ignored any not supported"))
}
}
// --- Macros
macro_rules! impl_into_integer {
($t:ty, $name:ident) => {
($t:ty) => {
impl TryFrom<&Data> for $t {
type Error = anyhow::Error;
@ -224,14 +468,14 @@ macro_rules! impl_into_integer {
}
macro_rules! impl_into_number {
($t:ty, $name:ident) => {
($t:ty) => {
impl TryFrom<&Data> for $t {
type Error = anyhow::Error;
fn try_from(value: &Data) -> Result<Self, Self::Error> {
Ok(match value {
Data::Integer(i) if *i == (*i as $t as _) => *i as $t,
Data::Number(n) => <$t>::try_from(*n)?,
Data::Number(n) if *n == (*n as $t as _) => *n as $t,
Data::String(s) => s.parse()?,
Data::Id(i) if i.0 == (i.0 as $t as _) => i.0 as $t,
_ => bail!("not a number"),
@ -241,10 +485,17 @@ macro_rules! impl_into_number {
};
}
impl_into_integer!(usize, as_usize);
impl_into_integer!(isize, as_isize);
impl_into_integer!(i16, as_i16);
impl_into_integer!(i32, as_i32);
impl_into_integer!(crate::Id, as_id);
impl_into_integer!(i8);
impl_into_integer!(i16);
impl_into_integer!(i32);
impl_into_integer!(i64);
impl_into_integer!(isize);
impl_into_integer!(u8);
impl_into_integer!(u16);
impl_into_integer!(u32);
impl_into_integer!(u64);
impl_into_integer!(usize);
impl_into_integer!(crate::Id);
impl_into_number!(f64, as_f64);
impl_into_number!(f32);
impl_into_number!(f64);

View file

@ -152,6 +152,14 @@ impl Cmd {
self.take(0)
}
pub fn take_second<T>(&mut self) -> Result<T>
where
T: TryFrom<Data>,
T::Error: Into<anyhow::Error>,
{
self.take(1)
}
pub fn take_seq<T>(&mut self) -> Vec<T>
where
T: TryFrom<Data>,

View file

@ -55,6 +55,18 @@ impl CmdCow {
}
}
pub fn take_second<'a, T>(&mut self) -> Result<T>
where
T: TryFrom<Data> + TryFrom<&'a Data>,
<T as TryFrom<Data>>::Error: Into<anyhow::Error>,
<T as TryFrom<&'a Data>>::Error: Into<anyhow::Error>,
{
match self {
Self::Owned(c) => c.take_second(),
Self::Borrowed(c) => c.second(),
}
}
pub fn take_seq<'a, T>(&mut self) -> Vec<T>
where
T: TryFrom<Data> + TryFrom<&'a Data>,

View file

@ -72,6 +72,8 @@ pub(super) trait LocAbleImpl<'p>: LocAble<'p> {
fn len(self) -> usize { self.as_encoded_bytes().len() }
fn parent(self) -> Option<Self>;
fn strip_prefix<'a, T>(self, base: T) -> Option<Self>
where
T: AsStrandView<'a, Self::Strand<'a>>;
@ -99,6 +101,8 @@ impl<'p> LocAbleImpl<'p> for &'p std::path::Path {
self.join(path.as_strand_view())
}
fn parent(self) -> Option<Self> { self.parent() }
fn strip_prefix<'a, T>(self, base: T) -> Option<Self>
where
T: AsStrandView<'a, Self::Strand<'a>>,
@ -129,6 +133,8 @@ impl<'p> LocAbleImpl<'p> for &'p typed_path::UnixPath {
self.join(path.as_strand_view())
}
fn parent(self) -> Option<Self> { self.parent() }
fn strip_prefix<'a, T>(self, base: T) -> Option<Self>
where
T: AsStrandView<'a, Self::Strand<'a>>,

View file

@ -182,7 +182,7 @@ where
let old = self.inner.len();
self.mutate(|path| path.set_file_name(name));
let new = self.len();
let new = self.inner.len();
if new == old {
return Ok(());
}
@ -215,6 +215,9 @@ where
loc
}
#[inline]
pub fn parent(&self) -> Option<P::Borrowed<'_>> { self.as_loc().parent() }
#[inline]
fn mutate<T, F: FnOnce(&mut P) -> T>(&mut self, f: F) -> T {
let mut inner = mem::take(&mut self.inner);

View file

@ -73,6 +73,64 @@ impl<'p, P> Loc<'p, P>
where
P: LocAble<'p> + LocAbleImpl<'p>,
{
#[inline]
pub fn as_inner(self) -> P { self.inner }
#[inline]
pub fn as_loc(self) -> Self { self }
pub fn bare<T>(path: T) -> Self
where
T: AsPathView<'p, P>,
{
let path = path.as_path_view();
let Some(name) = path.file_name() else {
let uri = path.strip_prefix(P::empty()).unwrap().len();
return Self { inner: path, uri, urn: 0, _phantom: PhantomData };
};
let name_len = name.len();
let prefix_len = unsafe {
name.as_encoded_bytes().as_ptr().offset_from_unsigned(path.as_encoded_bytes().as_ptr())
};
let bytes = &path.as_encoded_bytes()[..prefix_len + name_len];
Self {
inner: unsafe { P::from_encoded_bytes_unchecked(bytes) },
uri: bytes.len(),
urn: name_len,
_phantom: PhantomData,
}
}
#[inline]
pub fn base(self) -> P {
unsafe {
P::from_encoded_bytes_unchecked(
self.inner.as_encoded_bytes().get_unchecked(..self.inner.len() - self.uri),
)
}
}
pub fn floated<'a, T, S>(path: T, base: S) -> Self
where
T: AsPathView<'p, P>,
S: AsStrandView<'a, P::Strand<'a>>,
{
let mut loc = Self::bare(path);
loc.uri = loc.inner.strip_prefix(base).expect("Loc must start with the given base").len();
loc
}
#[inline]
pub fn has_base(self) -> bool { self.inner.len() != self.uri }
#[inline]
pub fn has_trail(self) -> bool { self.inner.len() != self.urn }
#[inline]
pub fn is_empty(self) -> bool { self.inner.len() == 0 }
pub fn new<'a, T, S>(path: T, base: S, trail: S) -> Self
where
T: AsPathView<'p, P>,
@ -84,6 +142,67 @@ where
loc
}
#[inline]
pub fn parent(self) -> Option<P> {
self.inner.parent().filter(|p| !p.as_encoded_bytes().is_empty())
}
pub fn saturated<'a, T>(path: T, kind: SchemeKind) -> Self
where
T: AsPathView<'p, P>,
{
match kind {
SchemeKind::Regular => Self::bare(path),
SchemeKind::Search => Self::zeroed(path),
SchemeKind::Archive => Self::zeroed(path),
SchemeKind::Sftp => Self::bare(path),
}
}
#[inline]
pub fn trail(self) -> P {
unsafe {
P::from_encoded_bytes_unchecked(
self.inner.as_encoded_bytes().get_unchecked(..self.inner.len() - self.urn),
)
}
}
#[inline]
pub fn triple(self) -> (P, P, P) {
let len = self.inner.len();
let base = ..len - self.uri;
let rest = len - self.uri..len - self.urn;
let urn = len - self.urn..;
unsafe {
(
P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(base)),
P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(rest)),
P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(urn)),
)
}
}
#[inline]
pub fn uri(self) -> P {
unsafe {
P::from_encoded_bytes_unchecked(
self.inner.as_encoded_bytes().get_unchecked(self.inner.len() - self.uri..),
)
}
}
#[inline]
pub fn urn(self) -> P {
unsafe {
P::from_encoded_bytes_unchecked(
self.inner.as_encoded_bytes().get_unchecked(self.inner.len() - self.urn..),
)
}
}
pub fn with<T>(path: T, uri: usize, urn: usize) -> Result<Self>
where
T: AsPathView<'p, P>,
@ -116,30 +235,6 @@ where
Ok(loc)
}
pub fn bare<T>(path: T) -> Self
where
T: AsPathView<'p, P>,
{
let path = path.as_path_view();
let Some(name) = path.file_name() else {
let uri = path.strip_prefix(P::empty()).unwrap().len();
return Self { inner: path, uri, urn: 0, _phantom: PhantomData };
};
let name_len = name.len();
let prefix_len = unsafe {
name.as_encoded_bytes().as_ptr().offset_from_unsigned(path.as_encoded_bytes().as_ptr())
};
let bytes = &path.as_encoded_bytes()[..prefix_len + name_len];
Self {
inner: unsafe { P::from_encoded_bytes_unchecked(bytes) },
uri: bytes.len(),
urn: name_len,
_phantom: PhantomData,
}
}
pub fn zeroed<T>(path: T) -> Self
where
T: AsPathView<'p, P>,
@ -148,94 +243,4 @@ where
(loc.uri, loc.urn) = (0, 0);
loc
}
pub fn floated<'a, T, S>(path: T, base: S) -> Self
where
T: AsPathView<'p, P>,
S: AsStrandView<'a, P::Strand<'a>>,
{
let mut loc = Self::bare(path);
loc.uri = loc.inner.strip_prefix(base).expect("Loc must start with the given base").len();
loc
}
pub fn saturated<'a, T>(path: T, kind: SchemeKind) -> Self
where
T: AsPathView<'p, P>,
{
match kind {
SchemeKind::Regular => Self::bare(path),
SchemeKind::Search => Self::zeroed(path),
SchemeKind::Archive => Self::zeroed(path),
SchemeKind::Sftp => Self::bare(path),
}
}
#[inline]
pub fn as_loc(self) -> Self { self }
#[inline]
pub fn as_inner(self) -> P { self.inner }
#[inline]
pub fn is_empty(self) -> bool { self.inner.len() == 0 }
#[inline]
pub fn uri(self) -> P {
unsafe {
P::from_encoded_bytes_unchecked(
self.inner.as_encoded_bytes().get_unchecked(self.inner.len() - self.uri..),
)
}
}
#[inline]
pub fn urn(self) -> P {
unsafe {
P::from_encoded_bytes_unchecked(
self.inner.as_encoded_bytes().get_unchecked(self.inner.len() - self.urn..),
)
}
}
#[inline]
pub fn base(self) -> P {
unsafe {
P::from_encoded_bytes_unchecked(
self.inner.as_encoded_bytes().get_unchecked(..self.inner.len() - self.uri),
)
}
}
#[inline]
pub fn has_base(self) -> bool { self.inner.len() != self.uri }
#[inline]
pub fn trail(self) -> P {
unsafe {
P::from_encoded_bytes_unchecked(
self.inner.as_encoded_bytes().get_unchecked(..self.inner.len() - self.urn),
)
}
}
#[inline]
pub fn has_trail(self) -> bool { self.inner.len() != self.urn }
#[inline]
pub fn triple(self) -> (P, P, P) {
let len = self.inner.len();
let base = ..len - self.uri;
let rest = len - self.uri..len - self.urn;
let urn = len - self.urn..;
unsafe {
(
P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(base)),
P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(rest)),
P::from_encoded_bytes_unchecked(self.inner.as_encoded_bytes().get_unchecked(urn)),
)
}
}
}

View file

@ -5,6 +5,7 @@ use anyhow::Result;
use crate::path::{AsPath, PathBufDyn, PathDyn, PathKind};
// --- PathCow
#[derive(Debug)]
pub enum PathCow<'a> {
Borrowed(PathDyn<'a>),
Owned(PathBufDyn),

View file

@ -139,8 +139,8 @@ impl<'p> PathDyn<'p> {
pub fn parent(self) -> Option<Self> {
Some(match self {
Self::Os(p) => Self::Os(p.parent()?),
Self::Unix(p) => Self::Unix(p.parent()?),
Self::Os(p) => Self::Os(p.parent().filter(|p| !p.as_os_str().is_empty())?),
Self::Unix(p) => Self::Unix(p.parent().filter(|p| !p.as_bytes().is_empty())?),
})
}

View file

@ -6,16 +6,16 @@ pub enum Source {
Unknown,
Key,
Ind,
Emit,
EmitInd,
Relay,
RelayInd,
Ind,
}
impl Source {
#[inline]
pub fn is_key(self) -> bool { self == Self::Key }
#[inline]
pub fn is_ind(self) -> bool { self == Self::Ind }
}

View file

@ -96,6 +96,10 @@ impl PartialEq for UrlBuf {
fn eq(&self, other: &Self) -> bool { self.as_url() == other.as_url() }
}
impl PartialEq<UrlBuf> for &UrlBuf {
fn eq(&self, other: &UrlBuf) -> bool { self.as_url() == other.as_url() }
}
impl PartialEq<Url<'_>> for UrlBuf {
fn eq(&self, other: &Url) -> bool { self.as_url() == *other }
}
@ -104,6 +108,14 @@ impl PartialEq<Url<'_>> for &UrlBuf {
fn eq(&self, other: &Url) -> bool { self.as_url() == *other }
}
impl PartialEq<UrlCow<'_>> for UrlBuf {
fn eq(&self, other: &UrlCow) -> bool { self.as_url() == other.as_url() }
}
impl PartialEq<UrlCow<'_>> for &UrlBuf {
fn eq(&self, other: &UrlCow) -> bool { self.as_url() == other.as_url() }
}
// --- Hash
impl Hash for UrlBuf {
fn hash<H: Hasher>(&self, state: &mut H) { self.as_url().hash(state) }
@ -293,7 +305,7 @@ mod tests {
("sftp://remote//", None),
// Relative
("search://kw:2:2/a/b", Some("search://kw:1:1/a")),
("search://kw:1:1/a", Some("search://kw/")),
("search://kw:1:1/a", None),
("search://kw/", None),
];