1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
mod ai;
mod assemble;
mod audiobook;
mod backup;
mod cli;
mod config;
mod config_tui;
mod continuity;
mod continuity_bible;
mod crash;
mod disk;
mod facts_scan;
mod health;
mod io_atomic;
mod path_safety;
mod pdf;
mod prompts_tui;
mod echo;
mod epub;
mod error;
mod export;
mod git;
mod grammar;
mod language_entry;
mod book_digest;
mod manuscript;
mod progress;
mod replace;
mod submission_gen;
mod submissions;
mod project;
mod scripting;
mod scrivener;
mod storage;
mod story_view;
mod store;
mod tension;
mod text;
mod timeline;
mod tui;
mod typst_check;
mod typst_compile;
mod typst_inprocess;
mod typst_paragraph_render;
mod typst_prose;
mod typst_world;
use clap::Parser;
fn main() {
// Install the crash-report panic hook before
// anything else. Catches panics in CLI
// subcommands, TUI startup, runtime init — every
// code path. TUI later registers its terminal-
// restore closure via crash::set_terminal_restore.
crash::install_panic_hook();
let cli = cli::Cli::parse();
// Tracing routing depends on the subcommand. TUI sessions must NOT
// write to stderr — any log line printed mid-frame corrupts ratatui's
// back-buffer (we'd see ghost panes or stray text inside the rendered
// grid). Route TUI logs to a per-project file and keep CLI logs on
// stderr where they're useful.
// `Command::Config` and `Command::PromptsEditor` are
// also TUIs (standalone HJSON / prompts editors) —
// same stderr-quiet logging requirement as the main
// editor.
let is_tui = matches!(
&cli.command,
None | Some(cli::Command::Tui)
| Some(cli::Command::Config)
| Some(cli::Command::PromptsEditor)
);
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("inkhaven=info,warn"));
if is_tui {
let log_path = tui_log_path(cli.project.as_deref());
// Best-effort file open; fall back to stderr-less /dev/null if the
// path can't be created (read-only project dir, full disk, etc.) —
// logs are diagnostic, not load-bearing.
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.ok();
if let Some(file) = file {
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_ansi(false)
.with_writer(std::sync::Mutex::new(file))
.init();
} else {
// Last resort: drop logs entirely. We don't want stderr writes
// bleeding into the TUI.
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::sink)
.init();
}
} else {
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
}
let rt = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
eprintln!("inkhaven: could not start tokio runtime: {e}");
std::process::exit(1);
}
};
let _guard = rt.enter();
match cli.run() {
Ok(()) => {}
Err(e) => {
// anyhow's `{:#}` chains causes without showing the backtrace.
eprintln!("inkhaven: {e:#}");
std::process::exit(1);
}
}
}
/// Where to write TUI session logs. Lives inside the project directory so
/// it's tied to the work being edited and easy to gitignore. Falls back to
/// the system temp dir if no `--project` was passed (the TUI will still try
/// to open `.` and may succeed).
fn tui_log_path(project: Option<&std::path::Path>) -> std::path::PathBuf {
match project {
Some(p) => p.join(".inkhaven.log"),
None => std::env::current_dir()
.unwrap_or_else(|_| std::env::temp_dir())
.join(".inkhaven.log"),
}
}