Skip to main content

thedes_bin/
main.rs

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}