initial UI framework
This commit is contained in:
parent
93195bab1b
commit
6b1098c863
|
@ -15,8 +15,8 @@ dependencies = [
|
||||||
name = "arkham"
|
name = "arkham"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console",
|
|
||||||
"criterion",
|
"criterion",
|
||||||
|
"crossterm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"textwrap 0.14.2",
|
"textwrap 0.14.2",
|
||||||
|
@ -42,9 +42,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.2.1"
|
version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bstr"
|
name = "bstr"
|
||||||
|
@ -90,21 +90,6 @@ dependencies = [
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "console"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7cc80946b3480f421c2f17ed1cb841753a371c7c5104f51d507e13f532c856aa"
|
|
||||||
dependencies = [
|
|
||||||
"encode_unicode",
|
|
||||||
"lazy_static",
|
|
||||||
"libc",
|
|
||||||
"regex",
|
|
||||||
"terminal_size",
|
|
||||||
"unicode-width",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "criterion"
|
name = "criterion"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
@ -185,6 +170,32 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"futures-core",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "csv"
|
name = "csv"
|
||||||
version = "1.1.6"
|
version = "1.1.6"
|
||||||
|
@ -214,10 +225,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encode_unicode"
|
name = "futures-core"
|
||||||
version = "0.3.6"
|
version = "0.3.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "half"
|
name = "half"
|
||||||
|
@ -234,6 +245,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -275,9 +295,18 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.88"
|
version = "0.2.112"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03b07a082330a35e43f63177cc01689da34fbffa0105e1246cf0311472cac73a"
|
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
|
@ -303,6 +332,37 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "0.7.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"miow",
|
||||||
|
"ntapi",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miow"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntapi"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
|
@ -334,6 +394,31 @@ version = "11.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"instant",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
|
@ -414,6 +499,15 @@ dependencies = [
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.2.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.4.3"
|
version = "1.4.3"
|
||||||
|
@ -527,6 +621,42 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smawk"
|
name = "smawk"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -544,16 +674,6 @@ dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "terminal_size"
|
|
||||||
version = "0.1.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "textwrap"
|
name = "textwrap"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
|
|
@ -14,12 +14,12 @@ categories = ["command-line-interface", "config"]
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# indicatif = "0.15.0"
|
|
||||||
console = "0.14.0"
|
|
||||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||||
serde_json = { version = "1.0", optional = true}
|
serde_json = { version = "1.0", optional = true}
|
||||||
toml = { version="0.5.8", optional = true}
|
toml = { version="0.5.8", optional = true}
|
||||||
textwrap = "0.14.2"
|
textwrap = "0.14.2"
|
||||||
|
crossterm = { version = "0.22.1", features=["event-stream"]}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -35,3 +35,7 @@ criterion = "0.3"
|
||||||
name = "arg_parsing"
|
name = "arg_parsing"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "space_fill"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
use arkham::ui;
|
||||||
|
use criterion::{criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
|
fn space_fill(c: &mut Criterion) {
|
||||||
|
c.bench_function("large_space_fill", |b| {
|
||||||
|
let mut space = ui::View::new(200, 200);
|
||||||
|
space.fill(
|
||||||
|
ui::Rect::new(50, 50, 100, 100),
|
||||||
|
*ui::Cell::default()
|
||||||
|
.fg(ui::Color::White)
|
||||||
|
.bg(ui::Color::Green),
|
||||||
|
);
|
||||||
|
b.iter(|| {
|
||||||
|
let mut output: Vec<u8> = vec![];
|
||||||
|
space
|
||||||
|
.render(ui::Pos::new(0, 0), &mut output)
|
||||||
|
.expect("couldnt render");
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("small_space_fill", |b| {
|
||||||
|
let mut space = ui::View::new(20, 20);
|
||||||
|
space.fill(
|
||||||
|
ui::Rect::new(0, 0, 20, 20),
|
||||||
|
*ui::Cell::default()
|
||||||
|
.fg(ui::Color::White)
|
||||||
|
.bg(ui::Color::Green),
|
||||||
|
);
|
||||||
|
b.iter(|| {
|
||||||
|
let mut output: Vec<u8> = vec![];
|
||||||
|
space
|
||||||
|
.render(ui::Pos::new(0, 0), &mut output)
|
||||||
|
.expect("couldnt render");
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("fill_all", |b| {
|
||||||
|
let mut space = ui::View::new(200, 200);
|
||||||
|
space.fill_all(
|
||||||
|
*ui::Cell::default()
|
||||||
|
.fg(ui::Color::White)
|
||||||
|
.bg(ui::Color::Green),
|
||||||
|
);
|
||||||
|
b.iter(|| {
|
||||||
|
let mut output: Vec<u8> = vec![];
|
||||||
|
space
|
||||||
|
.render(ui::Pos::new(0, 0), &mut output)
|
||||||
|
.expect("couldnt render");
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, space_fill);
|
||||||
|
criterion_main!(benches);
|
|
@ -0,0 +1,29 @@
|
||||||
|
use arkham::{
|
||||||
|
ui::{Cell, Color, Component, Rect, UI},
|
||||||
|
Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct OuterComponent;
|
||||||
|
|
||||||
|
impl Component for OuterComponent {
|
||||||
|
fn view(&mut self, ctx: &mut arkham::ui::Context) -> Result<()> {
|
||||||
|
ctx.view
|
||||||
|
.fill_all(*Cell::default().content(' ').bg(Color::Red));
|
||||||
|
ctx.add_component(Rect::new(20, 20, 40, 40), &mut InnerComponent)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InnerComponent;
|
||||||
|
|
||||||
|
impl Component for InnerComponent {
|
||||||
|
fn view(&mut self, ctx: &mut arkham::ui::Context) -> Result<()> {
|
||||||
|
ctx.view
|
||||||
|
.fill_all(*Cell::default().content(' ').bg(Color::Green));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
UI::new(OuterComponent).run().expect("Couldnt run UI loop");
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
// use arkham::TaskGroup;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// let tasks = TaskGroup::new();
|
|
||||||
// let task = tasks.start_task("task 1");
|
|
||||||
// task.tick();
|
|
||||||
// tasks.join();
|
|
||||||
}
|
|
|
@ -1,11 +1,12 @@
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use crate::{
|
use super::{
|
||||||
context::Context,
|
|
||||||
opt::{self, Opt},
|
opt::{self, Opt},
|
||||||
vox, App,
|
App, Context,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::helpers;
|
||||||
|
|
||||||
pub type Handler = fn(&App, &Context, &[String]);
|
pub type Handler = fn(&App, &Context, &[String]);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -58,7 +59,7 @@ impl Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn help(app: &App, _ctx: &Context, args: &[String]) {
|
pub(crate) fn help(app: &App, _ctx: &Context, args: &[String]) {
|
||||||
vox::print(app.application_header());
|
helpers::print(app.application_header());
|
||||||
print_command_help(&app.root, args);
|
print_command_help(&app.root, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,17 +69,17 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) {
|
||||||
return print_command_help(cmd, &args[1..]);
|
return print_command_help(cmd, &args[1..]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vox::print("");
|
helpers::print("");
|
||||||
if let Some(desc) = cmd.long_desc.as_ref().or_else(|| cmd.short_desc.as_ref()) {
|
if let Some(desc) = cmd.long_desc.as_ref().or_else(|| cmd.short_desc.as_ref()) {
|
||||||
if cmd.name != "root" {
|
if cmd.name != "root" {
|
||||||
vox::header(&cmd.name.to_uppercase());
|
helpers::header(&cmd.name.to_uppercase());
|
||||||
}
|
}
|
||||||
vox::print(desc);
|
helpers::print(desc);
|
||||||
vox::print("");
|
helpers::print("");
|
||||||
}
|
}
|
||||||
if !cmd.opts.is_empty() {
|
if !cmd.opts.is_empty() {
|
||||||
vox::header("OPTIONS");
|
helpers::header("OPTIONS");
|
||||||
vox::description_list(
|
helpers::description_list(
|
||||||
cmd.opts
|
cmd.opts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|o| (o.usage(), o.desc.clone().unwrap_or_default()))
|
.map(|o| (o.usage(), o.desc.clone().unwrap_or_default()))
|
||||||
|
@ -87,8 +88,8 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cmd.commands.is_empty() {
|
if !cmd.commands.is_empty() {
|
||||||
vox::header("Commands");
|
helpers::header("Commands");
|
||||||
vox::description_list(
|
helpers::description_list(
|
||||||
cmd.commands
|
cmd.commands
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| (c.name.clone(), c.short_desc.clone().unwrap_or_default()))
|
.map(|c| (c.name.clone(), c.short_desc.clone().unwrap_or_default()))
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::{Command, Opt};
|
use crate::{Command, Opt};
|
||||||
use std::{collections::BTreeMap, env};
|
use std::{collections::BTreeMap, env};
|
||||||
|
|
||||||
|
use super::command::print_command_help;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Map(BTreeMap<String, ContextValue>);
|
pub struct Map(BTreeMap<String, ContextValue>);
|
||||||
|
|
||||||
|
@ -240,7 +242,7 @@ impl Context {
|
||||||
|
|
||||||
/// Can be used to display the automatic help message for the current command.
|
/// Can be used to display the automatic help message for the current command.
|
||||||
pub fn display_help(&self) {
|
pub fn display_help(&self) {
|
||||||
crate::command::print_command_help(&self.cmd, &[])
|
print_command_help(&self.cmd, &[])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn load_config_file(&mut self, filename: &str) {
|
pub(crate) fn load_config_file(&mut self, filename: &str) {
|
|
@ -1,4 +1,4 @@
|
||||||
use console::style;
|
use crossterm::style::{self, style, Stylize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
@ -13,10 +13,9 @@ pub enum Color {
|
||||||
Magenta,
|
Magenta,
|
||||||
Cyan,
|
Cyan,
|
||||||
White,
|
White,
|
||||||
Color256(u8),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Color> for console::Color {
|
impl From<Color> for style::Color {
|
||||||
fn from(fr: Color) -> Self {
|
fn from(fr: Color) -> Self {
|
||||||
match fr {
|
match fr {
|
||||||
Color::Black => Self::Black,
|
Color::Black => Self::Black,
|
||||||
|
@ -27,33 +26,23 @@ impl From<Color> for console::Color {
|
||||||
Color::Magenta => Self::Magenta,
|
Color::Magenta => Self::Magenta,
|
||||||
Color::Cyan => Self::Cyan,
|
Color::Cyan => Self::Cyan,
|
||||||
Color::White => Self::White,
|
Color::White => Self::White,
|
||||||
Color::Color256(v) => Self::Color256(v),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn labeled(color: Color, label: &str, msg: &str) {
|
pub fn print<T: Display>(s: T) {
|
||||||
let label = format!("[{}]", label);
|
println!("{}", s);
|
||||||
println!("{} {}", console::style(label).fg(color.into()).bold(), msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn message<T: Display>(str: T) {
|
|
||||||
println!("{}", style(str).white().bold());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn header<T: Display>(str: T) {
|
pub fn header<T: Display>(str: T) {
|
||||||
println!(
|
println!(
|
||||||
"{} {} {}",
|
"{} {} {}",
|
||||||
style("-=[").red().dim(),
|
"-=[".red().dim(),
|
||||||
style(str).white().bold(),
|
style::style(str).white().bold(),
|
||||||
style("]=-").red().dim()
|
"]=-".red().dim()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn note<T: Display>(str: T) {
|
|
||||||
println!("{}", style(str).white().dim());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn description_list(list: HashMap<String, String>) {
|
pub fn description_list(list: HashMap<String, String>) {
|
||||||
let mut lines = list.into_iter().collect::<Vec<(String, String)>>();
|
let mut lines = list.into_iter().collect::<Vec<(String, String)>>();
|
||||||
let key_max_length = lines.iter().map(|(v, _)| v.len()).max().unwrap_or(10) + 3;
|
let key_max_length = lines.iter().map(|(v, _)| v.len()).max().unwrap_or(10) + 3;
|
||||||
|
@ -80,10 +69,6 @@ pub fn description_list(list: HashMap<String, String>) {
|
||||||
println!("{}", output);
|
println!("{}", output);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print<T: Display>(s: T) {
|
|
||||||
println!("{}", s);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error<T: Display>(s: T) {
|
pub fn error<T: Display>(s: T) {
|
||||||
println!("{}", style(s).red().bold());
|
println!("{}", style(s).red().bold());
|
||||||
}
|
}
|
|
@ -1,11 +1,17 @@
|
||||||
use crate::{print_command_help, vox};
|
mod command;
|
||||||
|
mod context;
|
||||||
|
mod helpers;
|
||||||
|
mod opt;
|
||||||
|
|
||||||
use super::command::{help, Command, Handler};
|
use command::help;
|
||||||
use super::context::Context;
|
pub use command::{Command, Handler};
|
||||||
use super::opt::{Opt, OptError, OptKind};
|
pub use context::Context;
|
||||||
|
pub use opt::{Opt, OptError, OptKind};
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
use command::print_command_help;
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, OptError>;
|
type Result<T> = std::result::Result<T, OptError>;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
@ -196,7 +202,7 @@ impl App {
|
||||||
if let Err(e) = run_command(self, &self.root, &args, &mut ctx) {
|
if let Err(e) = run_command(self, &self.root, &args, &mut ctx) {
|
||||||
match e {
|
match e {
|
||||||
OptError::InvalidOpt(opt) => {
|
OptError::InvalidOpt(opt) => {
|
||||||
vox::error(format!("Invalid options {}", &opt));
|
helpers::error(format!("Invalid options {}", &opt));
|
||||||
Err(OptError::InvalidOpt(opt))
|
Err(OptError::InvalidOpt(opt))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -315,7 +321,7 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) ->
|
||||||
|
|
||||||
// Automatic command help display
|
// Automatic command help display
|
||||||
if ignored.iter().any(|a| a == "-h" || a == "--help") {
|
if ignored.iter().any(|a| a == "-h" || a == "--help") {
|
||||||
super::command::print_command_help(cmd, &[]);
|
print_command_help(cmd, &[]);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,7 +338,7 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) ->
|
||||||
}
|
}
|
||||||
handler(app, ctx, &ignored);
|
handler(app, ctx, &ignored);
|
||||||
} else {
|
} else {
|
||||||
crate::vox::print(app.application_header());
|
helpers::print(app.application_header());
|
||||||
print_command_help(cmd, &[])
|
print_command_help(cmd, &[])
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ impl Opt {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) enum OptKind {
|
pub enum OptKind {
|
||||||
Flag,
|
Flag,
|
||||||
String,
|
String,
|
||||||
}
|
}
|
40
src/lib.rs
40
src/lib.rs
|
@ -2,13 +2,35 @@
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod command;
|
pub mod ui;
|
||||||
mod context;
|
|
||||||
mod opt;
|
use std::error::Error;
|
||||||
pub use console;
|
use std::fmt::{Debug, Display};
|
||||||
pub mod vox;
|
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use command::*;
|
pub use app::{App, Command, Opt};
|
||||||
pub use context::*;
|
|
||||||
pub use opt::*;
|
pub type Result<T> = std::result::Result<T, ArkhamError>;
|
||||||
pub use vox::*;
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ArkhamError {
|
||||||
|
IO(String),
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for ArkhamError {}
|
||||||
|
|
||||||
|
impl Display for ArkhamError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Other(v) => f.write_str(v),
|
||||||
|
Self::IO(v) => f.write_str(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for ArkhamError {
|
||||||
|
fn from(fr: std::io::Error) -> Self {
|
||||||
|
Self::IO(fr.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
64
src/tasks.rs
64
src/tasks.rs
|
@ -1,64 +0,0 @@
|
||||||
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
|
|
||||||
use std::{
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TaskGroup {
|
|
||||||
mp: Arc<MultiProgress>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskGroup {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
mp: Arc::new(MultiProgress::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_task(&self, desc: &str) -> Task {
|
|
||||||
let pb = self.mp.add(ProgressBar::new(1));
|
|
||||||
let spinner_style = ProgressStyle::default_spinner()
|
|
||||||
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ")
|
|
||||||
.template("{prefix:.bold.dim} {spinner} {wide_msg}");
|
|
||||||
pb.set_style(spinner_style);
|
|
||||||
Task::new(TaskState {
|
|
||||||
pb,
|
|
||||||
desc: desc.into(),
|
|
||||||
started_at: Instant::now(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn join(&self) {
|
|
||||||
self.mp.join().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Task(Arc<Mutex<TaskState>>);
|
|
||||||
|
|
||||||
impl Task {
|
|
||||||
pub fn new(state: TaskState) -> Self {
|
|
||||||
Self(Arc::new(Mutex::new(state)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(&self) {
|
|
||||||
let state = self.0.lock().unwrap();
|
|
||||||
state.pb.set_message(&format!(
|
|
||||||
"{} [{}]",
|
|
||||||
state.desc,
|
|
||||||
style(HumanDuration(state.started_at.elapsed())).yellow()
|
|
||||||
));
|
|
||||||
state.pb.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn complete(&self) {
|
|
||||||
self.0.lock().unwrap().pb.finish_with_message("OK");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TaskState {
|
|
||||||
pb: ProgressBar,
|
|
||||||
desc: String,
|
|
||||||
started_at: Instant,
|
|
||||||
}
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
use crossterm::queue;
|
||||||
|
use crossterm::style::{
|
||||||
|
Attribute, Attributes, SetAttributes, SetBackgroundColor, SetForegroundColor,
|
||||||
|
};
|
||||||
|
use crossterm::style::{Color, Print};
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Copy, PartialEq)]
|
||||||
|
pub struct Cell {
|
||||||
|
empty: bool,
|
||||||
|
fg: Color,
|
||||||
|
bg: Color,
|
||||||
|
content: char,
|
||||||
|
attributes: Attributes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Cell {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
empty: true,
|
||||||
|
fg: Color::Reset,
|
||||||
|
bg: Color::Reset,
|
||||||
|
content: ' ',
|
||||||
|
attributes: Attributes::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cell {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content(&mut self, v: char) -> &mut Cell {
|
||||||
|
self.empty = false;
|
||||||
|
self.content = v;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fg(&mut self, color: Color) -> &mut Cell {
|
||||||
|
self.fg = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bg(&mut self, color: Color) -> &mut Cell {
|
||||||
|
self.bg = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bold(&mut self, v: bool) -> &mut Cell {
|
||||||
|
if v {
|
||||||
|
self.attributes.set(Attribute::Bold);
|
||||||
|
} else {
|
||||||
|
self.attributes.unset(Attribute::Bold);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, output: &mut impl Write) -> Result<()> {
|
||||||
|
if !self.empty {
|
||||||
|
queue!(output, SetForegroundColor(self.fg))?;
|
||||||
|
queue!(output, SetBackgroundColor(self.bg))?;
|
||||||
|
queue!(output, SetAttributes(self.attributes))?;
|
||||||
|
queue!(output, Print(&self.content))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn raw(&self) -> Vec<u8> {
|
||||||
|
let mut output: Vec<u8> = vec![];
|
||||||
|
self.render(&mut output).expect("Render error");
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn cell_render() {
|
||||||
|
let mut cell = Cell::default();
|
||||||
|
cell.fg(Color::Red);
|
||||||
|
cell.bg(Color::White);
|
||||||
|
cell.content = 'T';
|
||||||
|
cell.bold(true);
|
||||||
|
let mut output: Vec<u8> = vec![];
|
||||||
|
cell.render(&mut output).expect("Render error");
|
||||||
|
let out_str = String::from_utf8(output).expect("Couldnt unwrap to utf8");
|
||||||
|
assert_eq!(out_str, "\u{1b}[38;5;9m\u{1b}[48;5;15m\u{1b}[1mT");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
use super::Context;
|
||||||
|
|
||||||
|
pub trait Component {
|
||||||
|
fn view(&mut self, _ctx: &mut Context) -> Result<()>;
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
use crossterm::event::Event;
|
||||||
|
|
||||||
|
use super::{Component, Rect, View};
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
pub view: View,
|
||||||
|
pub event: Option<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Context {
|
||||||
|
fn default() -> Self {
|
||||||
|
Context {
|
||||||
|
view: View::fullscreen(),
|
||||||
|
event: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn new(width: u16, height: u16, event: Option<Event>) -> Self {
|
||||||
|
Context {
|
||||||
|
view: View::new(width, height),
|
||||||
|
event,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_component(&mut self, rect: Rect, cmp: &mut impl Component) -> Result<()> {
|
||||||
|
let mut ctx = Context::new(rect.width, rect.height, self.event);
|
||||||
|
cmp.view(&mut ctx)?;
|
||||||
|
self.view.merge(rect.pos, ctx.view);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
use std::ops::{Add, Sub};
|
||||||
|
|
||||||
|
pub struct Rect {
|
||||||
|
pub pos: Pos,
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rect {
|
||||||
|
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
|
||||||
|
Rect {
|
||||||
|
pos: Pos::new(x, y),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct Pos(u16, u16);
|
||||||
|
|
||||||
|
impl Pos {
|
||||||
|
pub fn new(x: u16, y: u16) -> Self {
|
||||||
|
Pos(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn x(&self) -> u16 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y(&self) -> u16 {
|
||||||
|
self.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for Pos {
|
||||||
|
type Output = Pos;
|
||||||
|
fn add(self, rhs: Self) -> Self::Output {
|
||||||
|
Pos(self.x() + rhs.x(), self.y() + rhs.y())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub for Pos {
|
||||||
|
type Output = Pos;
|
||||||
|
fn sub(self, rhs: Self) -> Self::Output {
|
||||||
|
Pos(
|
||||||
|
(self.x() as i32 - rhs.x() as i32).max(0) as u16,
|
||||||
|
(self.y() as i32 - rhs.y() as i32).max(0) as u16,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
mod cell;
|
||||||
|
mod component;
|
||||||
|
mod context;
|
||||||
|
mod geometry;
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
pub use cell::Cell;
|
||||||
|
pub use component::Component;
|
||||||
|
pub use context::Context;
|
||||||
|
pub use crossterm::style::{Color, Stylize};
|
||||||
|
use crossterm::terminal::enable_raw_mode;
|
||||||
|
pub use geometry::{Pos, Rect};
|
||||||
|
pub use std::io::Write;
|
||||||
|
pub use view::View;
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
pub struct UI {
|
||||||
|
root: Box<dyn Component>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UI {
|
||||||
|
pub fn new<C: Component + 'static>(root: C) -> Self {
|
||||||
|
Self {
|
||||||
|
root: Box::new(root),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn run(&mut self) -> Result<()> {
|
||||||
|
let mut output = std::io::stdout();
|
||||||
|
let mut context = Context::default();
|
||||||
|
loop {
|
||||||
|
// enable_raw_mode();
|
||||||
|
self.root.view(&mut context)?;
|
||||||
|
context.view.render(Pos::new(0, 0), &mut output)?;
|
||||||
|
output.flush().expect("Couldnt flush output");
|
||||||
|
|
||||||
|
let event = crossterm::event::read()?;
|
||||||
|
context.event = Some(event);
|
||||||
|
self.root.view(&mut context)?;
|
||||||
|
context.view.render(Pos::new(0, 0), &mut output)?;
|
||||||
|
output.flush().expect("Couldnt flush output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
use super::Rect;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
cursor::{MoveDown, MoveTo, MoveToColumn},
|
||||||
|
queue, terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
use super::{Cell, Pos};
|
||||||
|
|
||||||
|
pub struct View {
|
||||||
|
cells: Vec<Cell>,
|
||||||
|
width: u16,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View {
|
||||||
|
pub fn new(width: u16, height: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
cells: vec![Cell::default(); (width * height) as usize],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fullscreen() -> Self {
|
||||||
|
let (width, height) = terminal::size().expect("Couldnt detect terminal size");
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
cells: vec![Cell::default(); (width * height) as usize],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, pos: Pos) -> Option<&Cell> {
|
||||||
|
self.cells.get((pos.y() * self.width + pos.x()) as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, pos: Pos, cell: Cell) {
|
||||||
|
self.cells[(pos.y() * self.width + pos.x()) as usize] = cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill(&mut self, rect: Rect, cell: Cell) {
|
||||||
|
for y in rect.pos.y()..rect.pos.y() + rect.height {
|
||||||
|
for x in rect.pos.x()..rect.pos.x() + rect.width {
|
||||||
|
self.set(Pos::new(x, y), cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_all(&mut self, cell: Cell) {
|
||||||
|
self.cells.fill(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&mut self, pos: Pos, view: View) {
|
||||||
|
for (idx, line) in view.cells.chunks(view.width as usize).enumerate() {
|
||||||
|
let start = ((pos.y() + idx as u16) * self.width + pos.x()) as usize;
|
||||||
|
let end = start + line.len();
|
||||||
|
self.cells.splice(start..end, line.iter().cloned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, pos: Pos, output: &mut impl Write) -> Result<()> {
|
||||||
|
queue!(output, MoveTo(pos.x(), pos.y()))?;
|
||||||
|
for line in self.cells.chunks(self.width as usize) {
|
||||||
|
for cell in line {
|
||||||
|
cell.render(output)?;
|
||||||
|
}
|
||||||
|
queue!(output, MoveDown(1))?;
|
||||||
|
queue!(output, MoveToColumn(pos.x()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue