mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 17:15:38 +08:00
refactor(Draft): management as crate (#5470)
* feat: implement draft functionality with apply and discard methods, and add benchmarks and tests * Refactor Draft management and integrate Tokio for asynchronous operations - Introduced a new `IVerge` struct for configuration management. - Updated `Draft` struct to use `Arc<RwLock>` for better concurrency handling. - Added asynchronous editing capabilities to `Draft` using Tokio. - Replaced synchronous editing methods with asynchronous counterparts. - Updated benchmark tests to reflect changes in the `Draft` API. - Removed redundant draft utility module and integrated its functionality into the main `Draft` implementation. - Adjusted tests to validate new behavior and ensure correctness of the `Draft` management flow.
This commit is contained in:
11
src-tauri/Cargo.lock
generated
11
src-tauri/Cargo.lock
generated
@@ -1122,6 +1122,7 @@ dependencies = [
|
|||||||
"criterion",
|
"criterion",
|
||||||
"deelevate",
|
"deelevate",
|
||||||
"delay_timer",
|
"delay_timer",
|
||||||
|
"draft",
|
||||||
"dunce",
|
"dunce",
|
||||||
"flexi_logger",
|
"flexi_logger",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -2016,6 +2017,16 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "draft"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"criterion",
|
||||||
|
"parking_lot",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
|||||||
tauri-build = { version = "2.5.2", features = [] }
|
tauri-build = { version = "2.5.2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
parking_lot = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
draft = { workspace = true }
|
||||||
warp = { version = "0.4.2", features = ["server"] }
|
warp = { version = "0.4.2", features = ["server"] }
|
||||||
anyhow = "1.0.100"
|
|
||||||
open = "5.3.2"
|
open = "5.3.2"
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
@@ -31,14 +34,7 @@ serde_yaml_ng = "0.10.0"
|
|||||||
once_cell = { version = "1.21.3", features = ["parking_lot"] }
|
once_cell = { version = "1.21.3", features = ["parking_lot"] }
|
||||||
port_scanner = "0.1.5"
|
port_scanner = "0.1.5"
|
||||||
delay_timer = "0.11.6"
|
delay_timer = "0.11.6"
|
||||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
|
||||||
percent-encoding = "2.3.2"
|
percent-encoding = "2.3.2"
|
||||||
tokio = { version = "1.48.0", features = [
|
|
||||||
"rt-multi-thread",
|
|
||||||
"macros",
|
|
||||||
"time",
|
|
||||||
"sync",
|
|
||||||
] }
|
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
reqwest = { version = "0.12.24", features = ["json", "cookies"] }
|
reqwest = { version = "0.12.24", features = ["json", "cookies"] }
|
||||||
regex = "1.12.2"
|
regex = "1.12.2"
|
||||||
@@ -120,6 +116,28 @@ tauri-plugin-autostart = "2.5.1"
|
|||||||
tauri-plugin-global-shortcut = "2.3.1"
|
tauri-plugin-global-shortcut = "2.3.1"
|
||||||
tauri-plugin-updater = "2.9.0"
|
tauri-plugin-updater = "2.9.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = { workspace = true }
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
draft = { path = "crates/draft" }
|
||||||
|
parking_lot = { version = "0.12.5", features = [
|
||||||
|
"hardware-lock-elision",
|
||||||
|
"send_guard",
|
||||||
|
] }
|
||||||
|
anyhow = "1.0.100"
|
||||||
|
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||||
|
tokio = { version = "1.48.0", features = [
|
||||||
|
"rt-multi-thread",
|
||||||
|
"macros",
|
||||||
|
"time",
|
||||||
|
"sync",
|
||||||
|
] }
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["crates/*"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["custom-protocol"]
|
default = ["custom-protocol"]
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
@@ -129,11 +147,6 @@ tokio-trace = ["console-subscriber"]
|
|||||||
clippy = ["tauri/test"]
|
clippy = ["tauri/test"]
|
||||||
tracing = []
|
tracing = []
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "draft_benchmark"
|
|
||||||
path = "benches/draft_benchmark.rs"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
@@ -167,9 +180,6 @@ strip = false
|
|||||||
name = "app_lib"
|
name = "app_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
# Core categories - most important for code safety and correctness
|
# Core categories - most important for code safety and correctness
|
||||||
correctness = { level = "deny", priority = -1 }
|
correctness = { level = "deny", priority = -1 }
|
||||||
|
|||||||
17
src-tauri/crates/draft/Cargo.toml
Normal file
17
src-tauri/crates/draft/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "draft"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "draft_bench"
|
||||||
|
path = "bench/benche_me.rs"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
parking_lot = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
@@ -3,17 +3,30 @@ use std::hint::black_box;
|
|||||||
use std::process;
|
use std::process;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
use app_lib::config::IVerge;
|
use draft::Draft;
|
||||||
use app_lib::utils::Draft as DraftNew;
|
|
||||||
|
|
||||||
/// 创建测试数据
|
#[derive(Clone, Debug)]
|
||||||
fn make_draft() -> DraftNew<IVerge> {
|
struct IVerge {
|
||||||
|
enable_auto_launch: Option<bool>,
|
||||||
|
enable_tun_mode: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for IVerge {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enable_auto_launch: None,
|
||||||
|
enable_tun_mode: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_draft() -> Draft<IVerge> {
|
||||||
let verge = IVerge {
|
let verge = IVerge {
|
||||||
enable_auto_launch: Some(true),
|
enable_auto_launch: Some(true),
|
||||||
enable_tun_mode: Some(false),
|
enable_tun_mode: Some(false),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
DraftNew::new(verge)
|
Draft::new(verge)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bench_draft(c: &mut Criterion) {
|
pub fn bench_draft(c: &mut Criterion) {
|
||||||
102
src-tauri/crates/draft/src/lib.rs
Normal file
102
src-tauri/crates/draft/src/lib.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub type SharedBox<T> = Arc<Box<T>>;
|
||||||
|
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
|
||||||
|
|
||||||
|
/// Draft 管理:committed 与 optional draft 都以 Arc<Box<T>> 存储,
|
||||||
|
// (committed_snapshot, optional_draft_snapshot)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Draft<T: Clone> {
|
||||||
|
inner: Arc<RwLock<DraftInner<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Draft<T> {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(data: T) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc)
|
||||||
|
#[inline]
|
||||||
|
pub fn data_arc(&self) -> SharedBox<T> {
|
||||||
|
let guard = self.inner.read();
|
||||||
|
Arc::clone(&guard.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
|
||||||
|
/// 这也是零拷贝:只 clone Arc,不 clone T
|
||||||
|
#[inline]
|
||||||
|
pub fn latest_arc(&self) -> SharedBox<T> {
|
||||||
|
let guard = self.inner.read();
|
||||||
|
guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T)
|
||||||
|
/// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T;
|
||||||
|
/// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。
|
||||||
|
#[inline]
|
||||||
|
pub fn edit_draft<F, R>(&self, f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut T) -> R,
|
||||||
|
{
|
||||||
|
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
|
||||||
|
let mut guard = self.inner.write();
|
||||||
|
let mut draft_arc = if guard.1.is_none() {
|
||||||
|
Arc::clone(&guard.0)
|
||||||
|
} else {
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
guard.1.take().unwrap()
|
||||||
|
};
|
||||||
|
drop(guard);
|
||||||
|
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
|
||||||
|
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
|
||||||
|
// 对 Box<T> 解引用得到 &mut T
|
||||||
|
let result = f(&mut **boxed);
|
||||||
|
// 恢复修改后的草稿 Arc
|
||||||
|
self.inner.write().1 = Some(draft_arc);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将草稿提交到已提交位置(替换),并清除草稿
|
||||||
|
#[inline]
|
||||||
|
pub fn apply(&self) {
|
||||||
|
let mut guard = self.inner.write();
|
||||||
|
if let Some(d) = guard.1.take() {
|
||||||
|
guard.0 = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 丢弃草稿(如果存在)
|
||||||
|
#[inline]
|
||||||
|
pub fn discard(&self) {
|
||||||
|
let mut guard = self.inner.write();
|
||||||
|
guard.1 = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 异步地以拥有 Box<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
|
||||||
|
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
|
||||||
|
#[inline]
|
||||||
|
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
F: FnOnce(Box<T>) -> Fut + Send,
|
||||||
|
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
|
||||||
|
{
|
||||||
|
// 读取已提交快照(cheap Arc clone, 然后得到 Box<T> 所有权 via clone)
|
||||||
|
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T(不可避免)
|
||||||
|
let local: Box<T> = {
|
||||||
|
let guard = self.inner.read();
|
||||||
|
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone)
|
||||||
|
(*guard.0).clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (new_local, res) = f(local).await?;
|
||||||
|
|
||||||
|
// 将新的 Box<T> 放到已提交位置(包进 Arc)
|
||||||
|
self.inner.write().0 = Arc::new(new_local);
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,110 +1,7 @@
|
|||||||
use parking_lot::RwLock;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub type SharedBox<T> = Arc<Box<T>>;
|
|
||||||
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
|
|
||||||
|
|
||||||
/// Draft 管理:committed 与 optional draft 都以 Arc<Box<T>> 存储,
|
|
||||||
// (committed_snapshot, optional_draft_snapshot)
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Draft<T: Clone> {
|
|
||||||
inner: Arc<RwLock<DraftInner<T>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> Draft<T> {
|
|
||||||
#[inline]
|
|
||||||
pub fn new(data: T) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc)
|
|
||||||
#[inline]
|
|
||||||
pub fn data_arc(&self) -> SharedBox<T> {
|
|
||||||
let guard = self.inner.read();
|
|
||||||
Arc::clone(&guard.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
|
|
||||||
/// 这也是零拷贝:只 clone Arc,不 clone T
|
|
||||||
#[inline]
|
|
||||||
pub fn latest_arc(&self) -> SharedBox<T> {
|
|
||||||
let guard = self.inner.read();
|
|
||||||
guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T)
|
|
||||||
/// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T;
|
|
||||||
/// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。
|
|
||||||
#[inline]
|
|
||||||
pub fn edit_draft<F, R>(&self, f: F) -> R
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut T) -> R,
|
|
||||||
{
|
|
||||||
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
|
|
||||||
let mut guard = self.inner.write();
|
|
||||||
let mut draft_arc = if guard.1.is_none() {
|
|
||||||
Arc::clone(&guard.0)
|
|
||||||
} else {
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
guard.1.take().unwrap()
|
|
||||||
};
|
|
||||||
drop(guard);
|
|
||||||
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
|
|
||||||
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
|
|
||||||
// 对 Box<T> 解引用得到 &mut T
|
|
||||||
let result = f(&mut **boxed);
|
|
||||||
// 恢复修改后的草稿 Arc
|
|
||||||
self.inner.write().1 = Some(draft_arc);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 将草稿提交到已提交位置(替换),并清除草稿
|
|
||||||
#[inline]
|
|
||||||
pub fn apply(&self) {
|
|
||||||
let mut guard = self.inner.write();
|
|
||||||
if let Some(d) = guard.1.take() {
|
|
||||||
guard.0 = d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 丢弃草稿(如果存在)
|
|
||||||
#[inline]
|
|
||||||
pub fn discard(&self) {
|
|
||||||
let mut guard = self.inner.write();
|
|
||||||
guard.1 = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 异步地以拥有 Box<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
|
|
||||||
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
|
|
||||||
#[inline]
|
|
||||||
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
|
|
||||||
where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
F: FnOnce(Box<T>) -> Fut + Send,
|
|
||||||
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
|
|
||||||
{
|
|
||||||
// 读取已提交快照(cheap Arc clone, 然后得到 Box<T> 所有权 via clone)
|
|
||||||
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T(不可避免)
|
|
||||||
let local: Box<T> = {
|
|
||||||
let guard = self.inner.read();
|
|
||||||
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone)
|
|
||||||
(*guard.0).clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (new_local, res) = f(local).await?;
|
|
||||||
|
|
||||||
// 将新的 Box<T> 放到已提交位置(包进 Arc)
|
|
||||||
self.inner.write().0 = Arc::new(new_local);
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use draft::Draft;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{cmd::StringifyErr as _, config::IVerge, feat, utils::draft::SharedBox};
|
use crate::{cmd::StringifyErr as _, config::IVerge, feat};
|
||||||
|
use draft::SharedBox;
|
||||||
|
|
||||||
/// 获取Verge配置
|
/// 获取Verge配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ use crate::{
|
|||||||
constants::{files, timing},
|
constants::{files, timing},
|
||||||
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
|
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
|
||||||
enhance, logging, logging_error,
|
enhance, logging, logging_error,
|
||||||
utils::{Draft, dirs, help, logging::Type},
|
utils::{dirs, help, logging::Type},
|
||||||
};
|
};
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use backoff::{Error as BackoffError, ExponentialBackoff};
|
use backoff::{Error as BackoffError, ExponentialBackoff};
|
||||||
|
use draft::Draft;
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ use crate::{
|
|||||||
core::{CoreManager, handle, hotkey, sysopt, tray},
|
core::{CoreManager, handle, hotkey, sysopt, tray},
|
||||||
logging, logging_error,
|
logging, logging_error,
|
||||||
module::{auto_backup::AutoBackupManager, lightweight},
|
module::{auto_backup::AutoBackupManager, lightweight},
|
||||||
utils::{draft::SharedBox, logging::Type},
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use draft::SharedBox;
|
||||||
use serde_yaml_ng::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
|
|
||||||
/// Patch Clash configuration
|
/// Patch Clash configuration
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
pub mod autostart;
|
pub mod autostart;
|
||||||
pub mod dirs;
|
pub mod dirs;
|
||||||
pub mod draft;
|
|
||||||
pub mod format;
|
pub mod format;
|
||||||
pub mod help;
|
pub mod help;
|
||||||
pub mod i18n;
|
pub mod i18n;
|
||||||
@@ -15,5 +14,3 @@ pub mod server;
|
|||||||
pub mod singleton;
|
pub mod singleton;
|
||||||
pub mod tmpl;
|
pub mod tmpl;
|
||||||
pub mod window_manager;
|
pub mod window_manager;
|
||||||
|
|
||||||
pub use draft::Draft;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user