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
// On wasm32 the bin target is meaningless — the browser entry point is the
// `#[wasm_bindgen(start)]` function inside `wasm_app.rs`, exposed via the
// crate's cdylib output. We still need a `main` symbol so `cargo build
// --bin cuqueclicker --target wasm32-unknown-unknown` (which trunk runs
// alongside the cdylib build) links cleanly.
#[cfg(target_arch = "wasm32")]
fn main() {}
#[cfg(not(target_arch = "wasm32"))]
fn main() -> anyhow::Result<()> {
native::main()
}
#[cfg(not(target_arch = "wasm32"))]
mod native {
use anyhow::Result;
use clap::{Parser, Subcommand};
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use cuqueclicker_lib::{
app::{App, build_demo_state},
build_info, i18n,
platform::{InstanceLock, Persistence},
self_cmd,
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io;
#[derive(Parser)]
#[command(
name = "cuqueclicker",
version,
about = "A TUI idle clicker where you finger an ASCII ass instead of clicking a cookie."
)]
struct Cli {
/// Disable debug mode — hides the overlay and disables the F-key
/// cheats (F1-F4). No effect in release builds, debug mode is never
/// active there regardless of this flag.
#[arg(long)]
no_debug: bool,
/// Start a self-playing demo on a pre-loaded rich state for
/// asciinema/SVG capture, optionally taking a duration in seconds.
/// Example: `--demo-for-recording 45`. Default 30s. Ignored in release
/// builds. Hidden from help.
#[arg(long, num_args = 0..=1, default_missing_value = "30", hide = true, value_name = "SECONDS")]
demo_for_recording: Option<u32>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
/// Manage this CuqueClicker installation.
#[command(name = "self", subcommand)]
Self_(SelfAction),
}
#[derive(Subcommand)]
enum SelfAction {
/// Update the binary to the latest released version.
Update,
}
pub fn main() -> Result<()> {
let cli = Cli::parse();
// Subcommands run in a plain stdio shell context — no terminal setup,
// no save lock, no game loop.
if let Some(Command::Self_(action)) = &cli.command {
return match action {
SelfAction::Update => self_cmd::update(),
};
}
install_panic_hook();
i18n::init();
// Debug mode is on by default in dev builds, opt-out via --no-debug.
// Release builds never show debug affordances, regardless of the flag.
let debug = build_info::is_dev_build() && !cli.no_debug;
// Demo mode (dev only) runs on an ephemeral rich state for asciinema
// recording. It never acquires the save lock, never reads the user's
// save, and never writes anything back — launching alongside a live
// game session is safe.
let demo_seconds = cli
.demo_for_recording
.filter(|_| build_info::is_dev_build());
let _lock;
let persistence = Persistence::new();
let state = if demo_seconds.is_some() {
build_demo_state()
} else {
_lock = match InstanceLock::try_acquire() {
Ok(l) => l,
Err(e) => {
eprintln!(
"CuqueClicker is already running (or the save directory is not writable)."
);
eprintln!("Close the other instance and try again.");
eprintln!("Details: {e}");
std::process::exit(1);
}
};
persistence.load()
};
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = App::new(state, debug, demo_seconds, persistence).run(&mut terminal);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
result
}
fn install_panic_hook() {
let original = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture);
original(info);
}));
}
}