Krolyxon
Commit 8b10288

Branches

Tags

Refs →

remagesrcmain.rs

248 lines · 7.5 KB · Rust

1//! remage: a keyboard-centric image viewer.
2
3mod action;
4mod anim;
5mod app;
6mod cache;
7mod cli;
8mod config;
9mod decode;
10mod exec;
11mod input;
12mod ipc;
13mod man;
14mod render;
15mod scan;
16mod state;
17mod thumbs;
18mod ui;
19mod watch;
20mod worker;
21
22use std::ffi::OsString;
23use std::io::{Read, Write};
24use std::os::unix::ffi::{OsStrExt, OsStringExt};
25use std::path::{Path, PathBuf};
26use std::time::Instant;
27
28use clap::Parser;
29use winit::event_loop::EventLoop;
30
31use app::{App, Options, Overrides, UserEvent};
32use state::queue::Queue;
33use state::{AppState, MsgKind, Settings};
34
35fn main() -> anyhow::Result<()> {
36 let started = Instant::now();
37 env_logger::init();
38 let cli = cli::Cli::parse();
39 match cli.command {
40 Some(cli::Sub::Msg {
41 pid,
42 query,
43 subscribe,
44 action,
45 }) => {
46 std::process::exit(ipc::client(ipc::ClientArgs {
47 pid,
48 query,
49 subscribe,
50 action,
51 }));
52 }
53 Some(cli::Sub::Completions { shell }) => {
54 let mut cmd = <cli::Cli as clap::CommandFactory>::command();
55 clap_complete::generate(shell, &mut cmd, "remage", &mut std::io::stdout());
56 return Ok(());
57 }
58 Some(cli::Sub::Man) => {
59 man::render(&mut std::io::stdout().lock())?;
60 return Ok(());
61 }
62 None => {}
63 }
64 if cli.dump_config {
65 print!("{}", config::dump());
66 return Ok(());
67 }
68 if cli.check_config {
69 check_config(cli.config.as_deref());
70 }
71 log::info!("remage {}", env!("CARGO_PKG_VERSION"));
72
73 let (loaded, config_error) = match config::load(cli.config.as_deref()) {
74 Ok(l) => (l, None),
75 Err(e) => {
76 log::error!("{e}");
77 let mut d = config::defaults();
78 d.watch = e.watch;
79 (d, Some(e.message))
80 }
81 };
82 for w in &loaded.warnings {
83 log::warn!("{w}");
84 }
85 log::debug!("config loaded after {:.1?}", started.elapsed());
86 let cfg = &loaded.config;
87 decode::set_decoders(&cfg.decoders);
88
89 let mut paths = cli.paths.clone();
90 if cli.stdin {
91 paths.extend(read_stdin_paths(cli.null)?);
92 }
93 let scan = if cli.stdin && paths.is_empty() {
94 // An empty list on stdin opens nothing, not the current directory.
95 scan::Scan::default()
96 } else {
97 scan::scan(
98 &paths,
99 &scan::ScanOptions {
100 recursive: cli.recursive || cfg.general.recursive,
101 // A list of files is just those files.
102 siblings: !cli.stdin && !cli.no_siblings && cfg.general.open_siblings,
103 sort: cli.sort.unwrap_or(cfg.general.sort),
104 reverse: cli.reverse || cfg.general.reverse,
105 },
106 )
107 };
108 log::debug!(
109 "scanned {} files ({} to sniff) in {:.1?}",
110 scan.files.len(),
111 scan.unknown.len(),
112 started.elapsed()
113 );
114
115 let overrides = Overrides {
116 fit: cli.fit,
117 zoom: cli.zoom.map(|z| z / 100.0),
118 bar: cli.no_bar.then_some(false),
119 slideshow_delay: cli
120 .slideshow
121 .flatten()
122 .filter(|s| s.is_finite() && *s > 0.0)
123 .map(std::time::Duration::from_secs_f32),
124 };
125 let mut settings = Settings::from_config(cfg);
126 overrides.apply(&mut settings);
127 let start_index = cli.start.unwrap_or(1).saturating_sub(1);
128 let queue = Queue::new(scan.files, scan.start.as_deref(), start_index);
129 let bar = overrides.bar.unwrap_or(cfg.ui.statusbar);
130 let mut state = AppState::new(queue, settings, bar, Instant::now());
131 if cli.grid || cfg.general.start_mode == config::StartMode::Grid {
132 state.set_mode(state::Mode::Grid, Instant::now());
133 }
134 state.theme = loaded.theme.clone();
135 state.nearest = cfg.image.filter.nearest();
136 state.alpha_bg = cfg.image.alpha_background;
137 if cli.slideshow.is_some() {
138 state.apply(
139 action::Action::Slideshow(action::Switch::On),
140 Instant::now(),
141 );
142 state.message = None;
143 }
144 if let Some(err) = scan.errors.first() {
145 state.notify(format!("error: {err}"), MsgKind::Error, Instant::now());
146 }
147
148 let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
149 let proxy = event_loop.create_proxy();
150 if !scan.unknown.is_empty() {
151 let proxy = proxy.clone();
152 let unknown = scan.unknown;
153 std::thread::spawn(move || {
154 let found: Vec<_> = unknown.into_iter().filter(|p| decode::sniff(p)).collect();
155 if !found.is_empty() {
156 let _ = proxy.send_event(UserEvent::Found(found));
157 }
158 });
159 }
160 let server = {
161 let proxy = proxy.clone();
162 let proxy = std::sync::Mutex::new(proxy);
163 match ipc::serve(move |inc| {
164 let proxy = proxy.lock().unwrap_or_else(|e| e.into_inner());
165 proxy.send_event(UserEvent::Ipc(inc)).is_ok()
166 }) {
167 Ok(s) => {
168 log::debug!("IPC socket {}", s.path().display());
169 Some(s)
170 }
171 Err(e) => {
172 log::warn!("IPC socket: {e}");
173 None
174 }
175 }
176 };
177 let watch = {
178 let proxy = proxy.clone();
179 config::watch::spawn(cli.config.clone(), loaded.watch.clone(), move |r| {
180 let _ = proxy.send_event(UserEvent::Config(Box::new(r)));
181 })
182 };
183
184 let opts = Options {
185 fullscreen: cli.fullscreen || cfg.window.fullscreen,
186 geometry: cli.geometry,
187 class: cli.class.unwrap_or_else(|| cfg.window.class.clone()),
188 overrides,
189 private: cli.private,
190 socket: server.as_ref().map(|s| s.path().to_path_buf()),
191 dirs: scan.dirs,
192 };
193 let mut app = App::new(opts, state, proxy, loaded, watch);
194 if let Some(e) = config_error {
195 app.config_failed(&e);
196 }
197 event_loop.run_app(&mut app)?;
198 if cli.print_marked
199 && let Some(marked) = app.marked_output()
200 {
201 let sep = if cli.null { b'\0' } else { b'\n' };
202 let mut out = std::io::stdout().lock();
203 for p in marked {
204 out.write_all(p.as_os_str().as_bytes())?;
205 out.write_all(&[sep])?;
206 }
207 out.flush()?;
208 }
209 // Lets in-flight IPC replies (to a `quit`, say) go out, then removes the socket.
210 drop(server);
211 match app.error.take() {
212 Some(e) => Err(e),
213 None => Ok(()),
214 }
215}
216
217/// `-i`: paths from stdin, one per line (NUL-separated with `-0`).
218fn read_stdin_paths(null: bool) -> anyhow::Result<Vec<PathBuf>> {
219 let mut buf = Vec::new();
220 std::io::stdin().lock().read_to_end(&mut buf)?;
221 let sep = if null { 0 } else { b'\n' };
222 Ok(buf
223 .split(|b| *b == sep)
224 .filter(|l| !l.is_empty())
225 .map(|l| PathBuf::from(OsString::from_vec(l.to_vec())))
226 .collect())
227}
228
229/// `--check-config`: report and exit, non-zero on errors.
230fn check_config(path: Option<&Path>) -> ! {
231 match config::load(path) {
232 Ok(l) => {
233 for w in &l.warnings {
234 eprintln!("warning: {w}");
235 }
236 match path.map(Path::to_path_buf).or_else(config::default_path) {
237 Some(p) if p.is_file() => println!("{}: ok", p.display()),
238 Some(p) => println!("{} not found: using the defaults", p.display()),
239 None => println!("no config directory: using the defaults"),
240 }
241 std::process::exit(0)
242 }
243 Err(e) => {
244 eprintln!("{e}");
245 std::process::exit(1)
246 }
247 }
248}