Krolyxon
Commit a44bdf7

Branches

Tags

Refs →

remagesrcmain.rs

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