refactor: move Term to its own yazi-term crate (#3629)

This commit is contained in:
三咲雅 misaki masa 2026-01-28 22:26:47 +08:00 committed by GitHub
parent 24c60419bb
commit 583345296f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 197 additions and 124 deletions

10
Cargo.lock generated
View file

@ -5658,6 +5658,7 @@ dependencies = [
"yazi-fs",
"yazi-macro",
"yazi-shared",
"yazi-shim",
"yazi-term",
]
@ -5809,6 +5810,7 @@ dependencies = [
"yazi-plugin",
"yazi-proxy",
"yazi-shared",
"yazi-shim",
"yazi-term",
"yazi-tty",
"yazi-vfs",
@ -6004,6 +6006,7 @@ dependencies = [
name = "yazi-shim"
version = "26.1.22"
dependencies = [
"crossterm 0.29.0",
"twox-hash",
"yazi-macro",
]
@ -6012,9 +6015,16 @@ dependencies = [
name = "yazi-term"
version = "26.1.22"
dependencies = [
"anyhow",
"crossterm 0.29.0",
"libc",
"ratatui",
"yazi-config",
"yazi-emulator",
"yazi-macro",
"yazi-shared",
"yazi-shim",
"yazi-tty",
]
[[package]]

View file

@ -8,13 +8,17 @@ function bugReportBody(creator, content, hash) {
return null
}
return `Hey @${creator}, I noticed that you did not correctly follow the issue template. Please ensure that:
return `Hey @${creator}, thank you for opening this issue to help us improve Yazi, appreciate it!
I noticed that you did not correctly follow the issue template. Please ensure that:
- The bug can still be reproduced on the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries).
- The debug information (\`yazi --debug\`) is updated for the newest nightly.
- The non-optional items in the checklist are checked.
Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity.
Our maintainers work on Yazi in a personal capacity, and debug info helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thank you for the understanding!
`
}
@ -23,13 +27,17 @@ function featureRequestBody(creator, content) {
return null
}
return `Hey @${creator}, I noticed that you did not correctly follow the issue template. Please ensure that:
return `Hey @${creator}, thank you for opening this issue to help us improve Yazi, appreciate it!
I noticed that you did not correctly follow the issue template. Please ensure that:
- The requested feature does not exist in the [newest nightly build](https://yazi-rs.github.io/docs/installation/#binaries).
- The debug information (\`yazi --debug\`) is updated for the newest nightly.
- The non-optional items in the checklist are checked.
Issues with \`${LABEL_NAME}\` will be marked ready once edited with the proper content, or closed after 2 days of inactivity.
Our maintainers work on Yazi in a personal capacity, and debug info helps them work efficiently, understand your setup quickly, and find a more appropriate solution. Thank you for the understanding!
`
}
@ -66,7 +74,7 @@ module.exports = async ({ github, context, core }) => {
const { data: events } = await github.rest.issues.listEvents({
...context.repo,
issue_number: id,
per_page : 100,
per_page: 100,
})
const all = events.filter(v => v.event === "labeled" && v.label?.name === LABEL_NAME)
@ -82,7 +90,7 @@ module.exports = async ({ github, context, core }) => {
const { data: events } = await github.rest.issues.listEvents({
...context.repo,
issue_number: id,
per_page : 100,
per_page: 100,
})
const all = events.filter(v => v.event === "unlabeled" && v.label?.name === LABEL_NAME)
@ -101,14 +109,14 @@ module.exports = async ({ github, context, core }) => {
await github.rest.issues.removeLabel({
...context.repo,
issue_number: id,
name : LABEL_NAME,
name: LABEL_NAME,
})
await hideOldComments(id)
} else if (mark && !marked && !await removedLabelManually(id)) {
await github.rest.issues.addLabels({
...context.repo,
issue_number: id,
labels : [LABEL_NAME],
labels: [LABEL_NAME],
})
await hideOldComments(id)
await github.rest.issues.createComment({
@ -127,7 +135,7 @@ module.exports = async ({ github, context, core }) => {
const comments = await github.paginate(github.rest.issues.listComments, {
...context.repo,
issue_number: id,
per_page : 100,
per_page: 100,
})
for (const c of comments) {
@ -157,7 +165,7 @@ module.exports = async ({ github, context, core }) => {
try {
const { data: issues } = await github.rest.issues.listForRepo({
...context.repo,
state : "open",
state: "open",
labels: LABEL_NAME,
})
@ -170,13 +178,13 @@ module.exports = async ({ github, context, core }) => {
await github.rest.issues.update({
...context.repo,
issue_number: issue.number,
state : "closed",
state: "closed",
state_reason: "not_planned",
})
await github.rest.issues.createComment({
...context.repo,
issue_number: issue.number,
body : `This issue has been automatically closed because it was marked as \`${LABEL_NAME}\` for more than 2 days without updates.
body: `This issue has been automatically closed because it was marked as \`${LABEL_NAME}\` for more than 2 days without updates.
If the problem persists, please file a new issue and complete the issue template so we can capture all the details necessary to investigate further.`,
})
}
@ -191,13 +199,13 @@ If the problem persists, please file a new issue and complete the issue template
await github.rest.issues.update({
...context.repo,
issue_number: id,
state : "closed",
state: "closed",
state_reason: "not_planned",
})
await github.rest.issues.createComment({
...context.repo,
issue_number: id,
body : `Unsupported issue template.
body: `Unsupported issue template.
Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) or [Feature Request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml) template should be used.`,
})
} catch (e) {

View file

@ -32,7 +32,5 @@ paste = { workspace = true }
ratatui = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
unicode-width = { workspace = true }
# Logging
tracing = { workspace = true }

View file

@ -62,6 +62,7 @@ impl Actions {
writeln!(s, " YAZI_FILE_ONE : {:?}", env::var_os("YAZI_FILE_ONE"))?;
writeln!(s, " YAZI_CONFIG_HOME : {:?}", env::var_os("YAZI_CONFIG_HOME"))?;
writeln!(s, " YAZI_ZOXIDE_OPTS : {:?}", env::var_os("YAZI_ZOXIDE_OPTS"))?;
writeln!(s, " SSH_AUTH_SOCK : {:?}", env::var_os("SSH_AUTH_SOCK"))?;
writeln!(s, " FZF_DEFAULT_OPTS : {:?}", env::var_os("FZF_DEFAULT_OPTS"))?;
writeln!(s, " FZF_DEFAULT_COMMAND: {:?}", env::var_os("FZF_DEFAULT_COMMAND"))?;

View file

@ -27,6 +27,7 @@ yazi-dds = { path = "../yazi-dds", version = "26.1.22" }
yazi-fs = { path = "../yazi-fs", version = "26.1.22" }
yazi-macro = { path = "../yazi-macro", version = "26.1.22" }
yazi-shared = { path = "../yazi-shared", version = "26.1.22" }
yazi-shim = { path = "../yazi-shim", version = "26.1.22" }
yazi-term = { path = "../yazi-term", version = "26.1.22" }
# External dependencies

View file

@ -49,7 +49,7 @@ impl Dependency {
use std::io::IsTerminal;
use crossterm::style::{Attribute, Print, SetAttributes};
use yazi_term::If;
use yazi_shim::crossterm::If;
let ansi = env::var_os("YA_FORCE_ANSI").is_some_and(|v| v == "1") || io::stdout().is_terminal();
crossterm::execute!(

View file

@ -40,6 +40,7 @@ yazi-parser = { path = "../yazi-parser", version = "26.1.22" }
yazi-plugin = { path = "../yazi-plugin", version = "26.1.22" }
yazi-proxy = { path = "../yazi-proxy", version = "26.1.22" }
yazi-shared = { path = "../yazi-shared", version = "26.1.22" }
yazi-shim = { path = "../yazi-shim", version = "26.1.22" }
yazi-term = { path = "../yazi-term", version = "26.1.22" }
yazi-tty = { path = "../yazi-tty", version = "26.1.22" }
yazi-vfs = { path = "../yazi-vfs", version = "26.1.22" }
@ -47,19 +48,17 @@ yazi-watcher = { path = "../yazi-watcher", version = "26.1.22" }
yazi-widgets = { path = "../yazi-widgets", version = "26.1.22" }
# External dependencies
anyhow = { workspace = true }
better-panic = "0.3.0"
crossterm = { workspace = true }
fdlimit = "0.3.0"
futures = { workspace = true }
mlua = { workspace = true }
paste = { workspace = true }
ratatui = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
# Logging
anyhow = { workspace = true }
better-panic = "0.3.0"
crossterm = { workspace = true }
fdlimit = "0.3.0"
futures = { workspace = true }
mlua = { workspace = true }
paste = { workspace = true }
ratatui = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
tracing-appender = "0.2.4"
tracing-subscriber = { version = "0.3.22", features = [ "env-filter" ] }

View file

@ -5,8 +5,9 @@ use tokio::{select, time::sleep};
use yazi_core::Core;
use yazi_macro::act;
use yazi_shared::event::{Event, NEED_RENDER};
use yazi_term::Term;
use crate::{Dispatcher, Signals, Term};
use crate::{Dispatcher, Signals};
pub(crate) struct App {
pub(crate) core: Core,

View file

@ -1,8 +1,9 @@
use yazi_boot::ARGS;
use yazi_fs::provider::{Provider, local::Local};
use yazi_shared::{event::EventQuit, strand::{StrandBuf, StrandLike, ToStrand}};
use yazi_term::Term;
use crate::{Term, app::App};
use crate::app::App;
impl App {
pub(crate) fn quit(&mut self, opt: EventQuit) -> ! {

View file

@ -2,8 +2,9 @@ use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::app::ResumeOpt;
use yazi_shared::data::Data;
use yazi_term::Term;
use crate::{Term, app::App};
use crate::app::App;
impl App {
pub(crate) fn resume(&mut self, opt: ResumeOpt) -> Result<Data> {

View file

@ -4,7 +4,7 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
yazi_macro::mod_pub!(app cmp confirm help input mgr notify pick spot tasks which);
yazi_macro::mod_flat!(dispatcher executor logs panic root router signals term);
yazi_macro::mod_flat!(dispatcher executor logs panic root router signals);
#[tokio::main]
async fn main() -> anyhow::Result<()> {

View file

@ -1,4 +1,4 @@
use crate::Term;
use yazi_term::Term;
pub(super) struct Panic;

View file

@ -35,3 +35,6 @@ ordered-float = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
tokio = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -15,4 +15,8 @@ workspace = true
yazi-macro = { path = "../yazi-macro", version = "26.1.22" }
# External dependencies
crossterm = { workspace = true }
twox-hash = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

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

View file

@ -0,0 +1,10 @@
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,34 +1,18 @@
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use crossterm::cursor::SetCursorStyle;
static SHAPE: AtomicU8 = AtomicU8::new(0);
static BLINK: AtomicBool = AtomicBool::new(false);
pub struct RestoreCursor;
impl RestoreCursor {
pub fn store(resp: &str) {
SHAPE.store(
resp
.split_once("\x1bP1$r")
.and_then(|(_, s)| s.bytes().next())
.filter(|&b| matches!(b, b'0'..=b'6'))
.map_or(u8::MAX, |b| b - b'0'),
Ordering::Relaxed,
);
BLINK.store(resp.contains("\x1b[?12;1$y"), Ordering::Relaxed);
}
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 SHAPE.load(Ordering::Relaxed) {
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(BLINK.load(Ordering::Relaxed));
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)?,

View file

@ -0,0 +1,10 @@
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 +1,3 @@
yazi_macro::mod_pub!(crossterm);
yazi_macro::mod_flat!(twox);

View file

@ -12,10 +12,17 @@ repository = "https://github.com/sxyazi/yazi"
workspace = true
[dependencies]
yazi-macro = { path = "../yazi-macro", version = "26.1.22" }
yazi-config = { path = "../yazi-config", version = "26.1.22" }
yazi-emulator = { path = "../yazi-emulator", version = "26.1.22" }
yazi-macro = { path = "../yazi-macro", version = "26.1.22" }
yazi-shared = { path = "../yazi-shared", version = "26.1.22" }
yazi-shim = { path = "../yazi-shim", version = "26.1.22" }
yazi-tty = { path = "../yazi-tty", version = "26.1.22" }
# External dependencies
crossterm = { workspace = true }
anyhow = { workspace = true }
crossterm = { workspace = true }
ratatui = { workspace = true }
[target."cfg(unix)".dependencies]
libc = { workspace = true }

View file

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

View file

@ -1 +1 @@
yazi_macro::mod_flat!(background cursor r#if);
yazi_macro::mod_flat!(option state term);

17
yazi-term/src/option.rs Normal file
View file

@ -0,0 +1,17 @@
use yazi_config::{THEME, YAZI};
pub(super) struct TermOption {
pub bg: String,
pub mouse: bool,
pub title: Option<String>,
}
impl Default for TermOption {
fn default() -> Self {
Self {
bg: THEME.app.bg_color(),
mouse: !YAZI.mgr.mouse_events.get().is_empty(),
title: YAZI.mgr.title(),
}
}
}

45
yazi-term/src/state.rs Normal file
View file

@ -0,0 +1,45 @@
use crate::TermOption;
#[derive(Clone, Copy)]
pub(super) struct TermState {
pub(super) bg: bool,
pub(super) csi_u: bool,
pub(super) mouse: bool,
pub(super) title: bool,
pub(super) cursor_shape: u8,
pub(super) cursor_blink: bool,
}
impl TermState {
pub(super) const fn default() -> Self {
Self {
bg: false,
csi_u: false,
mouse: false,
title: false,
cursor_shape: 0,
cursor_blink: false,
}
}
pub(super) fn new(resp: &str, opt: &TermOption) -> Self {
let csi_u = resp.contains("\x1b[?0u");
let cursor_shape = resp
.split_once("\x1bP1$r")
.and_then(|(_, s)| s.bytes().next())
.filter(|&b| matches!(b, b'0'..=b'6'))
.map_or(u8::MAX, |b| b - b'0');
let cursor_blink = resp.contains("\x1b[?12;1$y");
Self {
bg: !opt.bg.is_empty(),
csi_u,
mouse: opt.mouse,
title: opt.title.is_some(),
cursor_shape,
cursor_blink,
}
}
}

View file

@ -1,23 +1,26 @@
use std::{io, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, Ordering}};
use std::{io, ops::Deref};
use anyhow::Result;
use crossterm::{event::{DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode}};
use ratatui::{CompletedFrame, Frame, Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect};
use yazi_config::{THEME, YAZI};
use yazi_emulator::{Emulator, Mux, TMUX};
use yazi_shared::SyncCell;
use yazi_shim::crossterm::{If, RestoreBackground, RestoreCursor, SetBackground};
use yazi_tty::{TTY, TtyWriter};
static CSI_U: AtomicBool = AtomicBool::new(false);
use crate::{TermOption, TermState};
pub(super) struct Term {
static STATE: SyncCell<TermState> = SyncCell::new(TermState::default());
pub struct Term {
inner: Terminal<CrosstermBackend<TtyWriter<'static>>>,
last_area: Rect,
last_buffer: Buffer,
}
impl Term {
pub(super) fn start() -> Result<Self> {
pub fn start() -> Result<Self> {
let opt = TermOption::default();
let mut term = Self {
inner: Terminal::new(CrosstermBackend::new(TTY.writer()))?,
last_area: Default::default(),
@ -32,24 +35,23 @@ impl Term {
execute!(
TTY.writer(),
yazi_term::If(!TMUX.get(), EnterAlternateScreen),
If(!TMUX.get(), EnterAlternateScreen),
Print("\x1bP$q q\x1b\\"), // Request cursor shape (DECRQSS query for DECSCUSR)
Print("\x1b[?12$p"), // Request cursor blink status (DECRQM query for DECSET 12)
Print("\x1b[?u"), // Request keyboard enhancement flags (CSI u)
Print("\x1b[0c"), // Request device attributes
yazi_term::If(TMUX.get(), EnterAlternateScreen),
yazi_term::SetBackground(true, THEME.app.bg_color()), // Set app background
If(TMUX.get(), EnterAlternateScreen),
SetBackground(&opt.bg), // Set app background
EnableBracketedPaste,
EnableFocusChange,
yazi_term::If(!YAZI.mgr.mouse_events.get().is_empty(), EnableMouseCapture),
If(opt.mouse, EnableMouseCapture),
)?;
let resp = Emulator::read_until_da1();
Mux::tmux_drain()?;
yazi_term::RestoreCursor::store(&resp);
CSI_U.store(resp.contains("\x1b[?0u"), Ordering::Relaxed);
if CSI_U.load(Ordering::Relaxed) {
STATE.set(TermState::new(&resp, &opt));
if STATE.get().csi_u {
_ = queue!(
TTY.writer(),
PushKeyboardEnhancementFlags(
@ -59,52 +61,45 @@ impl Term {
);
}
if let Some(s) = YAZI.mgr.title() {
if let Some(s) = opt.title {
queue!(TTY.writer(), SetTitle(s)).ok();
}
term.hide_cursor()?;
term.clear()?;
term.flush()?;
term.inner.hide_cursor()?;
term.inner.clear()?;
term.inner.flush()?;
Ok(term)
}
fn stop(&mut self) -> Result<()> {
if CSI_U.swap(false, Ordering::Relaxed) {
queue!(TTY.writer(), PopKeyboardEnhancementFlags).ok();
}
if !YAZI.mgr.title_format.is_empty() {
queue!(TTY.writer(), SetTitle("")).ok();
}
let state = STATE.get();
execute!(
TTY.writer(),
yazi_term::If(!YAZI.mgr.mouse_events.get().is_empty(), DisableMouseCapture),
yazi_term::RestoreCursor,
If(state.mouse, DisableMouseCapture),
If(state.bg, RestoreBackground),
If(state.csi_u, PopKeyboardEnhancementFlags),
RestoreCursor { shape: state.cursor_shape, blink: state.cursor_blink },
If(state.title, SetTitle("")),
DisableFocusChange,
DisableBracketedPaste,
LeaveAlternateScreen,
)?;
self.show_cursor()?;
self.inner.show_cursor()?;
Ok(disable_raw_mode()?)
}
pub(super) fn goodbye(f: impl FnOnce() -> i32) -> ! {
if CSI_U.swap(false, Ordering::Relaxed) {
queue!(TTY.writer(), PopKeyboardEnhancementFlags).ok();
}
if !YAZI.mgr.title_format.is_empty() {
queue!(TTY.writer(), SetTitle("")).ok();
}
pub fn goodbye(f: impl FnOnce() -> i32) -> ! {
let state = STATE.get();
execute!(
TTY.writer(),
yazi_term::If(!YAZI.mgr.mouse_events.get().is_empty(), DisableMouseCapture),
yazi_term::SetBackground(false, THEME.app.bg_color()),
yazi_term::RestoreCursor,
If(state.mouse, DisableMouseCapture),
If(state.bg, RestoreBackground),
If(state.csi_u, PopKeyboardEnhancementFlags),
RestoreCursor { shape: state.cursor_shape, blink: state.cursor_blink },
If(state.title, SetTitle("")),
DisableFocusChange,
DisableBracketedPaste,
LeaveAlternateScreen,
@ -117,7 +112,7 @@ impl Term {
std::process::exit(f());
}
pub(super) fn draw(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result<CompletedFrame<'_>> {
pub fn draw(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result<CompletedFrame<'_>> {
let last = self.inner.draw(f)?;
self.last_area = last.area;
@ -125,10 +120,7 @@ impl Term {
Ok(last)
}
pub(super) fn draw_partial(
&mut self,
f: impl FnOnce(&mut Frame),
) -> io::Result<CompletedFrame<'_>> {
pub fn draw_partial(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result<CompletedFrame<'_>> {
self.inner.draw(|frame| {
let buffer = frame.buffer_mut();
for y in self.last_area.top()..self.last_area.bottom() {
@ -143,7 +135,7 @@ impl Term {
})
}
pub(super) fn can_partial(&mut self) -> bool {
pub fn can_partial(&mut self) -> bool {
self.inner.autoresize().is_ok() && self.last_area == self.inner.get_frame().area()
}
}
@ -157,7 +149,3 @@ impl Deref for Term {
fn deref(&self) -> &Self::Target { &self.inner }
}
impl DerefMut for Term {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner }
}

View file

@ -20,9 +20,6 @@ yazi-proxy = { path = "../yazi-proxy", version = "26.1.22" }
yazi-shared = { path = "../yazi-shared", version = "26.1.22" }
yazi-vfs = { path = "../yazi-vfs", version = "26.1.22" }
# Logging
tracing = { workspace = true }
# External dependencies
anyhow = { workspace = true }
hashbrown = { workspace = true }
@ -31,3 +28,4 @@ parking_lot = { workspace = true }
percent-encoding = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }