use std::error::Error;
use std::io::{self, Cursor};
use std::path::{Path, PathBuf};
use std::{env, fs};
use clap::Parser;
use futures::executor::block_on;
use futures::future;
use helper::entry_watcher;
use pagedump::page_dump;
use tracing::{debug, error};
use quake_core::entry::entry_defines::EntryDefines;
use quake_core::entry::entry_paths::EntryPaths;
use quake_core::quake::QuakeActionNode;
use quake_core::QuakeConfig;
use quake_tui::tui_main_loop;
use crate::server::quake_rocket;
use crate::usecases::generate_usecases::generate_by_flow;
pub mod cli;
pub mod helper;
pub mod pagedump;
pub mod server;
pub mod usecases;
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Parser)]
#[clap(version = "0.3.0", author = "Inherd <quake@inherd.org>")]
pub struct Opts {
#[clap(subcommand)]
cmd: SubCommand,
}
#[derive(Parser)]
pub enum SubCommand {
Init(Init),
Cmd(Command),
Server(WebServer),
Tui(Terminal),
Pagedump(PageDump),
Generate(Generate),
}
#[derive(Parser)]
pub struct Terminal {}
#[derive(Parser)]
pub struct PageDump {
#[clap(short, long, default_value = ".quake.yaml")]
config: String,
#[clap(short, long, default_value = "output")]
output: String,
}
#[derive(Parser)]
pub struct WebServer {
#[clap(short, long, default_value = ".quake.yaml")]
config: String,
#[clap(short, long)]
watch: bool,
}
#[derive(Parser)]
pub struct Init {
#[clap(short, long, default_value = ".")]
path: String,
#[clap(short, long)]
download: bool,
}
#[derive(Parser, Debug)]
pub struct Generate {
#[clap(short, long, default_value = ".quake.yaml")]
config: String,
#[clap(short, long)]
flow: String,
}
#[derive(Parser, Debug)]
pub struct Command {
#[clap(short, long, default_value = ".quake.yaml")]
config: String,
#[clap(short, long)]
input: String,
#[clap(short, long, default_value = "")]
editor: String,
}
#[rocket::main]
async fn main() {
let opts: Opts = Opts::parse();
setup_log();
if let Err(err) = process_cmd(opts).await {
error!("{:?}", err);
}
}
pub async fn process_cmd(opts: Opts) -> Result<(), Box<dyn Error>> {
match opts.cmd {
SubCommand::Init(init) => init_projects(init).await?,
SubCommand::Cmd(cmd) => {
let conf = config_quake(&cmd)?;
if !cmd.input.is_empty() {
let expr = QuakeActionNode::from_text(cmd.input.as_str())?;
cli::action(expr, conf)?
}
}
SubCommand::Server(server) => {
let config = load_config(&server.config)?;
run_server(server, config).await
}
SubCommand::Tui(_) => {
tui_main_loop()?;
}
SubCommand::Pagedump(dump) => {
let config = load_config(&dump.config)?;
page_dump(config);
}
SubCommand::Generate(generate) => {
let conf = load_config(&generate.config)?;
generate_by_flow(&generate.flow, &conf)?;
}
}
Ok(())
}
async fn run_server(server: WebServer, config: QuakeConfig) {
let workspace = config.workspace;
let search_url = config.search_url;
if server.watch {
block_on(async {
let (_s, _g) = future::join(
quake_rocket().launch(),
entry_watcher::async_watch(workspace, search_url),
)
.await;
});
} else {
#[allow(clippy::async_yields_async)]
let _ = block_on(async { quake_rocket().launch() }).await;
}
}
fn setup_log() {
use tracing_subscriber::prelude::*;
let filter_layer = tracing_subscriber::filter::LevelFilter::DEBUG;
let fmt_layer = tracing_subscriber::fmt::layer().with_target(true);
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.init();
}
fn config_quake(cmd: &Command) -> Result<QuakeConfig, Box<dyn Error>> {
let mut conf = load_config(&cmd.config)?;
if !cmd.editor.is_empty() {
conf.editor = cmd.editor.clone();
}
Ok(conf)
}
fn load_config(path: &str) -> Result<QuakeConfig, Box<dyn Error>> {
let content =
fs::read_to_string(path).expect("lost .quake.yaml config, please run `quake init`");
let conf: QuakeConfig = serde_yaml::from_str(content.as_str()).expect("serde .quake.yml error");
Ok(conf)
}
async fn init_projects(config: Init) -> Result<(), Box<dyn Error>> {
fs::create_dir_all(&config.path)?;
let workspace = PathBuf::from(&config.path);
let define = workspace.join(EntryPaths::entries_define());
let path = workspace.join(EntryPaths::quake_config());
let quake_config = QuakeConfig {
workspace: config.path,
editor: "".to_string(),
search_url: "http://127.0.0.1:7700".to_string(),
server_location: "web".to_string(),
port: 8000,
};
fs::write(&path, serde_yaml::to_string(&quake_config)?)?;
debug!(
"create {:} in {:?}",
EntryPaths::quake_config(),
&path.display()
);
let todo_define = "
- type: todo
display: Todo
properties:
- title: Title
- author: String
";
let file = EntryDefines {
entries: serde_yaml::from_str(todo_define).unwrap(),
};
fs::write(&define, serde_yaml::to_string(&file)?)?;
debug!("create default entry defines in {:?}", &define.display());
if config.download {
let target = format!(
"https://github.com/phodal/quake/releases/download/v{}/web.zip",
VERSION
);
debug!("download web.zip from {}", target);
let response = reqwest::get(&target).await?.bytes().await?;
unzip_all(Cursor::new(response.to_vec()), &workspace)?;
}
Ok(())
}
fn unzip_all(reader: Cursor<Vec<u8>>, workspace: &Path) -> Result<(), Box<dyn Error>> {
let mut archive = zip::ZipArchive::new(reader)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let out_path = match file.enclosed_name() {
Some(path) => workspace.join(path),
None => continue,
};
{
let comment = file.comment();
if !comment.is_empty() {
println!("Plugin {} comment: {}", i, comment);
}
}
if (&*file.name()).ends_with('/') {
println!("File {} extracted to \"{}\"", i, out_path.display());
fs::create_dir_all(&out_path)?;
} else {
println!(
"File {} extracted to \"{}\" ({} bytes)",
i,
out_path.display(),
file.size()
);
if let Some(p) = out_path.parent() {
if !p.exists() {
fs::create_dir_all(&p)?;
}
}
let mut outfile = fs::File::create(&out_path)?;
io::copy(&mut file, &mut outfile)?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&out_path, fs::Permissions::from_mode(mode))?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use async_std::task;
use quake_core::entry::entry_file::EntryFile;
use quake_core::entry::entry_paths::EntryPaths;
use quake_core::usecases::entry_usecases::sync_in_path;
use crate::{process_cmd, Command, Init, Opts, SubCommand};
#[test]
fn should_throw_not_exist_cmds() {
task::block_on(async {
let command = Command {
config: ".quake.yaml".to_string(),
input: "story.dddd".to_string(),
editor: "".to_string(),
};
let expected = process_cmd(Opts {
cmd: SubCommand::Cmd(command),
})
.await
.expect_err("");
let error_msg = "QuakeError(\"unknown entry action: QuakeActionNode { entry: \\\"story\\\", action: \\\"dddd\\\", text: \\\"\\\", parameters: [] }\")";
assert_eq!(format!("{:?}", expected), error_msg);
});
}
#[test]
fn should_create_test_entry() {
task::block_on(async {
let test_dir = "test_dir";
let command = Command {
config: format!("{:}", config_dir().display()),
input: "water.add: samples".to_string(),
editor: "".to_string(),
};
process_cmd(Opts {
cmd: SubCommand::Init(Init {
path: test_dir.to_string(),
download: false,
}),
})
.await
.unwrap();
process_cmd(Opts {
cmd: SubCommand::Cmd(command),
})
.await
.unwrap();
let paths = EntryPaths::init(
&format!("{:}", PathBuf::from(test_dir).display()),
&"water".to_string(),
);
let content = fs::read_to_string(paths.entry_path.join("0001-samples.md")).unwrap();
let file = EntryFile::from(content.as_str(), 1).unwrap();
let title = file.property("title");
assert_eq!(title.unwrap(), "samples");
fs::remove_dir_all(test_dir).unwrap();
});
}
#[test]
#[ignore = "need tokio runtime"]
fn should_download_webapp_dist() {
task::block_on(async {
let test_dir = "test_dir";
process_cmd(Opts {
cmd: SubCommand::Init(Init {
path: test_dir.to_string(),
download: true,
}),
})
.await
.unwrap();
})
}
fn config_dir() -> PathBuf {
PathBuf::from("_fixtures")
.join("configs")
.join(".quake.yaml")
}
#[ignore]
#[test]
fn placeholder() {
let paths = EntryPaths::init(&"examples".to_string(), &"notes".to_string());
sync_in_path(&paths).unwrap();
let paths = EntryPaths::init(&"examples".to_string(), &"blog".to_string());
sync_in_path(&paths).unwrap();
}
#[ignore]
#[test]
fn sync_todo() {
let paths = EntryPaths::init(&"examples".to_string(), &"microsoft_todo".to_string());
sync_in_path(&paths).unwrap();
}
}