Krolyxon
Commit 4654f72

Branches

Tags

Refs →

remagesrcapp.rs

2326 lines · 85.3 KB · Rust

1//! winit `ApplicationHandler`: routes events, owns the state and the renderer.
2
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, Instant, SystemTime};
7
8use serde_json::{Value, json};
9
10use winit::application::ApplicationHandler;
11use winit::dpi::LogicalSize;
12use winit::event::{ElementState, MouseButton, MouseScrollDelta, StartCause, WindowEvent};
13use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
14use winit::keyboard::{ModifiersState, PhysicalKey};
15use winit::window::{Fullscreen, Window, WindowId};
16
17use crate::action::{Action, CATALOG, ModeArg, Switch, Zoom};
18use crate::cache::ImageCache;
19use crate::config::watch::ConfigWatch;
20use crate::config::{Config, LoadError, Loaded, Output, Targets, suggest};
21use crate::decode::{self, DecodedImage};
22use crate::exec;
23use crate::input::keymap::{KeyTables, Keymap};
24use crate::input::keys::{Code, Key, KeySeq, Named, Scroll};
25use crate::input::{self};
26use crate::ipc::{self, Query, Request, Topic};
27use crate::render::{Renderer, text};
28use crate::scan;
29use crate::state::overlay::{Edit, Help, HelpRow, Info, InputKind, PaletteItem, WhichRow};
30use crate::state::queue::Queue;
31use crate::state::view::{Dir, FitMode};
32use crate::state::{AppState, Effects, Mode, MsgKind, Settings};
33use crate::thumbs::{self, CacheOptions, Thumb, ThumbError};
34use crate::ui;
35use crate::watch::{self, DirWatch};
36use crate::worker::{JobKey, Pool, Priority};
37
38/// GPU upload budget per event-loop turn, so preloads never cause a hitch.
39const UPLOAD_BUDGET: usize = 32 << 20;
40/// Show the window once the first image is ready, or after this long regardless.
41const SHOW_TIMEOUT: Duration = Duration::from_millis(300);
42/// Wheel pixels per zoom step for touchpads.
43const PIXELS_PER_STEP: f32 = 60.0;
44/// An ambiguous key sequence (`g` bound, and `gg` too) resolves after this long, like vim's
45/// `timeoutlen`.
46const SEQUENCE_TIMEOUT: Duration = Duration::from_millis(1000);
47/// A press and release closer than this is a click, not a drag.
48const CLICK_SLOP: f32 = 4.0;
49/// Config errors stay on screen until fixed (or dismissed with `<Esc>`).
50const CONFIG_ERROR_TIME: Duration = Duration::from_secs(3600);
51/// In the grid, the selected image decodes once the cursor has rested this long, so `<CR>`
52/// opens it at once without decoding everything the cursor passes over.
53const GRID_REST: Duration = Duration::from_millis(250);
54/// A scalable image is re-rendered for the zoom once the view has rested this long.
55const REFINE_DELAY: Duration = Duration::from_millis(150);
56/// Two clicks on the same thumbnail this close together open it.
57const DOUBLE_CLICK: Duration = Duration::from_millis(400);
58
59pub enum UserEvent {
60 Decoded {
61 path: PathBuf,
62 /// The file's mtime when decoding started, for `refresh`.
63 mtime: Option<SystemTime>,
64 result: Result<Arc<DecodedImage>, String>,
65 },
66 /// Unknown-extension files that turned out to be images.
67 Found(Vec<PathBuf>),
68 /// Embedded plus system fonts, loaded off-thread after the first frame.
69 Fonts(glyphon::fontdb::Database),
70 /// The config was reloaded (file change, SIGUSR1 or `reload-config`).
71 Config(Box<Result<Loaded, LoadError>>),
72 /// A re-sort finished: the queue in its new order.
73 Sorted(Vec<PathBuf>),
74 /// A thumbnail for cells of `px` pixels.
75 Thumb {
76 path: PathBuf,
77 px: u32,
78 mtime: Option<SystemTime>,
79 result: Result<Thumb, ThumbError>,
80 },
81 /// A sharper version of an image (SVG at the current zoom, a developed RAW).
82 Refined {
83 path: PathBuf,
84 result: Result<Arc<DecodedImage>, String>,
85 },
86 /// Output or the exit of a user command.
87 Command { id: u64, event: exec::Event },
88 /// `refresh` re-checked these files: their mtime, or `None` if they are gone.
89 Refreshed(Vec<(PathBuf, Option<SystemTime>)>),
90 /// A request on the IPC socket.
91 Ipc(ipc::Incoming),
92 /// Metadata for the info panel.
93 Info(Info),
94 /// The last line `ui.info_command` printed for a file.
95 InfoLine { path: PathBuf, line: String },
96 /// Files appeared, changed or vanished in a watched directory.
97 Dir(watch::Changes),
98 /// `open <path>` finished scanning.
99 Opened { arg: PathBuf, scan: scan::Scan },
100}
101
102/// Command-line settings that beat the config, across reloads too.
103#[derive(Clone, Debug, Default)]
104pub struct Overrides {
105 pub fit: Option<FitMode>,
106 pub zoom: Option<f32>,
107 pub bar: Option<bool>,
108 pub slideshow_delay: Option<Duration>,
109}
110
111impl Overrides {
112 pub fn apply(&self, s: &mut Settings) {
113 if let Some(d) = self.slideshow_delay {
114 s.slideshow_delay = d;
115 }
116 if let Some(fit) = self.fit {
117 s.fit = fit;
118 }
119 if self.zoom.is_some() {
120 s.zoom = self.zoom;
121 }
122 }
123}
124
125pub struct Options {
126 pub fullscreen: bool,
127 pub geometry: Option<(u32, u32)>,
128 pub class: String,
129 pub overrides: Overrides,
130 /// Never write the shared thumbnail cache (`-p`).
131 pub private: bool,
132 /// The IPC socket, passed to commands as `$REMAGE_SOCKET`.
133 pub socket: Option<PathBuf>,
134 /// Directories the queue came from, for `watch_dir`.
135 pub dirs: Vec<(PathBuf, bool)>,
136}
137
138/// Where a chain of actions came from, which decides what some of them act on.
139#[derive(Clone, Debug, Default)]
140struct Ctx {
141 /// The cursor, for mouse-triggered zoom.
142 anchor: Option<[f32; 2]>,
143 /// A command's targets, which its `refresh` re-checks (otherwise the whole queue).
144 targets: Option<Arc<[PathBuf]>>,
145}
146
147/// A user command from spawn to exit.
148struct Job {
149 name: String,
150 output: Output,
151 targets: Arc<[PathBuf]>,
152 /// The command's `after` actions, then whatever followed `run` in its binding: they wait
153 /// for a successful exit.
154 then: Vec<Action>,
155 /// stdout lines run as actions so far.
156 lines: usize,
157}
158
159/// An IPC connection streaming events.
160struct Subscriber {
161 topics: Vec<Topic>,
162 tx: crossbeam_channel::Sender<String>,
163}
164
165/// What subscribers last heard about.
166#[derive(Clone, Debug, PartialEq)]
167struct Published {
168 image: (Option<PathBuf>, usize, usize),
169 marks: u64,
170 mode: Mode,
171}
172
173/// Thumbnail jobs for the current cell size.
174#[derive(Default)]
175struct ThumbJobs {
176 /// Cell size the atlas and requests are for (0 = none yet).
177 cell: u32,
178 requested: HashMap<PathBuf, Priority>,
179 /// Paths near the view; the atlas never evicts these.
180 wanted: HashSet<PathBuf>,
181 /// Finished thumbnails waiting for the atlas.
182 arrivals: VecDeque<(PathBuf, Thumb)>,
183}
184
185struct Gfx {
186 renderer: Renderer,
187 // Dropped after the renderer, whose surface borrows the window.
188 window: Arc<Window>,
189}
190
191pub struct App {
192 opts: Options,
193 state: AppState,
194 config: Config,
195 keymap: Keymap,
196 watch: ConfigWatch,
197 /// A config error is on the status line.
198 config_error: bool,
199 /// `reload-config` ran, so the next reload is confirmed on screen.
200 reload_requested: bool,
201 /// When the pending key sequence became ambiguous.
202 ambiguous_since: Option<Instant>,
203 /// Actions a held key repeats.
204 repeat: Option<(PhysicalKey, Vec<Action>)>,
205 pool: Pool,
206 proxy: EventLoopProxy<UserEvent>,
207 cache: ImageCache,
208 /// Decoded images whose result event is still in the loop's queue.
209 arriving: Arc<Mutex<HashSet<PathBuf>>>,
210 gfx: Option<Gfx>,
211 uploads: VecDeque<(PathBuf, Arc<DecodedImage>)>,
212 thumbs: ThumbJobs,
213 thumb_cache: CacheOptions,
214 /// When the grid cursor last moved, until the selected image is requested.
215 grid_rest: Option<Instant>,
216 /// The last click in the grid, for double-clicks.
217 last_click: Option<(Instant, usize)>,
218 modifiers: ModifiersState,
219 cursor: [f32; 2],
220 dragging: bool,
221 /// Where the left button went down, while it is a click candidate.
222 press: Option<[f32; 2]>,
223 /// Wheel motion not yet turned into whole `<ScrollUp>`/`<ScrollDown>` presses.
224 scroll: [f32; 2],
225 held: HashMap<PhysicalKey, Dir>,
226 started: Instant,
227 shown_window: bool,
228 /// File mtimes when they were last decoded or thumbnailed, so `refresh` spots changes.
229 stamps: HashMap<PathBuf, SystemTime>,
230 jobs: HashMap<u64, Job>,
231 next_job: u64,
232 /// A command waiting for `y` at its confirm prompt.
233 confirm: Option<(Job, exec::Prepared)>,
234 subscribers: Vec<Subscriber>,
235 published: Option<Published>,
236 /// A refinement waiting for the view to rest: (path, target size, since).
237 refine_wait: Option<(PathBuf, u32, Instant)>,
238 /// The image being refined.
239 refining: Option<PathBuf>,
240 /// Refinements that failed; not retried.
241 refine_failed: HashSet<PathBuf>,
242 /// `quit-no-output` ran: `-o` prints nothing.
243 no_output: bool,
244 /// When the pending key sequence started waiting, for which-key.
245 which_key_since: Option<Instant>,
246 /// Watches the queue's directories while `watch_dir` is on.
247 dir_watch: Option<DirWatch>,
248 /// The file whose metadata is being read for the info panel.
249 info_loading: Option<PathBuf>,
250 /// The file `ui.info_command` last ran for.
251 info_line_for: Option<PathBuf>,
252 pub error: Option<anyhow::Error>,
253}
254
255impl App {
256 pub fn new(
257 opts: Options,
258 state: AppState,
259 proxy: EventLoopProxy<UserEvent>,
260 loaded: Loaded,
261 watch: ConfigWatch,
262 ) -> Self {
263 let cache = ImageCache::new(loaded.config.image.cache_mb << 20);
264 let thumb_cache = CacheOptions::new(loaded.config.grid.thumbnail_cache, opts.private);
265 let mut app = Self {
266 opts,
267 state,
268 keymap: keymap(&loaded.keys),
269 config: loaded.config,
270 watch,
271 config_error: false,
272 reload_requested: false,
273 ambiguous_since: None,
274 repeat: None,
275 pool: Pool::new(),
276 proxy,
277 cache,
278 arriving: Arc::default(),
279 gfx: None,
280 uploads: VecDeque::new(),
281 thumbs: ThumbJobs::default(),
282 thumb_cache,
283 grid_rest: None,
284 last_click: None,
285 modifiers: ModifiersState::empty(),
286 cursor: [0.0, 0.0],
287 dragging: false,
288 press: None,
289 scroll: [0.0, 0.0],
290 held: HashMap::new(),
291 started: Instant::now(),
292 shown_window: false,
293 stamps: HashMap::new(),
294 jobs: HashMap::new(),
295 next_job: 1,
296 confirm: None,
297 subscribers: Vec::new(),
298 published: None,
299 refine_wait: None,
300 refining: None,
301 refine_failed: HashSet::new(),
302 no_output: false,
303 which_key_since: None,
304 dir_watch: None,
305 info_loading: None,
306 info_line_for: None,
307 error: None,
308 };
309 app.set_dir_watch();
310 // Decode the first image while the window and GPU come up.
311 app.sync(true);
312 app
313 }
314
315 fn request_redraw(&self) {
316 if let Some(g) = &self.gfx {
317 g.window.request_redraw();
318 }
319 }
320
321 fn decode(&self, path: &Path, prio: Priority) {
322 if lock(&self.arriving).contains(path) {
323 return;
324 }
325 let proxy = self.proxy.clone();
326 let opts = self.decode_options();
327 let arriving = self.arriving.clone();
328 let path = path.to_path_buf();
329 self.pool
330 .submit(JobKey::Decode(path.clone()), prio, move || {
331 let t = Instant::now();
332 let mtime = mtime(&path);
333 let result = decode::decode(&path, &opts)
334 .map(Arc::new)
335 .map_err(|e| e.to_string());
336 log::debug!("decoded {} in {:.1?}", path.display(), t.elapsed());
337 lock(&arriving).insert(path.clone());
338 let _ = proxy.send_event(UserEvent::Decoded {
339 path,
340 mtime,
341 result,
342 });
343 });
344 }
345
346 fn decode_options(&self) -> decode::Options {
347 let [w, h] = self.state.window();
348 let long = w.max(h) as u32;
349 decode::Options {
350 // Before the window exists, a typical screen.
351 target: if long > 1 { long } else { 2048 },
352 auto_orient: self.config.image.auto_orient,
353 color: self.config.image.color_management,
354 }
355 }
356
357 /// Whether the shown image should be re-rendered sharper for the current zoom. Waits for
358 /// the view to rest; returns when to check again.
359 fn check_refine(&mut self, now: Instant) -> Option<Instant> {
360 if self.state.mode != Mode::Image || self.state.view.is_animating(now) {
361 return None;
362 }
363 let path = self
364 .state
365 .shown
366 .as_ref()
367 .filter(|s| Some(s.path.as_path()) == self.state.queue.current())?
368 .path
369 .clone();
370 if self.refining.is_some() || self.refine_failed.contains(&path) {
371 return None;
372 }
373 let img = self.cache.get(&path)?;
374 let refine = img.refine.clone()?;
375 let source = img.source_size[0].max(img.source_size[1]) as f32;
376 let want = (source * self.state.view.scale()).round() as u32;
377 let Some(target) = refine.wants(img.size[0].max(img.size[1]), want) else {
378 self.refine_wait = None;
379 return None;
380 };
381 match &self.refine_wait {
382 Some((p, t, since)) if *p == path && *t == target => {
383 let at = *since + REFINE_DELAY;
384 if now < at {
385 return Some(at);
386 }
387 }
388 _ => {
389 self.refine_wait = Some((path, target, now));
390 return Some(now + REFINE_DELAY);
391 }
392 }
393 self.refine_wait = None;
394 self.refining = Some(path.clone());
395 log::debug!("refining {} to {target} px", path.display());
396 let proxy = self.proxy.clone();
397 let opts = self.decode_options();
398 self.pool
399 .submit(JobKey::Refine(path.clone()), Priority::Current, move || {
400 let t = Instant::now();
401 let result = refine
402 .run(&path, target, &opts)
403 .map(Arc::new)
404 .map_err(|e| format!("{e:#}"));
405 log::debug!("refined {} in {:.1?}", path.display(), t.elapsed());
406 let _ = proxy.send_event(UserEvent::Refined { path, result });
407 });
408 None
409 }
410
411 fn on_refined(&mut self, path: PathBuf, result: Result<Arc<DecodedImage>, String>) {
412 if self.refining.as_ref() == Some(&path) {
413 self.refining = None;
414 }
415 match result {
416 Ok(img) => {
417 let wanted = self.window_paths();
418 let keep: Vec<&Path> = wanted.iter().map(PathBuf::as_path).collect();
419 self.cache.insert(path.clone(), img.clone(), &keep);
420 let shown = self.state.shown.as_ref().is_some_and(|s| s.path == path);
421 if shown && let Some(g) = &mut self.gfx {
422 // Same source size, so the view stays exactly where it is.
423 g.renderer.upload(&path, &img, usize::MAX);
424 g.window.request_redraw();
425 }
426 }
427 Err(e) => {
428 log::warn!("{}: {e}", path.display());
429 self.refine_failed.insert(path);
430 }
431 }
432 }
433
434 /// The current file and its preload neighbours, current first.
435 fn window_paths(&self) -> Vec<PathBuf> {
436 let q = &self.state.queue;
437 let Some(cur) = q.current() else {
438 return Vec::new();
439 };
440 let mut out = vec![cur.to_path_buf()];
441 for i in q.neighbours(self.state.settings.preload, self.state.settings.wrap) {
442 if let Some(p) = q.get(i) {
443 out.push(p.to_path_buf());
444 }
445 }
446 out
447 }
448
449 /// Make the GPU and decode queue match the current position.
450 fn sync(&mut self, navigated: bool) {
451 let now = Instant::now();
452 if navigated {
453 self.pool.bump_epoch();
454 // A pending refinement was for the image we left (and its job may be cancelled).
455 self.refine_wait = None;
456 self.refining = None;
457 }
458 let wanted = self.window_paths();
459 self.update_title();
460 self.request_info();
461 let Some(cur) = wanted.first().cloned() else {
462 self.state.clear_shown();
463 self.uploads.clear();
464 if let Some(g) = &mut self.gfx {
465 g.renderer.retain(|_| false);
466 }
467 self.request_redraw();
468 return;
469 };
470 if self.state.in_grid() {
471 if navigated || !self.cache_has(&cur) {
472 self.grid_rest = Some(now);
473 }
474 self.sync_thumbs();
475 self.request_redraw();
476 return;
477 }
478
479 let keep = |p: &Path| {
480 wanted.iter().any(|w| w == p)
481 || self.state.shown.as_ref().is_some_and(|s| s.path == p)
482 || self.state.fade.as_ref().is_some_and(|f| f.path == p)
483 };
484 if let Some(g) = &mut self.gfx {
485 g.renderer.retain(keep);
486 }
487 self.uploads.retain(|(p, _)| wanted.contains(p));
488
489 let showing_cur = self.state.shown.as_ref().is_some_and(|s| s.path == cur);
490 if !showing_cur {
491 match self.cache.get(&cur) {
492 Some(img) => self.show(&cur, img, now),
493 None => {
494 self.state.loading_since.get_or_insert(now);
495 self.decode(&cur, Priority::Current);
496 }
497 }
498 }
499 for p in &wanted[1..] {
500 match self.cache.get(p) {
501 Some(img) => {
502 let ready = self.gfx.as_ref().is_some_and(|g| g.renderer.is_ready(p));
503 if !ready && !self.uploads.iter().any(|(u, _)| u == p) {
504 self.uploads.push_back((p.clone(), img));
505 }
506 }
507 None => self.decode(p, Priority::Preload),
508 }
509 }
510 self.request_redraw();
511 }
512
513 fn cache_has(&mut self, path: &Path) -> bool {
514 self.cache.get(path).is_some()
515 }
516
517 /// Request thumbnails around the grid view: visible ones first, then a page above and
518 /// two below. Requests that scrolled out of range are cancelled.
519 fn sync_thumbs(&mut self) {
520 if !self.state.in_grid() {
521 return;
522 }
523 let Some(g) = &mut self.gfx else { return };
524 let px = self.state.cell_px();
525 if px != self.thumbs.cell {
526 for p in self.thumbs.requested.drain().map(|(p, _)| p) {
527 self.pool.cancel(&JobKey::Thumb(p));
528 }
529 self.thumbs.arrivals.clear();
530 self.state.thumbs.clear();
531 g.renderer.reset_thumbs(px);
532 self.thumbs.cell = px;
533 }
534 let l = self.state.grid_layout();
535 let visible = l.visible(self.state.grid.target(&l));
536 let span = l.page_rows() * l.cols;
537 let near = visible.start.saturating_sub(span)..(visible.end + 2 * span).min(l.count);
538 let q = &self.state.queue;
539 let mut wanted = HashSet::with_capacity(near.len());
540 let mut visible_paths = HashSet::with_capacity(visible.len());
541 let mut submit = Vec::new();
542 for i in near {
543 let Some(p) = q.get(i) else { continue };
544 wanted.insert(p.to_path_buf());
545 let prio = if visible.contains(&i) {
546 visible_paths.insert(p.to_path_buf());
547 Priority::ThumbVisible
548 } else {
549 Priority::Thumb
550 };
551 if !self.state.thumbs.contains_key(p) && self.thumbs.requested.get(p) != Some(&prio) {
552 submit.push((p.to_path_buf(), prio));
553 }
554 }
555 // Visible first, in order.
556 submit.sort_by_key(|(_, prio)| *prio);
557 for (p, prio) in submit {
558 self.request_thumb(p.clone(), prio);
559 self.thumbs.requested.insert(p, prio);
560 }
561 let mut demote = Vec::new();
562 self.thumbs.requested.retain(|p, prio| {
563 if !wanted.contains(p) {
564 self.pool.cancel(&JobKey::Thumb(p.clone()));
565 return false;
566 }
567 if *prio == Priority::ThumbVisible && !visible_paths.contains(p) {
568 *prio = Priority::Thumb;
569 demote.push(p.clone());
570 }
571 true
572 });
573 for p in demote {
574 self.request_thumb(p, Priority::Thumb);
575 }
576 self.thumbs.wanted = wanted;
577 }
578
579 fn request_thumb(&self, path: PathBuf, prio: Priority) {
580 let proxy = self.proxy.clone();
581 let cache = self.thumb_cache.clone();
582 let px = self.thumbs.cell;
583 self.pool
584 .submit(JobKey::Thumb(path.clone()), prio, move || {
585 let t = Instant::now();
586 let mtime = mtime(&path);
587 let result = thumbs::load(&path, px, &cache);
588 log::trace!("thumbnail {} in {:.1?}", path.display(), t.elapsed());
589 let _ = proxy.send_event(UserEvent::Thumb {
590 path,
591 px,
592 mtime,
593 result,
594 });
595 });
596 }
597
598 fn on_thumb(
599 &mut self,
600 path: PathBuf,
601 px: u32,
602 mtime: Option<SystemTime>,
603 result: Result<Thumb, ThumbError>,
604 ) {
605 self.thumbs.requested.remove(&path);
606 if px != self.thumbs.cell {
607 return;
608 }
609 match result {
610 Ok(t) => {
611 if let Some(m) = mtime {
612 self.stamps.insert(path.clone(), m);
613 }
614 self.thumbs.arrivals.push_back((path, t));
615 }
616 Err(e) => {
617 // Undecodable: it leaves the queue, as in image mode.
618 log::warn!("{}: {e}", path.display());
619 let was_current = self.state.queue.current() == Some(path.as_path());
620 if self.state.queue.remove(&path) {
621 self.state.skipped += 1;
622 }
623 self.sync(was_current);
624 self.request_redraw();
625 }
626 }
627 }
628
629 /// Every thumbnail on screen is in the atlas.
630 fn grid_settled(&self) -> bool {
631 let l = self.state.grid_layout();
632 let scroll = self.state.grid.scroll(&l, Instant::now());
633 l.visible(scroll).all(|i| {
634 self.state
635 .queue
636 .get(i)
637 .is_some_and(|p| self.state.thumbs.contains_key(p))
638 })
639 }
640
641 /// Upload `img` in full and put it on screen.
642 fn show(&mut self, path: &Path, img: Arc<DecodedImage>, now: Instant) {
643 let Some(g) = &mut self.gfx else {
644 // No GPU yet: keep it cached; `resumed` syncs again.
645 self.state.loading_since.get_or_insert(now);
646 return;
647 };
648 g.renderer.upload(path, &img, usize::MAX);
649 self.uploads.retain(|(p, _)| p != path);
650 self.state.show(path, img.source_size, img.format, now);
651 self.state.set_frames(img.delays(), img.animated, now);
652 self.request_info_line();
653 self.request_redraw();
654 }
655
656 fn update_title(&self) {
657 if let Some(g) = &self.gfx {
658 let title = match self.state.queue.current() {
659 Some(_) => {
660 ui::template::expand(&self.config.window.title, &|n| ui::var(&self.state, n))
661 }
662 None => "remage".into(),
663 };
664 g.window.set_title(&title);
665 }
666 }
667
668 /// Start, retarget or stop the directory watch to match `watch_dir` and the queue.
669 fn set_dir_watch(&mut self) {
670 let dirs = if self.config.general.watch_dir {
671 self.opts.dirs.clone()
672 } else {
673 Vec::new()
674 };
675 match &self.dir_watch {
676 Some(w) => w.set(dirs),
677 None if !dirs.is_empty() => {
678 let proxy = self.proxy.clone();
679 self.dir_watch = Some(watch::spawn(dirs, move |c| {
680 let _ = proxy.send_event(UserEvent::Dir(c));
681 }));
682 }
683 None => {}
684 }
685 }
686
687 /// Files changed on disk: new images join the queue, changed ones reload, gone ones leave.
688 fn on_dir(&mut self, c: watch::Changes) {
689 let known: HashSet<&PathBuf> = self.state.queue.all_files().iter().collect();
690 let (old, new): (Vec<_>, Vec<_>) =
691 c.present.into_iter().partition(|(p, _)| known.contains(p));
692 let mut found: Vec<(PathBuf, Option<SystemTime>)> =
693 old.into_iter().map(|(p, t)| (p, Some(t))).collect();
694 found.extend(c.gone.into_iter().map(|p| (p, None)));
695 if !found.is_empty() {
696 self.on_refreshed(found);
697 }
698 if !new.is_empty() {
699 log::debug!("{} new files on disk", new.len());
700 let was_empty = self.state.queue.is_empty();
701 self.state
702 .queue
703 .insert_sorted(new.into_iter().map(|(p, _)| p));
704 self.sync(was_empty);
705 }
706 }
707
708 /// `open <path>`: a file already queued is just selected; anything else is scanned
709 /// (with its siblings) off-thread and replaces the queue.
710 fn open(&mut self, arg: &str) {
711 let cwd = std::env::current_dir().unwrap_or_default();
712 let path = crate::config::expand_path(arg, &cwd);
713 if let Some(i) = self.state.queue.files().iter().position(|f| *f == path) {
714 let fx = self.state.apply(Action::Goto(i + 1), Instant::now());
715 if fx.navigated {
716 self.sync(true);
717 }
718 return;
719 }
720 let g = &self.config.general;
721 let opts = scan::ScanOptions {
722 recursive: g.recursive,
723 siblings: g.open_siblings,
724 sort: g.sort,
725 reverse: g.reverse,
726 };
727 let proxy = self.proxy.clone();
728 std::thread::spawn(move || {
729 let scan = scan::scan(std::slice::from_ref(&path), &opts);
730 let _ = proxy.send_event(UserEvent::Opened { arg: path, scan });
731 });
732 }
733
734 fn on_opened(&mut self, arg: PathBuf, scan: scan::Scan) {
735 let now = Instant::now();
736 if scan.files.is_empty() && scan.unknown.is_empty() {
737 let why = scan
738 .errors
739 .first()
740 .cloned()
741 .unwrap_or_else(|| format!("{}: no images", arg.display()));
742 self.state
743 .notify(format!("error: {why}"), MsgKind::Error, now);
744 self.request_redraw();
745 return;
746 }
747 log::debug!("opened {}: {} files", arg.display(), scan.files.len());
748 if !scan.unknown.is_empty() {
749 let proxy = self.proxy.clone();
750 let unknown = scan.unknown;
751 std::thread::spawn(move || {
752 let found: Vec<_> = unknown.into_iter().filter(|p| decode::sniff(p)).collect();
753 if !found.is_empty() {
754 let _ = proxy.send_event(UserEvent::Found(found));
755 }
756 });
757 }
758 self.state.queue = Queue::new(scan.files, scan.start.as_deref(), 0);
759 self.state.skipped = 0;
760 self.state.input = None;
761 self.opts.dirs = scan.dirs;
762 self.set_dir_watch();
763 if self.state.in_grid() {
764 let l = self.state.grid_layout();
765 self.state
766 .grid
767 .jump_to(l.follow(self.state.queue.index(), 0.0));
768 }
769 self.sync(true);
770 }
771
772 /// Read the current file's metadata for the info panel, off-thread.
773 fn request_info(&mut self) {
774 if !self.state.info_open {
775 return;
776 }
777 let Some(cur) = self.state.queue.current().map(Path::to_path_buf) else {
778 return;
779 };
780 if self.state.info.as_ref().is_some_and(|i| i.path == cur)
781 || self.info_loading.as_ref() == Some(&cur)
782 {
783 return;
784 }
785 self.info_loading = Some(cur.clone());
786 let proxy = self.proxy.clone();
787 std::thread::spawn(move || {
788 let rows = decode::meta::read(&cur);
789 let _ = proxy.send_event(UserEvent::Info(Info { path: cur, rows }));
790 });
791 }
792
793 /// Run `ui.info_command` for the image on screen (nsxiv's image-info: file, width,
794 /// height); its last line fills `{info}`.
795 fn request_info_line(&mut self) {
796 let cmd = self.config.ui.info_command.trim();
797 let Some(shown) = &self.state.shown else {
798 return;
799 };
800 if cmd.is_empty() || self.info_line_for.as_ref() == Some(&shown.path) {
801 return;
802 }
803 let path = shown.path.clone();
804 self.info_line_for = Some(path.clone());
805 let mut c = std::process::Command::new("sh");
806 c.arg("-c")
807 .arg(format!("{cmd} \"$@\""))
808 .arg("remage")
809 .arg(&path)
810 .arg(shown.size[0].to_string())
811 .arg(shown.size[1].to_string())
812 .stdin(std::process::Stdio::null())
813 .stderr(std::process::Stdio::null());
814 if let Some(sock) = &self.opts.socket {
815 c.env("REMAGE_SOCKET", sock);
816 }
817 let proxy = self.proxy.clone();
818 std::thread::spawn(move || {
819 let line = match c.output() {
820 Ok(o) => String::from_utf8_lossy(&o.stdout)
821 .lines()
822 .rev()
823 .find(|l| !l.trim().is_empty())
824 .unwrap_or("")
825 .trim()
826 .to_string(),
827 Err(e) => {
828 log::warn!("info_command: {e}");
829 String::new()
830 }
831 };
832 let _ = proxy.send_event(UserEvent::InfoLine { path, line });
833 });
834 }
835
836 /// What a binding does, in a few words: a command's own `desc` for `run`.
837 fn describe(&self, actions: &[Action]) -> String {
838 let one = |a: &Action| match a {
839 Action::Run(name) => self
840 .config
841 .commands
842 .get(name)
843 .map(|c| c.desc.clone())
844 .filter(|d| !d.is_empty())
845 .unwrap_or_else(|| a.description()),
846 _ => a.description(),
847 };
848 actions.iter().map(one).collect::<Vec<_>>().join(" · ")
849 }
850
851 /// The keys bound to exactly `action` in the current mode, keyboard first.
852 fn keys_for(&self, action: &Action) -> String {
853 let mut keys = self.keymap.keys_for(self.state.mode, action);
854 keys.sort_by_key(|k| {
855 k.0.iter()
856 .any(|k| matches!(k.code, Code::Mouse(_) | Code::Scroll(_)))
857 });
858 keys.iter()
859 .take(2)
860 .map(|k| ui::pretty_keys(&k.to_string()))
861 .collect::<Vec<_>>()
862 .join(" ")
863 }
864
865 /// Palette rows: user commands, then every action (each argument choice spelled out,
866 /// placeholders like `goto N` left to complete).
867 fn palette_items(&self) -> Vec<PaletteItem> {
868 let mut seen = HashSet::new();
869 let mut items = Vec::new();
870 let mut push = |items: &mut Vec<PaletteItem>,
871 action: String,
872 desc: String,
873 keys: String,
874 template: bool| {
875 if seen.insert(action.clone()) {
876 items.push(PaletteItem {
877 action,
878 desc,
879 keys,
880 template,
881 });
882 }
883 };
884 for (name, cmd) in &self.config.commands {
885 let a = Action::Run(name.clone());
886 let desc = if cmd.desc.is_empty() {
887 format!("Run {name}")
888 } else {
889 cmd.desc.clone()
890 };
891 push(&mut items, a.to_string(), desc, self.keys_for(&a), false);
892 }
893 for entry in CATALOG {
894 if entry.name == "run" {
895 continue;
896 }
897 let args = entry.args.trim_start_matches('[').trim_end_matches(']');
898 // Optional arguments (`[n]`, `[toggle|on|off]`) also offer the bare name.
899 let mut options: Vec<&str> = vec![""];
900 if !entry.args.is_empty() {
901 if !entry.args.starts_with('[') {
902 options.clear();
903 }
904 options.extend(
905 args.split('|')
906 .map(|o| o.trim_end_matches(" [n]").trim_end_matches(']'))
907 .filter(|o| *o != "n"),
908 );
909 }
910 for opt in options {
911 let text = format!("{} {opt}", entry.name);
912 let required = !entry.args.is_empty() && !entry.args.starts_with('[');
913 let placeholder = opt.contains(['<', 'N']);
914 match text.trim().parse::<Action>() {
915 Ok(a) if !placeholder => {
916 let desc = a.description();
917 push(&mut items, a.to_string(), desc, self.keys_for(&a), false);
918 }
919 _ if required || placeholder => {
920 push(
921 &mut items,
922 text.trim().to_string(),
923 entry.desc.to_string(),
924 String::new(),
925 true,
926 );
927 }
928 _ => {}
929 }
930 }
931 }
932 items
933 }
934
935 /// Help rows: the live keymap of the current mode, grouped, keys for one action merged.
936 fn help_rows(&self) -> Vec<HelpRow> {
937 const GROUPS: [&str; 8] = [
938 "Navigation",
939 "Marks",
940 "Grid",
941 "View",
942 "Playback",
943 "Interface",
944 "Queue",
945 "App",
946 ];
947 let rank = |a: &Action| {
948 CATALOG
949 .iter()
950 .position(|e| e.name == a.name())
951 .unwrap_or(usize::MAX)
952 };
953 // (group, catalog rank, action, keys, description)
954 let mut entries: Vec<(usize, usize, String, String, String)> = Vec::new();
955 for (keys, actions) in self.keymap.bindings(self.state.mode) {
956 let Some(first) = actions.first() else {
957 continue;
958 };
959 let group = GROUPS.iter().position(|g| *g == first.group()).unwrap_or(0);
960 let desc = self.describe(&actions);
961 let k = ui::pretty_keys(&keys.to_string());
962 match entries.iter_mut().find(|e| e.0 == group && e.4 == desc) {
963 Some(e) => {
964 e.3.push_str(" ");
965 e.3.push_str(&k);
966 }
967 None => entries.push((group, rank(first), first.to_string(), k, desc)),
968 }
969 }
970 // Within a group: catalog order, then by action (`next` before `next 10`).
971 entries.sort_by(|a, b| (a.0, a.1, &a.2).cmp(&(b.0, b.1, &b.2)));
972 let mut rows = Vec::new();
973 let mut last = None;
974 for (group, _, _, keys, desc) in entries {
975 if last != Some(group) {
976 rows.push(HelpRow::Header(GROUPS[group].to_string()));
977 last = Some(group);
978 }
979 rows.push(HelpRow::Binding { keys, desc });
980 }
981 rows
982 }
983
984 /// Which-key rows for the pending sequence.
985 fn which_key_rows(&self) -> Vec<WhichRow> {
986 let mode = self.state.mode;
987 let pending = self.keymap.pending_keys().to_vec();
988 let all = self.keymap.bindings(mode);
989 self.keymap
990 .candidates(mode)
991 .into_iter()
992 .map(|c| {
993 let desc = match &c.actions {
994 Some(a) => self.describe(a),
995 None => {
996 let mut prefix = pending.clone();
997 prefix.push(c.key);
998 let n = all.iter().filter(|(k, _)| k.0.starts_with(&prefix)).count();
999 format!("+{n} more")
1000 }
1001 };
1002 WhichRow {
1003 key: ui::pretty_keys(&KeySeq(vec![c.key]).to_string()),
1004 desc,
1005 prefix: c.prefix && c.actions.is_none(),
1006 }
1007 })
1008 .collect()
1009 }
1010
1011 /// Show a config error until the config is fixed.
1012 pub fn config_failed(&mut self, message: &str) {
1013 self.state.notify_for(
1014 format!("config error: {}", short_error(message)),
1015 MsgKind::Error,
1016 Instant::now(),
1017 CONFIG_ERROR_TIME,
1018 );
1019 self.config_error = true;
1020 self.request_redraw();
1021 }
1022
1023 /// Adopt a reloaded config. Runtime toggles (bar, filter, alpha background) only change
1024 /// when their config value did, so a reload doesn't undo what the user just toggled.
1025 fn apply_config(&mut self, loaded: Loaded) {
1026 let now = Instant::now();
1027 for w in &loaded.warnings {
1028 log::warn!("{w}");
1029 }
1030 let old = std::mem::replace(&mut self.config, loaded.config);
1031 let new = &self.config;
1032 decode::set_decoders(&new.decoders);
1033 let mut settings = Settings::from_config(new);
1034 self.opts.overrides.apply(&mut settings);
1035 let preload_changed = settings.preload != self.state.settings.preload;
1036 if old.grid.size != new.grid.size {
1037 self.state.grid.size = new.grid.size;
1038 }
1039 let watch_changed = old.general.watch_dir != new.general.watch_dir;
1040 if old.grid.thumbnail_cache != new.grid.thumbnail_cache {
1041 self.thumb_cache = CacheOptions::new(new.grid.thumbnail_cache, self.opts.private);
1042 }
1043 self.state.set_settings(settings);
1044 self.state.set_theme(loaded.theme, now);
1045 self.keymap = keymap(&loaded.keys);
1046 self.ambiguous_since = None;
1047 self.state.pending.clear();
1048 self.cache.set_budget(new.image.cache_mb << 20);
1049 if old.image.filter != new.image.filter {
1050 self.state.nearest = new.image.filter.nearest();
1051 }
1052 if old.image.alpha_background != new.image.alpha_background {
1053 self.state.alpha_bg = new.image.alpha_background;
1054 }
1055 if old.ui.statusbar != new.ui.statusbar && self.opts.overrides.bar.is_none() {
1056 let s = if new.ui.statusbar {
1057 Switch::On
1058 } else {
1059 Switch::Off
1060 };
1061 self.state.apply(Action::Bar(s), now);
1062 }
1063 if let Some(g) = &mut self.gfx {
1064 if old.window.decorations != new.window.decorations {
1065 g.window.set_decorations(new.window.decorations);
1066 }
1067 g.renderer.set_font_families(font_families(new));
1068 }
1069 if watch_changed {
1070 self.set_dir_watch();
1071 }
1072 self.info_line_for = None;
1073 self.request_info_line();
1074 self.update_title();
1075 // Silent on routine changes (a new wallpaper palette); confirm fixes and manual reloads.
1076 if std::mem::take(&mut self.config_error) || std::mem::take(&mut self.reload_requested) {
1077 self.state.notify("config reloaded", MsgKind::Info, now);
1078 }
1079 if preload_changed {
1080 self.sync(false);
1081 }
1082 self.sync_thumbs();
1083 self.request_redraw();
1084 }
1085
1086 fn on_decoded(
1087 &mut self,
1088 path: PathBuf,
1089 mtime: Option<SystemTime>,
1090 result: Result<Arc<DecodedImage>, String>,
1091 ) {
1092 lock(&self.arriving).remove(&path);
1093 let now = Instant::now();
1094 let is_current = self.state.queue.current() == Some(path.as_path());
1095 let in_grid = self.state.in_grid();
1096 match result {
1097 Ok(img) => {
1098 if let Some(m) = mtime {
1099 self.stamps.insert(path.clone(), m);
1100 }
1101 let wanted = self.window_paths();
1102 let keep: Vec<&Path> = wanted.iter().map(PathBuf::as_path).collect();
1103 self.cache.insert(path.clone(), img.clone(), &keep);
1104 if in_grid {
1105 // Cached for when the grid opens it.
1106 } else if is_current {
1107 // Already on screen means this was a reload: replace the texture.
1108 let reload = self.state.shown.as_ref().is_some_and(|s| s.path == path);
1109 if reload && let Some(g) = &mut self.gfx {
1110 g.renderer.retain(|p| p != path);
1111 }
1112 self.show(&path, img, now);
1113 } else if wanted.contains(&path) && !self.uploads.iter().any(|(p, _)| *p == path) {
1114 self.uploads.push_back((path, img));
1115 }
1116 }
1117 Err(e) => {
1118 log::warn!("{}: {e}", path.display());
1119 self.state.queue.remove(&path);
1120 self.state.skipped += 1;
1121 if is_current {
1122 self.state.loading_since = None;
1123 let msg = format!("error: {}: {e}", ui::file_name(&path));
1124 self.state.notify(msg, MsgKind::Error, now);
1125 self.sync(true);
1126 }
1127 self.request_redraw();
1128 }
1129 }
1130 }
1131
1132 fn reload(&mut self) {
1133 if let Some(cur) = self.state.queue.current().map(Path::to_path_buf) {
1134 self.cache.remove(&cur);
1135 self.decode(&cur, Priority::Current);
1136 }
1137 }
1138
1139 /// Run actions in order. A `run` suspends the rest of the chain until its command exits
1140 /// successfully. Errors are shown on the status line; the first is also returned for IPC.
1141 fn run(&mut self, actions: Vec<Action>, ctx: &Ctx, el: &ActiveEventLoop) -> Result<(), String> {
1142 let mut rest = actions.into_iter();
1143 while let Some(action) = rest.next() {
1144 match &action {
1145 Action::Run(name) => return self.start_command(name, rest.collect()),
1146 Action::Cancel => {
1147 self.keymap.cancel();
1148 self.state.pending.clear();
1149 self.config_error = false;
1150 self.answer(false);
1151 }
1152 Action::QuitNoOutput => self.no_output = true,
1153 _ => {}
1154 }
1155 let fx = self.state.apply_at(action, ctx.anchor, Instant::now());
1156 self.effects(fx, ctx, el);
1157 if fx.quit {
1158 break;
1159 }
1160 }
1161 Ok(())
1162 }
1163
1164 /// Start `[commands.<name>]`, or ask first when it has a `confirm`.
1165 fn start_command(&mut self, name: &str, then: Vec<Action>) -> Result<(), String> {
1166 let now = Instant::now();
1167 let fail = |state: &mut AppState, msg: String| {
1168 state.notify(format!("error: {msg}"), MsgKind::Error, now);
1169 Err(msg)
1170 };
1171 let Some(cmd) = self.config.commands.get(name).cloned() else {
1172 let hint = suggest(name, self.config.commands.keys().map(String::as_str))
1173 .map(|s| format!(", did you mean `{s}`?"))
1174 .unwrap_or_default();
1175 self.request_redraw();
1176 return fail(&mut self.state, format!("no command named `{name}`{hint}"));
1177 };
1178 let q = &self.state.queue;
1179 let targets: Arc<[PathBuf]> =
1180 exec::targets(cmd.targets, q.current(), &q.marked(), q.files()).into();
1181 if targets.is_empty() && exec::needs_files(&cmd) {
1182 let why = if cmd.targets == Targets::Marked {
1183 "no marked files"
1184 } else {
1185 "no images"
1186 };
1187 self.request_redraw();
1188 return fail(&mut self.state, format!("{name}: {why}"));
1189 }
1190 let size = self
1191 .state
1192 .shown
1193 .as_ref()
1194 .filter(|s| Some(s.path.as_path()) == q.current())
1195 .map(|s| s.size);
1196 let ctx = exec::Context {
1197 current: q.current(),
1198 index: q.index(),
1199 targets: &targets,
1200 size,
1201 mode: self.state.mode.name(),
1202 socket: self.opts.socket.as_deref(),
1203 };
1204 let prepared = exec::prepare(&cmd, &ctx);
1205 let question = cmd.confirm.as_deref().map(|c| exec::expand_text(c, &ctx));
1206 let mut chain = cmd.after.0.unwrap_or_default();
1207 chain.extend(then);
1208 let job = Job {
1209 name: name.to_string(),
1210 output: cmd.output,
1211 targets,
1212 then: chain,
1213 lines: 0,
1214 };
1215 match question {
1216 Some(q) => {
1217 self.answer(false);
1218 self.state.prompt = Some(q);
1219 self.confirm = Some((job, prepared));
1220 self.request_redraw();
1221 }
1222 None => self.spawn_job(job, prepared),
1223 }
1224 Ok(())
1225 }
1226
1227 /// Settle a confirm prompt: `y` runs the command, anything else drops it.
1228 fn answer(&mut self, yes: bool) {
1229 let Some((job, prepared)) = self.confirm.take() else {
1230 return;
1231 };
1232 self.state.prompt = None;
1233 if yes {
1234 self.spawn_job(job, prepared);
1235 }
1236 self.request_redraw();
1237 }
1238
1239 fn spawn_job(&mut self, job: Job, prepared: exec::Prepared) {
1240 let id = self.next_job;
1241 self.next_job += 1;
1242 log::info!(
1243 "run {}: {} {:?}",
1244 job.name,
1245 prepared.program.to_string_lossy(),
1246 prepared.args
1247 );
1248 let proxy = self.proxy.clone();
1249 exec::spawn(&job.name, prepared, job.output, move |event| {
1250 let _ = proxy.send_event(UserEvent::Command { id, event });
1251 });
1252 self.state.running.push((id, job.name.clone()));
1253 self.jobs.insert(id, job);
1254 self.request_redraw();
1255 }
1256
1257 fn on_command(&mut self, id: u64, event: exec::Event, el: &ActiveEventLoop) {
1258 let now = Instant::now();
1259 match event {
1260 exec::Event::Line(line) => {
1261 let Some(job) = self.jobs.get_mut(&id) else {
1262 return;
1263 };
1264 job.lines += 1;
1265 let name = job.name.clone();
1266 let ctx = Ctx {
1267 anchor: None,
1268 targets: Some(job.targets.clone()),
1269 };
1270 match line.parse::<Action>() {
1271 Ok(a) => {
1272 let _ = self.run(vec![a], &ctx, el);
1273 }
1274 Err(e) => {
1275 log::warn!("{name}: {line}: {e}");
1276 self.state
1277 .notify(format!("error: {name}: {e}"), MsgKind::Error, now);
1278 self.request_redraw();
1279 }
1280 }
1281 }
1282 exec::Event::Done(outcome) => {
1283 let Some(job) = self.jobs.remove(&id) else {
1284 return;
1285 };
1286 self.state.running.retain(|(j, _)| *j != id);
1287 self.request_redraw();
1288 if !outcome.success() {
1289 let msg = format!("error: {}", outcome.error());
1290 log::warn!("{}: {msg}", job.name);
1291 self.state.notify(msg, MsgKind::Error, now);
1292 return;
1293 }
1294 log::debug!("{}: done", job.name);
1295 match (job.output, outcome.last_line) {
1296 (Output::Status, Some(line)) => self.state.notify(line, MsgKind::Info, now),
1297 // Actions speak for themselves.
1298 (Output::Actions, _) if job.lines > 0 => {}
1299 _ => self.state.notify("done", MsgKind::Info, now),
1300 }
1301 let ctx = Ctx {
1302 anchor: None,
1303 targets: Some(job.targets),
1304 };
1305 let _ = self.run(job.then, &ctx, el);
1306 }
1307 }
1308 }
1309
1310 /// Re-check `targets` (default: the whole queue, plus the current file) on disk, off-thread.
1311 fn refresh(&mut self, targets: Option<Arc<[PathBuf]>>) {
1312 let mut files: Vec<PathBuf> = match targets {
1313 Some(t) => t.to_vec(),
1314 None => self.state.queue.all_files().to_vec(),
1315 };
1316 if let Some(cur) = self.state.queue.current()
1317 && !files.iter().any(|f| f == cur)
1318 {
1319 files.push(cur.to_path_buf());
1320 }
1321 let proxy = self.proxy.clone();
1322 std::thread::spawn(move || {
1323 let found = files
1324 .into_iter()
1325 .map(|p| {
1326 let m = std::fs::metadata(&p)
1327 .ok()
1328 .map(|m| m.modified().unwrap_or(SystemTime::UNIX_EPOCH));
1329 (p, m)
1330 })
1331 .collect();
1332 let _ = proxy.send_event(UserEvent::Refreshed(found));
1333 });
1334 }
1335
1336 /// Drop files that vanished and reload ones that changed since they were loaded.
1337 fn on_refreshed(&mut self, found: Vec<(PathBuf, Option<SystemTime>)>) {
1338 let cur = self.state.queue.current().map(Path::to_path_buf);
1339 let showing = |s: &AppState, p: &Path| {
1340 s.mode == Mode::Image && s.shown.as_ref().is_some_and(|s| s.path == p)
1341 };
1342 let mut navigated = false;
1343 let mut stale = Vec::new();
1344 let mut gone = 0;
1345 for (path, mtime) in found {
1346 match mtime {
1347 None => {
1348 if self.state.queue.remove(&path) {
1349 gone += 1;
1350 navigated |= cur.as_ref() == Some(&path);
1351 }
1352 self.cache.remove(&path);
1353 self.stamps.remove(&path);
1354 self.state.thumbs.remove(&path);
1355 }
1356 Some(t) if self.stamps.get(&path).is_some_and(|s| *s != t) => stale.push(path),
1357 Some(_) => {}
1358 }
1359 }
1360 let mut reload_current = false;
1361 for path in &stale {
1362 self.cache.remove(path);
1363 self.stamps.remove(path);
1364 self.state.thumbs.remove(path);
1365 self.uploads.retain(|(p, _)| p != path);
1366 if cur.as_ref() == Some(path) && showing(&self.state, path) {
1367 // Keep it on screen until the new version replaces it.
1368 reload_current = true;
1369 } else if let Some(g) = &mut self.gfx {
1370 g.renderer.retain(|p| p != path);
1371 }
1372 }
1373 if gone + stale.len() > 0 {
1374 log::debug!("refresh: {gone} gone, {} changed", stale.len());
1375 }
1376 if reload_current && let Some(cur) = &cur {
1377 self.decode(cur, Priority::Current);
1378 }
1379 if gone + stale.len() > 0 {
1380 self.sync(navigated);
1381 self.request_redraw();
1382 }
1383 }
1384
1385 fn on_ipc(&mut self, inc: ipc::Incoming, el: &ActiveEventLoop) {
1386 let reply = match inc.request {
1387 Request::Actions(actions) => match self.run(actions, &Ctx::default(), el) {
1388 Ok(()) => ipc::ok(Value::Null),
1389 Err(e) => ipc::error(&e),
1390 },
1391 Request::Query(q) => ipc::ok(self.query(q)),
1392 Request::Subscribe(topics) => match inc.events {
1393 Some(tx) => {
1394 if self.published.is_none() {
1395 self.published = Some(self.snapshot());
1396 }
1397 self.subscribers.push(Subscriber { topics, tx });
1398 ipc::ok(Value::Null)
1399 }
1400 None => ipc::error("no event channel"),
1401 },
1402 };
1403 let _ = inc.reply.send(reply);
1404 }
1405
1406 fn query(&self, q: Query) -> Value {
1407 let s = &self.state;
1408 let paths = |ps: &mut dyn Iterator<Item = &Path>| -> Value {
1409 ps.map(ipc::path_json).collect::<Vec<_>>().into()
1410 };
1411 match q {
1412 Query::Current => s.queue.current().map_or(Value::Null, ipc::path_json),
1413 Query::Marked => paths(&mut s.queue.marked().into_iter()),
1414 Query::Queue => paths(&mut s.queue.files().iter().map(PathBuf::as_path)),
1415 Query::State => {
1416 let shown = s
1417 .shown
1418 .as_ref()
1419 .filter(|sh| Some(sh.path.as_path()) == s.queue.current());
1420 let fullscreen = self
1421 .gfx
1422 .as_ref()
1423 .is_some_and(|g| g.window.fullscreen().is_some());
1424 json!({
1425 "mode": s.mode.name(),
1426 "current": s.queue.current().map(ipc::path_json),
1427 "index": (!s.queue.is_empty()).then(|| s.queue.index() + 1),
1428 "count": s.queue.len(),
1429 "marked": s.queue.marked_count(),
1430 "width": shown.map(|sh| sh.size[0]),
1431 "height": shown.map(|sh| sh.size[1]),
1432 "format": shown.map(|sh| sh.format),
1433 "zoom": shown.map(|_| (s.view.scale() * 1000.0).round() / 10.0),
1434 "fit": s.settings.fit,
1435 "bar": s.bar,
1436 "fullscreen": fullscreen,
1437 "skipped": s.skipped,
1438 "filter": s.queue.filter(),
1439 "slideshow": s.slideshow.is_some(),
1440 "running": s.running.iter().map(|(_, n)| n.as_str()).collect::<Vec<_>>(),
1441 "pid": std::process::id(),
1442 })
1443 }
1444 }
1445 }
1446
1447 fn snapshot(&self) -> Published {
1448 let q = &self.state.queue;
1449 Published {
1450 image: (q.current().map(Path::to_path_buf), q.index(), q.len()),
1451 marks: q.marks_version(),
1452 mode: self.state.mode,
1453 }
1454 }
1455
1456 /// Tell subscribers what changed since they last heard.
1457 fn publish(&mut self) {
1458 if self.subscribers.is_empty() {
1459 self.published = None;
1460 return;
1461 }
1462 let now = self.snapshot();
1463 let Some(before) = self.published.replace(now.clone()) else {
1464 return;
1465 };
1466 let q = &self.state.queue;
1467 let mut events = Vec::new();
1468 if before.image != now.image {
1469 let data = json!({
1470 "path": q.current().map(ipc::path_json),
1471 "index": (!q.is_empty()).then(|| q.index() + 1),
1472 "count": q.len(),
1473 });
1474 events.push((Topic::Image, data));
1475 }
1476 if before.marks != now.marks {
1477 let files: Vec<Value> = q.marked().into_iter().map(ipc::path_json).collect();
1478 events.push((
1479 Topic::Marks,
1480 json!({ "count": files.len(), "files": files }),
1481 ));
1482 }
1483 if before.mode != now.mode {
1484 events.push((Topic::Mode, json!(now.mode.name())));
1485 }
1486 for (topic, data) in events {
1487 let line = ipc::event(topic, data);
1488 self.subscribers
1489 .retain(|s| !s.topics.contains(&topic) || s.tx.send(line.clone()).is_ok());
1490 }
1491 }
1492
1493 /// Marked files for `-o`, unless the user quit with `quit-no-output`.
1494 pub fn marked_output(&self) -> Option<Vec<PathBuf>> {
1495 (!self.no_output).then(|| {
1496 self.state
1497 .queue
1498 .marked()
1499 .into_iter()
1500 .map(Path::to_path_buf)
1501 .collect()
1502 })
1503 }
1504
1505 fn effects(&mut self, fx: Effects, ctx: &Ctx, event_loop: &ActiveEventLoop) {
1506 if fx.quit {
1507 event_loop.exit();
1508 return;
1509 }
1510 if let Some(s) = fx.fullscreen
1511 && let Some(g) = &self.gfx
1512 {
1513 let on = g.window.fullscreen().is_some();
1514 let want = match s {
1515 Switch::Toggle => !on,
1516 Switch::On => true,
1517 Switch::Off => false,
1518 };
1519 if want != on {
1520 g.window
1521 .set_fullscreen(want.then_some(Fullscreen::Borderless(None)));
1522 }
1523 }
1524 if fx.reload {
1525 self.reload();
1526 }
1527 if fx.palette {
1528 let items = self.palette_items();
1529 self.keymap.cancel();
1530 self.state.open_palette(items);
1531 }
1532 if fx.help {
1533 let rows = self.help_rows();
1534 self.state.help = Some(Help { rows, scroll: 0 });
1535 }
1536 if fx.info {
1537 self.request_info();
1538 }
1539 if fx.open
1540 && let Some(p) = self.state.open.take()
1541 {
1542 self.open(&p);
1543 }
1544 if fx.reload_config {
1545 self.reload_requested = true;
1546 self.watch.reload();
1547 }
1548 if fx.refresh {
1549 self.refresh(ctx.targets.clone());
1550 }
1551 if let Some(key) = fx.sort {
1552 let mut files = self.state.queue.files().to_vec();
1553 let proxy = self.proxy.clone();
1554 std::thread::spawn(move || {
1555 scan::sort_paths(&mut files, key, false);
1556 let _ = proxy.send_event(UserEvent::Sorted(files));
1557 });
1558 }
1559 if fx.mode {
1560 // Opening an image from the grid loads it like navigating to it.
1561 self.sync(self.state.mode == Mode::Image || fx.navigated);
1562 } else if fx.navigated {
1563 self.sync(true);
1564 } else if fx.reordered {
1565 self.sync(false);
1566 }
1567 // Scrolling, a new size or the bar can bring new thumbnails into view.
1568 self.sync_thumbs();
1569 if fx.redraw || fx.layout {
1570 self.request_redraw();
1571 }
1572 }
1573
1574 /// Mirror the keymap's pending keys on the status line and arm the ambiguity timeout.
1575 fn update_pending(&mut self, now: Instant) {
1576 let pending = self.keymap.pending_display();
1577 if pending != self.state.pending {
1578 self.state.pending = pending;
1579 self.request_redraw();
1580 if !self.keymap.is_pending() {
1581 self.which_key_since = None;
1582 self.state.which_key = None;
1583 } else if self.state.which_key.is_some() {
1584 // Already showing: follow the sequence at once.
1585 self.state.which_key = Some(self.which_key_rows());
1586 } else {
1587 self.which_key_since = Some(now);
1588 }
1589 }
1590 self.ambiguous_since = self
1591 .keymap
1592 .is_ambiguous(self.state.mode)
1593 .then(|| self.ambiguous_since.unwrap_or(now));
1594 }
1595
1596 fn on_key(&mut self, event: winit::event::KeyEvent, event_loop: &ActiveEventLoop) {
1597 let now = Instant::now();
1598 if event.state == ElementState::Released {
1599 if let Some(dir) = self.held.remove(&event.physical_key) {
1600 self.state.hold_pan(dir, false, now);
1601 self.request_redraw();
1602 }
1603 if self
1604 .repeat
1605 .as_ref()
1606 .is_some_and(|(k, _)| *k == event.physical_key)
1607 {
1608 self.repeat = None;
1609 }
1610 return;
1611 }
1612 if self.confirm.is_some() {
1613 // The prompt takes the next key: `y` runs the command, anything else cancels.
1614 if !event.repeat
1615 && let Some(key) = input::key(&event.logical_key, self.modifiers)
1616 {
1617 self.answer(key == Key::plain('y') || key == Key::plain('Y'));
1618 }
1619 return;
1620 }
1621 if self.state.input.is_some() {
1622 self.on_input_key(&event, event_loop);
1623 return;
1624 }
1625 if self.state.help.is_some()
1626 && !event.repeat
1627 && let Some(key) = input::key(&event.logical_key, self.modifiers)
1628 && !self.keymap.is_pending()
1629 && self.help_key(key)
1630 {
1631 return;
1632 }
1633 if event.repeat {
1634 // Repeats bypass the trie: only a completed, repeatable binding repeats.
1635 if let Some((k, actions)) = &self.repeat
1636 && *k == event.physical_key
1637 {
1638 let actions = actions.clone();
1639 let _ = self.run(actions, &Ctx::default(), event_loop);
1640 }
1641 return;
1642 }
1643 let Some(key) = input::key(&event.logical_key, self.modifiers) else {
1644 return;
1645 };
1646 let runs = self.keymap.feed(self.state.mode, key);
1647 self.update_pending(now);
1648 self.repeat = None;
1649 for run in runs {
1650 let actions = run.counted();
1651 if let [Action::Pan(dir)] = actions[..]
1652 && self.state.mode == Mode::Image
1653 {
1654 if self.state.shown.is_some() {
1655 self.held.insert(event.physical_key, dir);
1656 self.state.hold_pan(dir, true, now);
1657 self.request_redraw();
1658 }
1659 continue;
1660 }
1661 if !actions.is_empty() && actions.iter().all(Action::repeats) {
1662 self.repeat = Some((event.physical_key, actions.clone()));
1663 }
1664 let _ = self.run(actions, &Ctx::default(), event_loop);
1665 }
1666 }
1667
1668 /// A key while the palette or search line is open: it edits the line.
1669 fn on_input_key(&mut self, event: &winit::event::KeyEvent, el: &ActiveEventLoop) {
1670 let now = Instant::now();
1671 let Some(key) = input::key(&event.logical_key, self.modifiers) else {
1672 return;
1673 };
1674 let ctrl = |c: char| {
1675 Key::new(
1676 input::keys::Mods {
1677 ctrl: true,
1678 ..Default::default()
1679 },
1680 Code::Char(c),
1681 )
1682 };
1683 let named = |n: Named| Key::new(Default::default(), Code::Named(n));
1684 let shift_tab = Key::new(
1685 input::keys::Mods {
1686 shift: true,
1687 ..Default::default()
1688 },
1689 Code::Named(Named::Tab),
1690 );
1691 let palette = self
1692 .state
1693 .input
1694 .as_ref()
1695 .is_some_and(|i| i.kind == InputKind::Palette);
1696 let empty = self.state.input.as_ref().is_some_and(|i| i.text.is_empty());
1697 let edit = if key == named(Named::Esc) || key == ctrl('c') || key == ctrl('[') {
1698 let fx = self.state.input_cancel(now);
1699 self.effects(fx, &Ctx::default(), el);
1700 return;
1701 } else if key == named(Named::Enter) {
1702 if event.repeat {
1703 return;
1704 }
1705 let (fx, run) = self.state.input_submit(now);
1706 self.effects(fx, &Ctx::default(), el);
1707 if let Some(text) = run {
1708 match text.parse::<Action>() {
1709 Ok(a) => {
1710 let _ = self.run(vec![a], &Ctx::default(), el);
1711 }
1712 Err(e) => self
1713 .state
1714 .notify(format!("error: {e}"), MsgKind::Error, now),
1715 }
1716 }
1717 self.request_redraw();
1718 return;
1719 } else if key == named(Named::Backspace) && empty {
1720 let fx = self.state.input_cancel(now);
1721 self.effects(fx, &Ctx::default(), el);
1722 return;
1723 } else if key == named(Named::Backspace) || key == ctrl('h') {
1724 Edit::Backspace
1725 } else if key == ctrl('w') {
1726 Edit::DeleteWord
1727 } else if key == ctrl('u') {
1728 Edit::Clear
1729 } else if key == named(Named::Tab) && palette {
1730 Edit::Complete
1731 } else if key == named(Named::Up) || key == ctrl('p') || key == shift_tab {
1732 Edit::Up
1733 } else if key == named(Named::Down) || key == ctrl('n') || key == named(Named::Tab) {
1734 Edit::Down
1735 } else if key == named(Named::Left) {
1736 Edit::Left
1737 } else if key == named(Named::Right) {
1738 Edit::Right
1739 } else if key == named(Named::Space) && key.mods.is_empty() {
1740 Edit::Insert(" ".into())
1741 } else {
1742 let m = self.modifiers;
1743 match &event.text {
1744 Some(t) if !m.control_key() && !m.alt_key() && !m.super_key() => {
1745 Edit::Insert(t.to_string())
1746 }
1747 _ => return,
1748 }
1749 };
1750 let fx = self.state.input_edit(edit, now);
1751 self.effects(fx, &Ctx::default(), el);
1752 }
1753
1754 /// Scrolling and closing keys while help is open. Returns whether the key was used.
1755 fn help_key(&mut self, key: Key) -> bool {
1756 let page = self.state.help_page();
1757 let Some(help) = &mut self.state.help else {
1758 return false;
1759 };
1760 let ctrl = |c: char| {
1761 Key::new(
1762 input::keys::Mods {
1763 ctrl: true,
1764 ..Default::default()
1765 },
1766 Code::Char(c),
1767 )
1768 };
1769 let named = |n: Named| Key::new(Default::default(), Code::Named(n));
1770 let half = (page / 2).max(1) as isize;
1771 let delta = match key {
1772 k if k == Key::plain('j') || k == named(Named::Down) => 1,
1773 k if k == Key::plain('k') || k == named(Named::Up) => -1,
1774 k if k == ctrl('d') || k == ctrl('f') || k == named(Named::PageDown) => half,
1775 k if k == ctrl('u') || k == ctrl('b') || k == named(Named::PageUp) => -half,
1776 k if k == Key::plain('g') || k == named(Named::Home) => isize::MIN / 2,
1777 k if k == Key::plain('G') || k == named(Named::End) => isize::MAX / 2,
1778 k if k == Key::plain('q') || k == Key::plain('?') || k == named(Named::Esc) => {
1779 self.state.help = None;
1780 self.request_redraw();
1781 return true;
1782 }
1783 _ => return false,
1784 };
1785 help.scroll_by(delta, page);
1786 self.request_redraw();
1787 true
1788 }
1789
1790 /// A mouse button or wheel notch: bound actions run at the cursor, bypassing sequences.
1791 fn on_mouse_key(&mut self, key: Key, event_loop: &ActiveEventLoop) {
1792 self.keymap.cancel();
1793 self.update_pending(Instant::now());
1794 if let Some(actions) = self.keymap.lookup(self.state.mode, &[key]) {
1795 let actions = actions.to_vec();
1796 let ctx = Ctx {
1797 anchor: Some(self.cursor),
1798 ..Ctx::default()
1799 };
1800 let _ = self.run(actions, &ctx, event_loop);
1801 }
1802 }
1803
1804 /// `pixels` is set for touchpads, whose deltas are pixels divided by [`PIXELS_PER_STEP`].
1805 fn on_wheel(&mut self, delta: [f32; 2], pixels: bool, event_loop: &ActiveEventLoop) {
1806 let now = Instant::now();
1807 let [dx, dy] = delta;
1808 if self.state.help.is_some() {
1809 let page = self.state.help_page();
1810 self.scroll[1] += dy;
1811 let rows = self.scroll[1].trunc();
1812 self.scroll[1] -= rows;
1813 if let Some(h) = &mut self.state.help {
1814 h.scroll_by(-(rows as isize) * 3, page);
1815 }
1816 self.request_redraw();
1817 return;
1818 }
1819 if dy != 0.0 {
1820 let dir = if dy > 0.0 { Scroll::Up } else { Scroll::Down };
1821 let key = input::scroll(dir, self.modifiers);
1822 // Wheel zoom and grid scrolling stay continuous for touchpads rather than stepping
1823 // per notch.
1824 let in_grid = self.state.in_grid();
1825 match self.keymap.lookup(self.state.mode, &[key]) {
1826 Some([Action::Scroll(d @ (Dir::Up | Dir::Down), n)]) if in_grid => {
1827 let sign = if *d == Dir::Up { -1.0 } else { 1.0 };
1828 let amount = dy.abs() * *n as f32 * sign;
1829 let moved = if pixels {
1830 self.state.grid_drag(-amount * PIXELS_PER_STEP, now)
1831 } else {
1832 self.state.grid_scroll(amount, now)
1833 };
1834 self.after_grid_scroll(moved);
1835 return;
1836 }
1837 Some([Action::Zoom(Zoom::In(n))]) => {
1838 let steps = dy.abs() * *n as f32;
1839 self.state.zoom_at(steps, self.cursor, now);
1840 self.request_redraw();
1841 return;
1842 }
1843 Some([Action::Zoom(Zoom::Out(n))]) => {
1844 let steps = -dy.abs() * *n as f32;
1845 self.state.zoom_at(steps, self.cursor, now);
1846 self.request_redraw();
1847 return;
1848 }
1849 _ => {}
1850 }
1851 }
1852 for (axis, d, pos, neg) in [
1853 (0, dx, Scroll::Left, Scroll::Right),
1854 (1, dy, Scroll::Up, Scroll::Down),
1855 ] {
1856 if d == 0.0 {
1857 continue;
1858 }
1859 // Reversing direction drops the leftover fraction.
1860 if self.scroll[axis] * d < 0.0 {
1861 self.scroll[axis] = 0.0;
1862 }
1863 self.scroll[axis] += d;
1864 while self.scroll[axis].abs() >= 1.0 {
1865 let up = self.scroll[axis] > 0.0;
1866 self.scroll[axis] -= self.scroll[axis].signum();
1867 let key = input::scroll(if up { pos } else { neg }, self.modifiers);
1868 self.on_mouse_key(key, event_loop);
1869 }
1870 }
1871 }
1872
1873 /// The grid scrolled; `moved` says whether it pulled the cursor along.
1874 fn after_grid_scroll(&mut self, moved: bool) {
1875 if moved {
1876 self.sync(true);
1877 } else {
1878 self.sync_thumbs();
1879 }
1880 self.request_redraw();
1881 }
1882
1883 /// A click (press and release in place) in the grid selects; a second one opens.
1884 fn grid_click(&mut self, event_loop: &ActiveEventLoop) {
1885 let now = Instant::now();
1886 let Some(i) = self.state.grid_hit(self.cursor, now) else {
1887 return;
1888 };
1889 let double = self
1890 .last_click
1891 .is_some_and(|(t, j)| j == i && now < t + DOUBLE_CLICK);
1892 self.last_click = (!double).then_some((now, i));
1893 let mut actions = vec![Action::Goto(i + 1)];
1894 if double {
1895 actions.push(Action::Mode(ModeArg::Image));
1896 }
1897 let _ = self.run(actions, &Ctx::default(), event_loop);
1898 }
1899
1900 fn redraw(&mut self) {
1901 let now = Instant::now();
1902 let ready = self.state.shown.is_some()
1903 || self.state.queue.is_empty()
1904 || (self.state.in_grid() && self.grid_settled())
1905 || now >= self.started + SHOW_TIMEOUT;
1906 if !self.shown_window && !ready {
1907 return; // about_to_wait wakes us at the timeout
1908 }
1909 self.state.advance(now);
1910 self.state.tick(now);
1911 let settled = if self.state.in_grid() {
1912 self.thumbs.arrivals.is_empty() && self.grid_settled()
1913 } else {
1914 self.state.loading_since.is_none()
1915 };
1916 // An animation moved on (or a page turned): put that frame in the texture.
1917 let frame = self.state.frame();
1918 if let Some(path) = self
1919 .state
1920 .shown
1921 .as_ref()
1922 .filter(|_| self.state.mode == Mode::Image)
1923 .map(|s| s.path.clone())
1924 && let Some(g) = &self.gfx
1925 && g.renderer.frame(&path).is_some_and(|f| f != frame)
1926 && let Some(img) = self.cache.get(&path)
1927 && let Some(g) = &mut self.gfx
1928 {
1929 g.renderer.show_frame(&path, &img, frame);
1930 }
1931 let Some(g) = &mut self.gfx else { return };
1932 if !self.shown_window {
1933 g.window.set_visible(true);
1934 }
1935 let theme = self.state.theme_at(now);
1936 let scene = ui::scene(&self.state, &theme, now, &mut |t| g.renderer.measure(t));
1937 let result = g.renderer.render(&scene);
1938 if let Some(path) = std::env::var_os("REMAGE_SNAPSHOT")
1939 && !self.state.is_animating(now)
1940 && settled
1941 {
1942 match g.renderer.snapshot(&scene, Path::new(&path)) {
1943 Ok(()) => log::info!("snapshot written to {}", Path::new(&path).display()),
1944 Err(e) => log::error!("snapshot: {e:#}"),
1945 }
1946 }
1947 drop(scene);
1948 if let Err(e) = result {
1949 log::error!("render: {e:#}");
1950 self.state
1951 .notify(format!("error: {e}"), MsgKind::Error, now);
1952 }
1953 if !self.shown_window {
1954 self.shown_window = true;
1955 log::debug!("first frame after {:.1?}", self.started.elapsed());
1956 let proxy = self.proxy.clone();
1957 std::thread::spawn(move || {
1958 let _ = proxy.send_event(UserEvent::Fonts(text::all_fonts()));
1959 });
1960 }
1961 if self.state.is_animating(Instant::now()) {
1962 g.window.request_redraw();
1963 }
1964 }
1965
1966 /// Upload thumbnails, then preloaded images a slice at a time. Returns whether work remains.
1967 fn pump_uploads(&mut self) -> bool {
1968 let Some(g) = &mut self.gfx else { return false };
1969 let now = Instant::now();
1970 let mut bytes = 0;
1971 while bytes < UPLOAD_BUDGET
1972 && let Some((path, t)) = self.thumbs.arrivals.pop_front()
1973 {
1974 bytes += t.pixels.len();
1975 let wanted = &self.thumbs.wanted;
1976 for gone in g.renderer.insert_thumb(&path, &t, |p| wanted.contains(p)) {
1977 self.state.thumbs.remove(&gone);
1978 }
1979 if g.renderer.has_thumb(&path) {
1980 self.state.thumb_ready(path, t.size, now);
1981 }
1982 g.window.request_redraw();
1983 }
1984 if !self.thumbs.arrivals.is_empty() {
1985 return true;
1986 }
1987 if let Some((path, img)) = self.uploads.front()
1988 && g.renderer.upload(path, img, UPLOAD_BUDGET)
1989 {
1990 self.uploads.pop_front();
1991 }
1992 !self.uploads.is_empty()
1993 }
1994}
1995
1996/// A config error trimmed for the status line: `config.toml:2:1: unknown field `fitt`; did you
1997/// mean `fit`?` without the directory or the list of every valid field.
1998fn short_error(message: &str) -> String {
1999 let mut s = match message.split_once(": ") {
2000 Some((loc, rest)) if loc.contains('/') => {
2001 format!("{}: {rest}", loc.rsplit('/').next().unwrap_or(loc))
2002 }
2003 _ => message.to_string(),
2004 };
2005 if let Some(start) = s.find(", expected one of") {
2006 let end = s[start..]
2007 .find("; did you mean")
2008 .map_or(s.len(), |i| start + i);
2009 s.replace_range(start..end, "");
2010 }
2011 s
2012}
2013
2014fn keymap(layers: &[KeyTables]) -> Keymap {
2015 let refs: Vec<&KeyTables> = layers.iter().collect();
2016 Keymap::new(&refs)
2017}
2018
2019fn font_families(c: &Config) -> [String; 3] {
2020 [
2021 c.ui.font_ui.clone(),
2022 c.ui.font_mono.clone(),
2023 c.ui.font_display.clone(),
2024 ]
2025}
2026
2027fn mtime(path: &Path) -> Option<SystemTime> {
2028 std::fs::metadata(path).and_then(|m| m.modified()).ok()
2029}
2030
2031fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
2032 m.lock().unwrap_or_else(|e| e.into_inner())
2033}
2034
2035impl ApplicationHandler<UserEvent> for App {
2036 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
2037 if self.gfx.is_some() {
2038 return;
2039 }
2040 let (w, h) = self.opts.geometry.unwrap_or((1280, 800));
2041 let mut attrs = Window::default_attributes()
2042 .with_title("remage")
2043 .with_inner_size(LogicalSize::new(w, h))
2044 .with_decorations(self.config.window.decorations)
2045 .with_visible(false);
2046 if self.opts.fullscreen {
2047 attrs = attrs.with_fullscreen(Some(Fullscreen::Borderless(None)));
2048 }
2049 let class = self.opts.class.clone();
2050 attrs = winit::platform::wayland::WindowAttributesExtWayland::with_name(
2051 attrs,
2052 class.clone(),
2053 class.clone(),
2054 );
2055 attrs =
2056 winit::platform::x11::WindowAttributesExtX11::with_name(attrs, class.clone(), class);
2057 let window = match event_loop.create_window(attrs) {
2058 Ok(w) => Arc::new(w),
2059 Err(e) => {
2060 self.error = Some(anyhow::anyhow!("create window: {e}"));
2061 event_loop.exit();
2062 return;
2063 }
2064 };
2065 let renderer = match Renderer::new(window.clone(), event_loop.owned_display_handle()) {
2066 Ok(mut r) => {
2067 r.set_font_families(font_families(&self.config));
2068 r
2069 }
2070 Err(e) => {
2071 self.error = Some(e.context("initialise GPU"));
2072 event_loop.exit();
2073 return;
2074 }
2075 };
2076 log::debug!("GPU ready after {:.1?}", self.started.elapsed());
2077 let size = window.inner_size();
2078 self.state.set_window(
2079 size.width as f32,
2080 size.height as f32,
2081 window.scale_factor() as f32,
2082 );
2083 self.gfx = Some(Gfx { renderer, window });
2084 self.sync(false);
2085 }
2086
2087 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
2088 match event {
2089 UserEvent::Decoded {
2090 path,
2091 mtime,
2092 result,
2093 } => self.on_decoded(path, mtime, result),
2094 UserEvent::Found(paths) => {
2095 log::debug!("sniffing found {} more images", paths.len());
2096 let was_empty = self.state.queue.is_empty();
2097 self.state.queue.insert_sorted(paths);
2098 self.sync(was_empty);
2099 }
2100 UserEvent::Fonts(db) => {
2101 if let Some(g) = &mut self.gfx {
2102 g.renderer.set_fonts(db);
2103 g.window.request_redraw();
2104 }
2105 }
2106 UserEvent::Config(result) => match *result {
2107 Ok(loaded) => {
2108 log::info!("config reloaded");
2109 self.apply_config(loaded);
2110 }
2111 Err(e) => {
2112 log::error!("{e}");
2113 self.config_failed(&e.message);
2114 }
2115 },
2116 UserEvent::Sorted(files) => {
2117 self.state.queue.reorder(files);
2118 self.sync(false);
2119 }
2120 UserEvent::Thumb {
2121 path,
2122 px,
2123 mtime,
2124 result,
2125 } => self.on_thumb(path, px, mtime, result),
2126 UserEvent::Refined { path, result } => self.on_refined(path, result),
2127 UserEvent::Command { id, event } => self.on_command(id, event, event_loop),
2128 UserEvent::Refreshed(found) => self.on_refreshed(found),
2129 UserEvent::Ipc(inc) => self.on_ipc(inc, event_loop),
2130 UserEvent::Info(info) => {
2131 if self.info_loading.as_ref() == Some(&info.path) {
2132 self.info_loading = None;
2133 }
2134 if self.state.queue.current() == Some(info.path.as_path()) {
2135 self.state.info = Some(info);
2136 self.request_redraw();
2137 }
2138 }
2139 UserEvent::InfoLine { path, line } => {
2140 self.state.info_line = Some((path, line));
2141 self.request_redraw();
2142 }
2143 UserEvent::Dir(changes) => self.on_dir(changes),
2144 UserEvent::Opened { arg, scan } => self.on_opened(arg, scan),
2145 }
2146 }
2147
2148 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
2149 match event {
2150 WindowEvent::CloseRequested => event_loop.exit(),
2151 WindowEvent::Resized(size) => {
2152 let Some(g) = &mut self.gfx else { return };
2153 g.renderer.resize(size.width, size.height);
2154 let sf = g.window.scale_factor() as f32;
2155 self.state
2156 .set_window(size.width as f32, size.height as f32, sf);
2157 self.sync_thumbs();
2158 self.request_redraw();
2159 }
2160 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2161 let [w, h] = self.state.window();
2162 self.state.set_window(w, h, scale_factor as f32);
2163 self.sync_thumbs();
2164 self.request_redraw();
2165 }
2166 WindowEvent::RedrawRequested => self.redraw(),
2167 WindowEvent::ModifiersChanged(m) => self.modifiers = m.state(),
2168 WindowEvent::Focused(false) => {
2169 self.held.clear();
2170 self.state.stop_all_pans();
2171 self.dragging = false;
2172 }
2173 WindowEvent::KeyboardInput {
2174 event,
2175 is_synthetic: false,
2176 ..
2177 } => {
2178 self.on_key(event, event_loop);
2179 }
2180 WindowEvent::CursorMoved { position, .. } => {
2181 let p = [position.x as f32, position.y as f32];
2182 let delta = [p[0] - self.cursor[0], p[1] - self.cursor[1]];
2183 self.cursor = p;
2184 if self.dragging && self.state.in_grid() {
2185 let moved = self.state.grid_drag(delta[1], Instant::now());
2186 self.after_grid_scroll(moved);
2187 } else if self.dragging && self.state.shown.is_some() {
2188 self.state.view.pan_pixels(delta);
2189 self.request_redraw();
2190 }
2191 }
2192 WindowEvent::MouseInput { state, button, .. } => {
2193 let pressed = state == ElementState::Pressed;
2194 if button == MouseButton::Left {
2195 // Drag pans; a press and release in place is a `<LeftMouse>` click.
2196 self.dragging = pressed;
2197 if pressed {
2198 self.press = Some(self.cursor);
2199 return;
2200 }
2201 let Some(p) = self.press.take() else { return };
2202 let moved = (self.cursor[0] - p[0]).hypot(self.cursor[1] - p[1]);
2203 if moved > CLICK_SLOP * self.state.scale_factor() {
2204 return;
2205 }
2206 if self.state.in_grid() {
2207 self.grid_click(event_loop);
2208 }
2209 } else if !pressed {
2210 return;
2211 }
2212 if let Some(key) = input::button(button, self.modifiers) {
2213 self.on_mouse_key(key, event_loop);
2214 }
2215 }
2216 WindowEvent::MouseWheel { delta, .. } => {
2217 let (d, pixels) = match delta {
2218 MouseScrollDelta::LineDelta(x, y) => ([x, y], false),
2219 MouseScrollDelta::PixelDelta(p) => (
2220 [p.x as f32 / PIXELS_PER_STEP, p.y as f32 / PIXELS_PER_STEP],
2221 true,
2222 ),
2223 };
2224 self.on_wheel(d, pixels, event_loop);
2225 }
2226 _ => {}
2227 }
2228 }
2229
2230 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
2231 if matches!(cause, StartCause::ResumeTimeReached { .. }) {
2232 self.request_redraw();
2233 }
2234 }
2235
2236 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
2237 let now = Instant::now();
2238 let busy = self.pump_uploads();
2239 if let Some(t) = self.ambiguous_since
2240 && now >= t + SEQUENCE_TIMEOUT
2241 {
2242 let runs = self.keymap.timeout(self.state.mode);
2243 self.update_pending(now);
2244 for run in runs {
2245 let _ = self.run(run.counted(), &Ctx::default(), event_loop);
2246 }
2247 }
2248 if let Some(t) = self.grid_rest
2249 && now >= t + GRID_REST
2250 {
2251 self.grid_rest = None;
2252 if self.state.in_grid()
2253 && let Some(cur) = self.state.queue.current().map(Path::to_path_buf)
2254 && !self.cache_has(&cur)
2255 {
2256 self.decode(&cur, Priority::Preload);
2257 }
2258 }
2259 if let Some(t) = self.which_key_since
2260 && now >= t + self.state.settings.which_key_delay
2261 {
2262 self.which_key_since = None;
2263 if self.keymap.is_pending() && self.state.input.is_none() {
2264 self.state.which_key = Some(self.which_key_rows());
2265 self.request_redraw();
2266 }
2267 }
2268 if self.state.slideshow_due(now) {
2269 let fx = self.state.apply(Action::Next(1), now);
2270 self.state.slideshow_advanced(fx.navigated);
2271 if !fx.navigated {
2272 self.state.notify("slideshow done", MsgKind::Info, now);
2273 }
2274 self.effects(fx, &Ctx::default(), event_loop);
2275 }
2276 self.publish();
2277 let (redraw, mut next) = self.state.tick(now);
2278 if let Some(t) = self.which_key_since {
2279 let at = t + self.state.settings.which_key_delay;
2280 next = Some(next.map_or(at, |n| n.min(at)));
2281 }
2282 if let Some(t) = self.grid_rest {
2283 let at = t + GRID_REST;
2284 next = Some(next.map_or(at, |n| n.min(at)));
2285 }
2286 if let Some(t) = self.ambiguous_since {
2287 let at = t + SEQUENCE_TIMEOUT;
2288 next = Some(next.map_or(at, |n| n.min(at)));
2289 }
2290 if let Some(at) = self.check_refine(now) {
2291 next = Some(next.map_or(at, |n| n.min(at)));
2292 }
2293 if redraw {
2294 self.request_redraw();
2295 }
2296 if !self.shown_window {
2297 let at = self.started + SHOW_TIMEOUT;
2298 next = Some(next.map_or(at, |n| n.min(at)));
2299 }
2300 event_loop.set_control_flow(match (busy, next) {
2301 (true, _) => ControlFlow::Poll,
2302 (false, Some(t)) => ControlFlow::WaitUntil(t),
2303 (false, None) => ControlFlow::Wait,
2304 });
2305 }
2306}
2307
2308#[cfg(test)]
2309mod tests {
2310 use super::*;
2311
2312 #[test]
2313 fn config_errors_are_shortened_for_the_bar() {
2314 assert_eq!(
2315 short_error(
2316 "/home/u/.config/remage/config.toml:2:1: unknown field `fitt`, expected one of \
2317 `fit`, `zoom_step`; did you mean `fit`?"
2318 ),
2319 "config.toml:2:1: unknown field `fitt`; did you mean `fit`?"
2320 );
2321 assert_eq!(
2322 short_error("a.toml:1:1: unknown field `x`, expected `y`"),
2323 "a.toml:1:1: unknown field `x`, expected `y`"
2324 );
2325 }
2326}