1mod helpers;
2mod index;
3mod project;
4mod task;
5mod views;
6
7use std::path::Path;
8use std::sync::Arc;
9
10use anyhow::Result;
11use axum::response::IntoResponse;
12use axum::routing::{get, post};
13use axum::Router;
14
15use crate::db;
16
17#[derive(Clone)]
18struct AppState {
19 data_root: Arc<std::path::PathBuf>,
20}
21
22async fn static_oat_css() -> impl IntoResponse {
23 (
24 [(axum::http::header::CONTENT_TYPE, "text/css; charset=utf-8")],
25 include_bytes!("../../../static/oat.min.css").as_slice(),
26 )
27}
28
29async fn static_td_css() -> impl IntoResponse {
30 (
31 [(axum::http::header::CONTENT_TYPE, "text/css; charset=utf-8")],
32 include_bytes!("../../../static/td.css").as_slice(),
33 )
34}
35
36async fn static_js() -> impl IntoResponse {
37 (
38 [(
39 axum::http::header::CONTENT_TYPE,
40 "application/javascript; charset=utf-8",
41 )],
42 include_bytes!("../../../static/oat.min.js").as_slice(),
43 )
44}
45
46async fn static_td_js() -> impl IntoResponse {
47 (
48 [(
49 axum::http::header::CONTENT_TYPE,
50 "application/javascript; charset=utf-8",
51 )],
52 include_bytes!("../../../static/td.js").as_slice(),
53 )
54}
55
56pub fn run(cwd: &Path, host: &str, port: u16, explicit_project: Option<&str>) -> Result<()> {
57 let data_root = db::data_root()?;
58 let state = AppState {
59 data_root: Arc::new(data_root),
60 };
61
62 let app = Router::new()
63 .route("/", get(index::index_handler))
64 .route(
65 "/projects",
66 post(project::mutations::create_project_handler),
67 )
68 .route("/projects/{name}", get(project::project_handler))
69 .route(
70 "/projects/{name}/tasks",
71 post(project::mutations::create_handler),
72 )
73 .route(
74 "/projects/{name}/tasks/{id}",
75 get(task::task_handler).post(project::mutations::update_handler),
76 )
77 .route(
78 "/projects/{name}/tasks/{id}/log",
79 post(project::mutations::log_handler),
80 )
81 .route(
82 "/projects/{name}/tasks/{id}/done",
83 post(project::mutations::done_handler),
84 )
85 .route(
86 "/projects/{name}/tasks/{id}/reopen",
87 post(project::mutations::reopen_handler),
88 )
89 .route(
90 "/projects/{name}/tasks/{id}/labels",
91 post(project::mutations::label_handler),
92 )
93 .route(
94 "/projects/{name}/tasks/{id}/deps",
95 post(project::mutations::dep_handler),
96 )
97 .route(
98 "/projects/{name}/tasks/{id}/delete",
99 post(project::mutations::delete_handler),
100 )
101 .route("/static/oat.min.css", get(static_oat_css))
102 .route("/static/td.css", get(static_td_css))
103 .route("/static/oat.min.js", get(static_js))
104 .route("/static/td.js", get(static_td_js))
105 .with_state(state);
106
107 let addr = format!("{host}:{port}");
108 let root_url = format!("http://{addr}");
109
110 let project_url = match explicit_project {
111 Some(p) => Some(format!("{root_url}/projects/{p}")),
112 None => db::try_open(cwd)
113 .ok()
114 .flatten()
115 .map(|s| format!("{root_url}/projects/{}", s.project_name())),
116 };
117
118 eprintln!("listening on {root_url}");
119 if let Some(ref url) = project_url {
120 eprintln!("project: {url}");
121 }
122
123 tokio::runtime::Builder::new_multi_thread()
124 .enable_all()
125 .build()?
126 .block_on(async {
127 let listener = tokio::net::TcpListener::bind(&addr).await?;
128 axum::serve(listener, app).await?;
129 Ok(())
130 })
131}