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