1use std::{
2 backtrace::Backtrace,
3 env,
4 error::Error,
5 io,
6 panic,
7 path::PathBuf,
8 sync::{Arc, atomic},
9};
10
11use chrono::{Datelike, Timelike};
12use thiserror::Error;
13use tokio::{fs, runtime};
14use tracing::level_filters::LevelFilter;
15use tracing_subscriber::{
16 EnvFilter,
17 Layer,
18 filter::FromEnvError,
19 layer::SubscriberExt,
20 util::{SubscriberInitExt, TryInitError},
21};
22
23const LOG_ENABLED_ENV_VAR: &'static str = "THEDES_LOG";
24const LOG_LEVEL_ENV_VAR: &'static str = "THEDES_LOG_LEVEL";
25const LOG_PATH_ENV_VAR: &'static str = "THEDES_LOG_PATH";
26
27const THREAD_STACK_SIZE: usize = 4 * 1024 * 1024;
28
29#[derive(Debug, Error)]
30enum ProgramError {
31 #[error("Failed to open log file {}", .path.display())]
32 OpenLogFile {
33 path: PathBuf,
34 #[source]
35 cause: io::Error,
36 },
37 #[error("Failed to get log filter")]
38 LogFilter(
39 #[source]
40 #[from]
41 FromEnvError,
42 ),
43 #[error("Failed to init logger")]
44 LogInit(
45 #[source]
46 #[from]
47 TryInitError,
48 ),
49 #[error("Failed to start asynchronous runtime")]
50 AsyncRuntime(#[source] io::Error),
51 #[error(transparent)]
52 TuiApp(#[from] thedes_app::Error),
53 #[error(transparent)]
54 TuiRuntime(#[from] thedes_tui::core::runtime::Error),
55 #[error("Failed to create saves directory {path}")]
56 CreateSavesDir {
57 path: PathBuf,
58 #[source]
59 source: io::Error,
60 },
61 #[error("Failed to create settings directory {path}")]
62 CreateSettingsDir {
63 path: PathBuf,
64 #[source]
65 source: io::Error,
66 },
67}
68
69async fn async_runtime_main() -> Result<(), ProgramError> {
70 let config = thedes_tui::core::runtime::Config::new();
71 let app_config = match directories::ProjectDirs::from(
72 "io.github",
73 "brunoczim",
74 "Thedes",
75 ) {
76 Some(dirs) => {
77 let mut saves_dir =
78 dirs.state_dir().unwrap_or(dirs.data_dir()).to_owned();
79 saves_dir.push("saves");
80 fs::create_dir_all(&saves_dir).await.map_err(|source| {
81 ProgramError::CreateSavesDir { source, path: saves_dir.clone() }
82 })?;
83
84 let mut settings_path =
85 dirs.state_dir().unwrap_or(dirs.data_dir()).to_owned();
86 fs::create_dir_all(&settings_path).await.map_err(|source| {
87 ProgramError::CreateSettingsDir {
88 source,
89 path: settings_path.clone(),
90 }
91 })?;
92 settings_path.push("thedes-settings.json");
93
94 thedes_app::Config::new()
95 .with_saves_dir(saves_dir)
96 .with_settings_path(settings_path)
97 },
98 None => thedes_app::Config::new(),
99 };
100
101 let runtime_future = config.run(|app| app_config.run(app));
102 runtime_future.await??;
103 Ok(())
104}
105
106fn get_project_dirs() -> Option<directories::ProjectDirs> {
107 directories::ProjectDirs::from("io.github", "brunoczim", "Thedes")
108}
109
110fn setup_logger() -> Result<(), ProgramError> {
111 let mut options = std::fs::OpenOptions::new();
112
113 options.write(true).append(true).create(true).truncate(false);
114
115 let path = match env::var_os(LOG_PATH_ENV_VAR) {
116 Some(path) => path.into(),
117 None => {
118 let now = chrono::Local::now();
119 let stem = format!(
120 "log_{:04}-{:02}-{:02}_{:02}-{:02}-{:02}.txt",
121 now.year(),
122 now.month(),
123 now.day(),
124 now.hour(),
125 now.minute(),
126 now.second(),
127 );
128 match get_project_dirs() {
129 Some(dirs) => dirs.cache_dir().join(stem),
130 None => stem.into(),
131 }
132 },
133 };
134
135 if let Some(dir) = path.parent() {
136 std::fs::create_dir_all(&dir).map_err(|cause| {
137 ProgramError::OpenLogFile { path: path.clone(), cause }
138 })?;
139 }
140
141 let file = options
142 .open(&path)
143 .map_err(|cause| ProgramError::OpenLogFile { path, cause })?;
144
145 tracing_subscriber::registry()
146 .with(
147 tracing_subscriber::fmt::layer()
148 .with_writer(Arc::new(file))
149 .with_filter(
150 EnvFilter::builder()
151 .with_default_directive(LevelFilter::INFO.into())
152 .with_env_var(LOG_LEVEL_ENV_VAR)
153 .from_env()?,
154 ),
155 )
156 .try_init()?;
157
158 Ok(())
159}
160
161fn setup_panic_handler() {
162 panic::set_hook(Box::new(|info| {
163 tracing::error!("{}\n", info);
164 let backtrace = Backtrace::capture();
165 tracing::error!("backtrace:\n{}\n", backtrace);
166 eprintln!("{}", info);
167 eprintln!("backtrace:\n{}\n", backtrace);
168 }));
169}
170
171fn try_main() -> Result<(), ProgramError> {
172 setup_panic_handler();
173
174 let mut log_enabled = false;
175 if env::var_os(LOG_LEVEL_ENV_VAR).is_some()
176 || env::var_os(LOG_PATH_ENV_VAR).is_some()
177 {
178 log_enabled = true;
179 }
180 if let Some(log_enabled_var) = env::var_os(LOG_ENABLED_ENV_VAR) {
181 if log_enabled_var.eq_ignore_ascii_case("true")
182 || log_enabled_var.eq_ignore_ascii_case("t")
183 || log_enabled_var.eq_ignore_ascii_case("on")
184 || log_enabled_var.eq_ignore_ascii_case("yes")
185 || log_enabled_var.eq_ignore_ascii_case("y")
186 || log_enabled_var == "1"
187 {
188 log_enabled = true;
189 } else if log_enabled_var.eq_ignore_ascii_case("false")
190 || log_enabled_var.eq_ignore_ascii_case("f")
191 || log_enabled_var.eq_ignore_ascii_case("off")
192 || log_enabled_var.eq_ignore_ascii_case("no")
193 || log_enabled_var.eq_ignore_ascii_case("n")
194 || log_enabled_var == "0"
195 {
196 log_enabled = false;
197 }
198 }
199
200 if log_enabled {
201 setup_logger()?;
202 }
203
204 let runtime = runtime::Builder::new_multi_thread()
205 .enable_time()
206 .thread_stack_size(THREAD_STACK_SIZE)
207 .build()
208 .map_err(ProgramError::AsyncRuntime)?;
209
210 runtime.block_on(async_runtime_main())
211}
212
213fn main() {
214 unsafe {
215 env::set_var("RUST_BACKTRACE", "1");
216 atomic::fence(atomic::Ordering::SeqCst);
217 }
218
219 if let Err(error) = try_main() {
220 eprintln!("thedes found a fatal error!");
221 let mut current_error = Some(&error as &dyn Error);
222 while let Some(error) = current_error {
223 eprintln!("caused by:");
224 eprintln!(" {error}");
225 current_error = error.source();
226 }
227 }
228}