Krolyxon
Commit e515e5a

Branches

Tags

Refs →

remagesrcapp.rs

2336 lines · 85.7 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 px = self.state.cell_px();
524 if px != self.thumbs.cell {
525 for p in self.thumbs.requested.drain().map(|(p, _)| p) {
526 self.pool.cancel(&JobKey::Thumb(p));
527 }
528 self.thumbs.arrivals.clear();
529 self.state.thumbs.clear();
530 // Before the GPU exists, thumbnails wait in `arrivals` for an atlas of this size.
531 if let Some(g) = &mut self.gfx {
532 g.renderer.reset_thumbs(px);
533 }
534 self.thumbs.cell = px;
535 }
536 let l = self.state.grid_layout();
537 let visible = l.visible(self.state.grid.target(&l));
538 let span = l.page_rows() * l.cols;
539 let near = visible.start.saturating_sub(span)..(visible.end + 2 * span).min(l.count);
540 let q = &self.state.queue;
541 let mut wanted = HashSet::with_capacity(near.len());
542 let mut visible_paths = HashSet::with_capacity(visible.len());
543 let mut submit = Vec::new();
544 for i in near {
545 let Some(p) = q.get(i) else { continue };
546 wanted.insert(p.to_path_buf());
547 let prio = if visible.contains(&i) {
548 visible_paths.insert(p.to_path_buf());
549 Priority::ThumbVisible
550 } else {
551 Priority::Thumb
552 };
553 if !self.state.thumbs.contains_key(p) && self.thumbs.requested.get(p) != Some(&prio) {
554 submit.push((p.to_path_buf(), prio));
555 }
556 }
557 // Visible first, in order.
558 submit.sort_by_key(|(_, prio)| *prio);
559 for (p, prio) in submit {
560 self.request_thumb(p.clone(), prio);
561 self.thumbs.requested.insert(p, prio);
562 }
563 let mut demote = Vec::new();
564 self.thumbs.requested.retain(|p, prio| {
565 if !wanted.contains(p) {
566 self.pool.cancel(&JobKey::Thumb(p.clone()));
567 return false;
568 }
569 if *prio == Priority::ThumbVisible && !visible_paths.contains(p) {
570 *prio = Priority::Thumb;
571 demote.push(p.clone());
572 }
573 true
574 });
575 for p in demote {
576 self.request_thumb(p, Priority::Thumb);
577 }
578 self.thumbs.wanted = wanted;
579 }
580
581 fn request_thumb(&self, path: PathBuf, prio: Priority) {
582 let proxy = self.proxy.clone();
583 let cache = self.thumb_cache.clone();
584 let px = self.thumbs.cell;
585 self.pool
586 .submit(JobKey::Thumb(path.clone()), prio, move || {
587 let t = Instant::now();
588 let mtime = mtime(&path);
589 let result = thumbs::load(&path, px, &cache);
590 log::trace!("thumbnail {} in {:.1?}", path.display(), t.elapsed());
591 let _ = proxy.send_event(UserEvent::Thumb {
592 path,
593 px,
594 mtime,
595 result,
596 });
597 });
598 }
599
600 fn on_thumb(
601 &mut self,
602 path: PathBuf,
603 px: u32,
604 mtime: Option<SystemTime>,
605 result: Result<Thumb, ThumbError>,
606 ) {
607 self.thumbs.requested.remove(&path);
608 if px != self.thumbs.cell {
609 return;
610 }
611 match result {
612 Ok(t) => {
613 if let Some(m) = mtime {
614 self.stamps.insert(path.clone(), m);
615 }
616 self.thumbs.arrivals.push_back((path, t));
617 }
618 Err(e) => {
619 // Undecodable: it leaves the queue, as in image mode.
620 log::warn!("{}: {e}", path.display());
621 let was_current = self.state.queue.current() == Some(path.as_path());
622 if self.state.queue.remove(&path) {
623 self.state.skipped += 1;
624 }
625 self.sync(was_current);
626 self.request_redraw();
627 }
628 }
629 }
630
631 /// Every thumbnail on screen is in the atlas.
632 fn grid_settled(&self) -> bool {
633 let l = self.state.grid_layout();
634 let scroll = self.state.grid.scroll(&l, Instant::now());
635 l.visible(scroll).all(|i| {
636 self.state
637 .queue
638 .get(i)
639 .is_some_and(|p| self.state.thumbs.contains_key(p))
640 })
641 }
642
643 /// Upload `img` in full and put it on screen.
644 fn show(&mut self, path: &Path, img: Arc<DecodedImage>, now: Instant) {
645 let Some(g) = &mut self.gfx else {
646 // No GPU yet: keep it cached; `resumed` syncs again.
647 self.state.loading_since.get_or_insert(now);
648 return;
649 };
650 g.renderer.upload(path, &img, usize::MAX);
651 self.uploads.retain(|(p, _)| p != path);
652 self.state.show(path, img.source_size, img.format, now);
653 self.state.set_frames(img.delays(), img.animated, now);
654 self.request_info_line();
655 self.request_redraw();
656 }
657
658 fn update_title(&self) {
659 if let Some(g) = &self.gfx {
660 let title = match self.state.queue.current() {
661 Some(_) => {
662 ui::template::expand(&self.config.window.title, &|n| ui::var(&self.state, n))
663 }
664 None => "remage".into(),
665 };
666 g.window.set_title(&title);
667 }
668 }
669
670 /// Start, retarget or stop the directory watch to match `watch_dir` and the queue.
671 fn set_dir_watch(&mut self) {
672 let dirs = if self.config.general.watch_dir {
673 self.opts.dirs.clone()
674 } else {
675 Vec::new()
676 };
677 match &self.dir_watch {
678 Some(w) => w.set(dirs),
679 None if !dirs.is_empty() => {
680 let proxy = self.proxy.clone();
681 self.dir_watch = Some(watch::spawn(dirs, move |c| {
682 let _ = proxy.send_event(UserEvent::Dir(c));
683 }));
684 }
685 None => {}
686 }
687 }
688
689 /// Files changed on disk: new images join the queue, changed ones reload, gone ones leave.
690 fn on_dir(&mut self, c: watch::Changes) {
691 let known: HashSet<&PathBuf> = self.state.queue.all_files().iter().collect();
692 let (old, new): (Vec<_>, Vec<_>) =
693 c.present.into_iter().partition(|(p, _)| known.contains(p));
694 let mut found: Vec<(PathBuf, Option<SystemTime>)> =
695 old.into_iter().map(|(p, t)| (p, Some(t))).collect();
696 found.extend(c.gone.into_iter().map(|p| (p, None)));
697 if !found.is_empty() {
698 self.on_refreshed(found);
699 }
700 if !new.is_empty() {
701 log::debug!("{} new files on disk", new.len());
702 let was_empty = self.state.queue.is_empty();
703 self.state
704 .queue
705 .insert_sorted(new.into_iter().map(|(p, _)| p));
706 self.sync(was_empty);
707 }
708 }
709
710 /// `open <path>`: a file already queued is just selected; anything else is scanned
711 /// (with its siblings) off-thread and replaces the queue.
712 fn open(&mut self, arg: &str) {
713 let cwd = std::env::current_dir().unwrap_or_default();
714 let path = crate::config::expand_path(arg, &cwd);
715 if let Some(i) = self.state.queue.files().iter().position(|f| *f == path) {
716 let fx = self.state.apply(Action::Goto(i + 1), Instant::now());
717 if fx.navigated {
718 self.sync(true);
719 }
720 return;
721 }
722 let g = &self.config.general;
723 let opts = scan::ScanOptions {
724 recursive: g.recursive,
725 siblings: g.open_siblings,
726 sort: g.sort,
727 reverse: g.reverse,
728 };
729 let proxy = self.proxy.clone();
730 std::thread::spawn(move || {
731 let scan = scan::scan(std::slice::from_ref(&path), &opts);
732 let _ = proxy.send_event(UserEvent::Opened { arg: path, scan });
733 });
734 }
735
736 fn on_opened(&mut self, arg: PathBuf, scan: scan::Scan) {
737 let now = Instant::now();
738 if scan.files.is_empty() && scan.unknown.is_empty() {
739 let why = scan
740 .errors
741 .first()
742 .cloned()
743 .unwrap_or_else(|| format!("{}: no images", arg.display()));
744 self.state
745 .notify(format!("error: {why}"), MsgKind::Error, now);
746 self.request_redraw();
747 return;
748 }
749 log::debug!("opened {}: {} files", arg.display(), scan.files.len());
750 if !scan.unknown.is_empty() {
751 let proxy = self.proxy.clone();
752 let unknown = scan.unknown;
753 std::thread::spawn(move || {
754 let found: Vec<_> = unknown.into_iter().filter(|p| decode::sniff(p)).collect();
755 if !found.is_empty() {
756 let _ = proxy.send_event(UserEvent::Found(found));
757 }
758 });
759 }
760 self.state.queue = Queue::new(scan.files, scan.start.as_deref(), 0);
761 self.state.skipped = 0;
762 self.state.input = None;
763 self.opts.dirs = scan.dirs;
764 self.set_dir_watch();
765 if self.state.in_grid() {
766 let l = self.state.grid_layout();
767 self.state
768 .grid
769 .jump_to(l.follow(self.state.queue.index(), 0.0));
770 }
771 self.sync(true);
772 }
773
774 /// Read the current file's metadata for the info panel, off-thread.
775 fn request_info(&mut self) {
776 if !self.state.info_open {
777 return;
778 }
779 let Some(cur) = self.state.queue.current().map(Path::to_path_buf) else {
780 return;
781 };
782 if self.state.info.as_ref().is_some_and(|i| i.path == cur)
783 || self.info_loading.as_ref() == Some(&cur)
784 {
785 return;
786 }
787 self.info_loading = Some(cur.clone());
788 let proxy = self.proxy.clone();
789 std::thread::spawn(move || {
790 let rows = decode::meta::read(&cur);
791 let _ = proxy.send_event(UserEvent::Info(Info { path: cur, rows }));
792 });
793 }
794
795 /// Run `ui.info_command` for the image on screen (nsxiv's image-info: file, width,
796 /// height); its last line fills `{info}`.
797 fn request_info_line(&mut self) {
798 let cmd = self.config.ui.info_command.trim();
799 let Some(shown) = &self.state.shown else {
800 return;
801 };
802 if cmd.is_empty() || self.info_line_for.as_ref() == Some(&shown.path) {
803 return;
804 }
805 let path = shown.path.clone();
806 self.info_line_for = Some(path.clone());
807 let mut c = std::process::Command::new("sh");
808 c.arg("-c")
809 .arg(format!("{cmd} \"$@\""))
810 .arg("remage")
811 .arg(&path)
812 .arg(shown.size[0].to_string())
813 .arg(shown.size[1].to_string())
814 .stdin(std::process::Stdio::null())
815 .stderr(std::process::Stdio::null());
816 if let Some(sock) = &self.opts.socket {
817 c.env("REMAGE_SOCKET", sock);
818 }
819 let proxy = self.proxy.clone();
820 std::thread::spawn(move || {
821 let line = match c.output() {
822 Ok(o) => String::from_utf8_lossy(&o.stdout)
823 .lines()
824 .rev()
825 .find(|l| !l.trim().is_empty())
826 .unwrap_or("")
827 .trim()
828 .to_string(),
829 Err(e) => {
830 log::warn!("info_command: {e}");
831 String::new()
832 }
833 };
834 let _ = proxy.send_event(UserEvent::InfoLine { path, line });
835 });
836 }
837
838 /// What a binding does, in a few words: a command's own `desc` for `run`.
839 fn describe(&self, actions: &[Action]) -> String {
840 let one = |a: &Action| match a {
841 Action::Run(name) => self
842 .config
843 .commands
844 .get(name)
845 .map(|c| c.desc.clone())
846 .filter(|d| !d.is_empty())
847 .unwrap_or_else(|| a.description()),
848 _ => a.description(),
849 };
850 actions.iter().map(one).collect::<Vec<_>>().join(" · ")
851 }
852
853 /// The keys bound to exactly `action` in the current mode, keyboard first.
854 fn keys_for(&self, action: &Action) -> String {
855 let mut keys = self.keymap.keys_for(self.state.mode, action);
856 keys.sort_by_key(|k| {
857 k.0.iter()
858 .any(|k| matches!(k.code, Code::Mouse(_) | Code::Scroll(_)))
859 });
860 keys.iter()
861 .take(2)
862 .map(|k| ui::pretty_keys(&k.to_string()))
863 .collect::<Vec<_>>()
864 .join(" ")
865 }
866
867 /// Palette rows: user commands, then every action (each argument choice spelled out,
868 /// placeholders like `goto N` left to complete).
869 fn palette_items(&self) -> Vec<PaletteItem> {
870 let mut seen = HashSet::new();
871 let mut items = Vec::new();
872 let mut push = |items: &mut Vec<PaletteItem>,
873 action: String,
874 desc: String,
875 keys: String,
876 template: bool| {
877 if seen.insert(action.clone()) {
878 items.push(PaletteItem {
879 action,
880 desc,
881 keys,
882 template,
883 });
884 }
885 };
886 for (name, cmd) in &self.config.commands {
887 let a = Action::Run(name.clone());
888 let desc = if cmd.desc.is_empty() {
889 format!("Run {name}")
890 } else {
891 cmd.desc.clone()
892 };
893 push(&mut items, a.to_string(), desc, self.keys_for(&a), false);
894 }
895 for entry in CATALOG {
896 if entry.name == "run" {
897 continue;
898 }
899 let args = entry.args.trim_start_matches('[').trim_end_matches(']');
900 // Optional arguments (`[n]`, `[toggle|on|off]`) also offer the bare name.
901 let mut options: Vec<&str> = vec![""];
902 if !entry.args.is_empty() {
903 if !entry.args.starts_with('[') {
904 options.clear();
905 }
906 options.extend(
907 args.split('|')
908 .map(|o| o.trim_end_matches(" [n]").trim_end_matches(']'))
909 .filter(|o| *o != "n"),
910 );
911 }
912 for opt in options {
913 let text = format!("{} {opt}", entry.name);
914 let required = !entry.args.is_empty() && !entry.args.starts_with('[');
915 let placeholder = opt.contains(['<', 'N']);
916 match text.trim().parse::<Action>() {
917 Ok(a) if !placeholder => {
918 let desc = a.description();
919 push(&mut items, a.to_string(), desc, self.keys_for(&a), false);
920 }
921 _ if required || placeholder => {
922 push(
923 &mut items,
924 text.trim().to_string(),
925 entry.desc.to_string(),
926 String::new(),
927 true,
928 );
929 }
930 _ => {}
931 }
932 }
933 }
934 items
935 }
936
937 /// Help rows: the live keymap of the current mode, grouped, keys for one action merged.
938 fn help_rows(&self) -> Vec<HelpRow> {
939 const GROUPS: [&str; 8] = [
940 "Navigation",
941 "Marks",
942 "Grid",
943 "View",
944 "Playback",
945 "Interface",
946 "Queue",
947 "App",
948 ];
949 let rank = |a: &Action| {
950 CATALOG
951 .iter()
952 .position(|e| e.name == a.name())
953 .unwrap_or(usize::MAX)
954 };
955 // (group, catalog rank, action, keys, description)
956 let mut entries: Vec<(usize, usize, String, String, String)> = Vec::new();
957 for (keys, actions) in self.keymap.bindings(self.state.mode) {
958 let Some(first) = actions.first() else {
959 continue;
960 };
961 let group = GROUPS.iter().position(|g| *g == first.group()).unwrap_or(0);
962 let desc = self.describe(&actions);
963 let k = ui::pretty_keys(&keys.to_string());
964 match entries.iter_mut().find(|e| e.0 == group && e.4 == desc) {
965 Some(e) => {
966 e.3.push_str(" ");
967 e.3.push_str(&k);
968 }
969 None => entries.push((group, rank(first), first.to_string(), k, desc)),
970 }
971 }
972 // Within a group: catalog order, then by action (`next` before `next 10`).
973 entries.sort_by(|a, b| (a.0, a.1, &a.2).cmp(&(b.0, b.1, &b.2)));
974 let mut rows = Vec::new();
975 let mut last = None;
976 for (group, _, _, keys, desc) in entries {
977 if last != Some(group) {
978 rows.push(HelpRow::Header(GROUPS[group].to_string()));
979 last = Some(group);
980 }
981 rows.push(HelpRow::Binding { keys, desc });
982 }
983 rows
984 }
985
986 /// Which-key rows for the pending sequence.
987 fn which_key_rows(&self) -> Vec<WhichRow> {
988 let mode = self.state.mode;
989 let pending = self.keymap.pending_keys().to_vec();
990 let all = self.keymap.bindings(mode);
991 self.keymap
992 .candidates(mode)
993 .into_iter()
994 .map(|c| {
995 let desc = match &c.actions {
996 Some(a) => self.describe(a),
997 None => {
998 let mut prefix = pending.clone();
999 prefix.push(c.key);
1000 let n = all.iter().filter(|(k, _)| k.0.starts_with(&prefix)).count();
1001 format!("+{n} more")
1002 }
1003 };
1004 WhichRow {
1005 key: ui::pretty_keys(&KeySeq(vec![c.key]).to_string()),
1006 desc,
1007 prefix: c.prefix && c.actions.is_none(),
1008 }
1009 })
1010 .collect()
1011 }
1012
1013 /// Show a config error until the config is fixed.
1014 pub fn config_failed(&mut self, message: &str) {
1015 self.state.notify_for(
1016 format!("config error: {}", short_error(message)),
1017 MsgKind::Error,
1018 Instant::now(),
1019 CONFIG_ERROR_TIME,
1020 );
1021 self.config_error = true;
1022 self.request_redraw();
1023 }
1024
1025 /// Adopt a reloaded config. Runtime toggles (bar, filter, alpha background) only change
1026 /// when their config value did, so a reload doesn't undo what the user just toggled.
1027 fn apply_config(&mut self, loaded: Loaded) {
1028 let now = Instant::now();
1029 for w in &loaded.warnings {
1030 log::warn!("{w}");
1031 }
1032 let old = std::mem::replace(&mut self.config, loaded.config);
1033 let new = &self.config;
1034 decode::set_decoders(&new.decoders);
1035 let mut settings = Settings::from_config(new);
1036 self.opts.overrides.apply(&mut settings);
1037 let preload_changed = settings.preload != self.state.settings.preload;
1038 if old.grid.size != new.grid.size {
1039 self.state.grid.size = new.grid.size;
1040 }
1041 let watch_changed = old.general.watch_dir != new.general.watch_dir;
1042 if old.grid.thumbnail_cache != new.grid.thumbnail_cache {
1043 self.thumb_cache = CacheOptions::new(new.grid.thumbnail_cache, self.opts.private);
1044 }
1045 self.state.set_settings(settings);
1046 self.state.set_theme(loaded.theme, now);
1047 self.keymap = keymap(&loaded.keys);
1048 self.ambiguous_since = None;
1049 self.state.pending.clear();
1050 self.cache.set_budget(new.image.cache_mb << 20);
1051 if old.image.filter != new.image.filter {
1052 self.state.nearest = new.image.filter.nearest();
1053 }
1054 if old.image.alpha_background != new.image.alpha_background {
1055 self.state.alpha_bg = new.image.alpha_background;
1056 }
1057 if old.ui.statusbar != new.ui.statusbar && self.opts.overrides.bar.is_none() {
1058 let s = if new.ui.statusbar {
1059 Switch::On
1060 } else {
1061 Switch::Off
1062 };
1063 self.state.apply(Action::Bar(s), now);
1064 }
1065 if let Some(g) = &mut self.gfx {
1066 if old.window.decorations != new.window.decorations {
1067 g.window.set_decorations(new.window.decorations);
1068 }
1069 g.renderer.set_font_families(font_families(new));
1070 }
1071 if watch_changed {
1072 self.set_dir_watch();
1073 }
1074 self.info_line_for = None;
1075 self.request_info_line();
1076 self.update_title();
1077 // Silent on routine changes (a new wallpaper palette); confirm fixes and manual reloads.
1078 if std::mem::take(&mut self.config_error) || std::mem::take(&mut self.reload_requested) {
1079 self.state.notify("config reloaded", MsgKind::Info, now);
1080 }
1081 if preload_changed {
1082 self.sync(false);
1083 }
1084 self.sync_thumbs();
1085 self.request_redraw();
1086 }
1087
1088 fn on_decoded(
1089 &mut self,
1090 path: PathBuf,
1091 mtime: Option<SystemTime>,
1092 result: Result<Arc<DecodedImage>, String>,
1093 ) {
1094 lock(&self.arriving).remove(&path);
1095 let now = Instant::now();
1096 let is_current = self.state.queue.current() == Some(path.as_path());
1097 let in_grid = self.state.in_grid();
1098 match result {
1099 Ok(img) => {
1100 if let Some(m) = mtime {
1101 self.stamps.insert(path.clone(), m);
1102 }
1103 let wanted = self.window_paths();
1104 let keep: Vec<&Path> = wanted.iter().map(PathBuf::as_path).collect();
1105 self.cache.insert(path.clone(), img.clone(), &keep);
1106 if in_grid {
1107 // Cached for when the grid opens it.
1108 } else if is_current {
1109 // Already on screen means this was a reload: replace the texture.
1110 let reload = self.state.shown.as_ref().is_some_and(|s| s.path == path);
1111 if reload && let Some(g) = &mut self.gfx {
1112 g.renderer.retain(|p| p != path);
1113 }
1114 self.show(&path, img, now);
1115 } else if wanted.contains(&path) && !self.uploads.iter().any(|(p, _)| *p == path) {
1116 self.uploads.push_back((path, img));
1117 }
1118 }
1119 Err(e) => {
1120 log::warn!("{}: {e}", path.display());
1121 self.state.queue.remove(&path);
1122 self.state.skipped += 1;
1123 if is_current {
1124 self.state.loading_since = None;
1125 let msg = format!("error: {}: {e}", ui::file_name(&path));
1126 self.state.notify(msg, MsgKind::Error, now);
1127 self.sync(true);
1128 }
1129 self.request_redraw();
1130 }
1131 }
1132 }
1133
1134 fn reload(&mut self) {
1135 if let Some(cur) = self.state.queue.current().map(Path::to_path_buf) {
1136 self.cache.remove(&cur);
1137 self.decode(&cur, Priority::Current);
1138 }
1139 }
1140
1141 /// Run actions in order. A `run` suspends the rest of the chain until its command exits
1142 /// successfully. Errors are shown on the status line; the first is also returned for IPC.
1143 fn run(&mut self, actions: Vec<Action>, ctx: &Ctx, el: &ActiveEventLoop) -> Result<(), String> {
1144 let mut rest = actions.into_iter();
1145 while let Some(action) = rest.next() {
1146 match &action {
1147 Action::Run(name) => return self.start_command(name, rest.collect()),
1148 Action::Cancel => {
1149 self.keymap.cancel();
1150 self.state.pending.clear();
1151 self.config_error = false;
1152 self.answer(false);
1153 }
1154 Action::QuitNoOutput => self.no_output = true,
1155 _ => {}
1156 }
1157 let fx = self.state.apply_at(action, ctx.anchor, Instant::now());
1158 self.effects(fx, ctx, el);
1159 if fx.quit {
1160 break;
1161 }
1162 }
1163 Ok(())
1164 }
1165
1166 /// Start `[commands.<name>]`, or ask first when it has a `confirm`.
1167 fn start_command(&mut self, name: &str, then: Vec<Action>) -> Result<(), String> {
1168 let now = Instant::now();
1169 let fail = |state: &mut AppState, msg: String| {
1170 state.notify(format!("error: {msg}"), MsgKind::Error, now);
1171 Err(msg)
1172 };
1173 let Some(cmd) = self.config.commands.get(name).cloned() else {
1174 let hint = suggest(name, self.config.commands.keys().map(String::as_str))
1175 .map(|s| format!(", did you mean `{s}`?"))
1176 .unwrap_or_default();
1177 self.request_redraw();
1178 return fail(&mut self.state, format!("no command named `{name}`{hint}"));
1179 };
1180 let q = &self.state.queue;
1181 let targets: Arc<[PathBuf]> =
1182 exec::targets(cmd.targets, q.current(), &q.marked(), q.files()).into();
1183 if targets.is_empty() && exec::needs_files(&cmd) {
1184 let why = if cmd.targets == Targets::Marked {
1185 "no marked files"
1186 } else {
1187 "no images"
1188 };
1189 self.request_redraw();
1190 return fail(&mut self.state, format!("{name}: {why}"));
1191 }
1192 let size = self
1193 .state
1194 .shown
1195 .as_ref()
1196 .filter(|s| Some(s.path.as_path()) == q.current())
1197 .map(|s| s.size);
1198 let ctx = exec::Context {
1199 current: q.current(),
1200 index: q.index(),
1201 targets: &targets,
1202 size,
1203 mode: self.state.mode.name(),
1204 socket: self.opts.socket.as_deref(),
1205 };
1206 let prepared = exec::prepare(&cmd, &ctx);
1207 let question = cmd.confirm.as_deref().map(|c| exec::expand_text(c, &ctx));
1208 let mut chain = cmd.after.0.unwrap_or_default();
1209 chain.extend(then);
1210 let job = Job {
1211 name: name.to_string(),
1212 output: cmd.output,
1213 targets,
1214 then: chain,
1215 lines: 0,
1216 };
1217 match question {
1218 Some(q) => {
1219 self.answer(false);
1220 self.state.prompt = Some(q);
1221 self.confirm = Some((job, prepared));
1222 self.request_redraw();
1223 }
1224 None => self.spawn_job(job, prepared),
1225 }
1226 Ok(())
1227 }
1228
1229 /// Settle a confirm prompt: `y` runs the command, anything else drops it.
1230 fn answer(&mut self, yes: bool) {
1231 let Some((job, prepared)) = self.confirm.take() else {
1232 return;
1233 };
1234 self.state.prompt = None;
1235 if yes {
1236 self.spawn_job(job, prepared);
1237 }
1238 self.request_redraw();
1239 }
1240
1241 fn spawn_job(&mut self, job: Job, prepared: exec::Prepared) {
1242 let id = self.next_job;
1243 self.next_job += 1;
1244 log::info!(
1245 "run {}: {} {:?}",
1246 job.name,
1247 prepared.program.to_string_lossy(),
1248 prepared.args
1249 );
1250 let proxy = self.proxy.clone();
1251 exec::spawn(&job.name, prepared, job.output, move |event| {
1252 let _ = proxy.send_event(UserEvent::Command { id, event });
1253 });
1254 self.state.running.push((id, job.name.clone()));
1255 self.jobs.insert(id, job);
1256 self.request_redraw();
1257 }
1258
1259 fn on_command(&mut self, id: u64, event: exec::Event, el: &ActiveEventLoop) {
1260 let now = Instant::now();
1261 match event {
1262 exec::Event::Line(line) => {
1263 let Some(job) = self.jobs.get_mut(&id) else {
1264 return;
1265 };
1266 job.lines += 1;
1267 let name = job.name.clone();
1268 let ctx = Ctx {
1269 anchor: None,
1270 targets: Some(job.targets.clone()),
1271 };
1272 match line.parse::<Action>() {
1273 Ok(a) => {
1274 let _ = self.run(vec![a], &ctx, el);
1275 }
1276 Err(e) => {
1277 log::warn!("{name}: {line}: {e}");
1278 self.state
1279 .notify(format!("error: {name}: {e}"), MsgKind::Error, now);
1280 self.request_redraw();
1281 }
1282 }
1283 }
1284 exec::Event::Done(outcome) => {
1285 let Some(job) = self.jobs.remove(&id) else {
1286 return;
1287 };
1288 self.state.running.retain(|(j, _)| *j != id);
1289 self.request_redraw();
1290 if !outcome.success() {
1291 let msg = format!("error: {}", outcome.error());
1292 log::warn!("{}: {msg}", job.name);
1293 self.state.notify(msg, MsgKind::Error, now);
1294 return;
1295 }
1296 log::debug!("{}: done", job.name);
1297 match (job.output, outcome.last_line) {
1298 (Output::Status, Some(line)) => self.state.notify(line, MsgKind::Info, now),
1299 // Actions speak for themselves.
1300 (Output::Actions, _) if job.lines > 0 => {}
1301 _ => self.state.notify("done", MsgKind::Info, now),
1302 }
1303 let ctx = Ctx {
1304 anchor: None,
1305 targets: Some(job.targets),
1306 };
1307 let _ = self.run(job.then, &ctx, el);
1308 }
1309 }
1310 }
1311
1312 /// Re-check `targets` (default: the whole queue, plus the current file) on disk, off-thread.
1313 fn refresh(&mut self, targets: Option<Arc<[PathBuf]>>) {
1314 let mut files: Vec<PathBuf> = match targets {
1315 Some(t) => t.to_vec(),
1316 None => self.state.queue.all_files().to_vec(),
1317 };
1318 if let Some(cur) = self.state.queue.current()
1319 && !files.iter().any(|f| f == cur)
1320 {
1321 files.push(cur.to_path_buf());
1322 }
1323 let proxy = self.proxy.clone();
1324 std::thread::spawn(move || {
1325 let found = files
1326 .into_iter()
1327 .map(|p| {
1328 let m = std::fs::metadata(&p)
1329 .ok()
1330 .map(|m| m.modified().unwrap_or(SystemTime::UNIX_EPOCH));
1331 (p, m)
1332 })
1333 .collect();
1334 let _ = proxy.send_event(UserEvent::Refreshed(found));
1335 });
1336 }
1337
1338 /// Drop files that vanished and reload ones that changed since they were loaded.
1339 fn on_refreshed(&mut self, found: Vec<(PathBuf, Option<SystemTime>)>) {
1340 let cur = self.state.queue.current().map(Path::to_path_buf);
1341 let showing = |s: &AppState, p: &Path| {
1342 s.mode == Mode::Image && s.shown.as_ref().is_some_and(|s| s.path == p)
1343 };
1344 let mut navigated = false;
1345 let mut stale = Vec::new();
1346 let mut gone = 0;
1347 for (path, mtime) in found {
1348 match mtime {
1349 None => {
1350 if self.state.queue.remove(&path) {
1351 gone += 1;
1352 navigated |= cur.as_ref() == Some(&path);
1353 }
1354 self.cache.remove(&path);
1355 self.stamps.remove(&path);
1356 self.state.thumbs.remove(&path);
1357 }
1358 Some(t) if self.stamps.get(&path).is_some_and(|s| *s != t) => stale.push(path),
1359 Some(_) => {}
1360 }
1361 }
1362 let mut reload_current = false;
1363 for path in &stale {
1364 self.cache.remove(path);
1365 self.stamps.remove(path);
1366 self.state.thumbs.remove(path);
1367 self.uploads.retain(|(p, _)| p != path);
1368 if cur.as_ref() == Some(path) && showing(&self.state, path) {
1369 // Keep it on screen until the new version replaces it.
1370 reload_current = true;
1371 } else if let Some(g) = &mut self.gfx {
1372 g.renderer.retain(|p| p != path);
1373 }
1374 }
1375 if gone + stale.len() > 0 {
1376 log::debug!("refresh: {gone} gone, {} changed", stale.len());
1377 }
1378 if reload_current && let Some(cur) = &cur {
1379 self.decode(cur, Priority::Current);
1380 }
1381 if gone + stale.len() > 0 {
1382 self.sync(navigated);
1383 self.request_redraw();
1384 }
1385 }
1386
1387 fn on_ipc(&mut self, inc: ipc::Incoming, el: &ActiveEventLoop) {
1388 let reply = match inc.request {
1389 Request::Actions(actions) => match self.run(actions, &Ctx::default(), el) {
1390 Ok(()) => ipc::ok(Value::Null),
1391 Err(e) => ipc::error(&e),
1392 },
1393 Request::Query(q) => ipc::ok(self.query(q)),
1394 Request::Subscribe(topics) => match inc.events {
1395 Some(tx) => {
1396 if self.published.is_none() {
1397 self.published = Some(self.snapshot());
1398 }
1399 self.subscribers.push(Subscriber { topics, tx });
1400 ipc::ok(Value::Null)
1401 }
1402 None => ipc::error("no event channel"),
1403 },
1404 };
1405 let _ = inc.reply.send(reply);
1406 }
1407
1408 fn query(&self, q: Query) -> Value {
1409 let s = &self.state;
1410 let paths = |ps: &mut dyn Iterator<Item = &Path>| -> Value {
1411 ps.map(ipc::path_json).collect::<Vec<_>>().into()
1412 };
1413 match q {
1414 Query::Current => s.queue.current().map_or(Value::Null, ipc::path_json),
1415 Query::Marked => paths(&mut s.queue.marked().into_iter()),
1416 Query::Queue => paths(&mut s.queue.files().iter().map(PathBuf::as_path)),
1417 Query::State => {
1418 let shown = s
1419 .shown
1420 .as_ref()
1421 .filter(|sh| Some(sh.path.as_path()) == s.queue.current());
1422 let fullscreen = self
1423 .gfx
1424 .as_ref()
1425 .is_some_and(|g| g.window.fullscreen().is_some());
1426 json!({
1427 "mode": s.mode.name(),
1428 "current": s.queue.current().map(ipc::path_json),
1429 "index": (!s.queue.is_empty()).then(|| s.queue.index() + 1),
1430 "count": s.queue.len(),
1431 "marked": s.queue.marked_count(),
1432 "width": shown.map(|sh| sh.size[0]),
1433 "height": shown.map(|sh| sh.size[1]),
1434 "format": shown.map(|sh| sh.format),
1435 "zoom": shown.map(|_| (s.view.scale() * 1000.0).round() / 10.0),
1436 "fit": s.settings.fit,
1437 "bar": s.bar,
1438 "fullscreen": fullscreen,
1439 "skipped": s.skipped,
1440 "filter": s.queue.filter(),
1441 "slideshow": s.slideshow.is_some(),
1442 "running": s.running.iter().map(|(_, n)| n.as_str()).collect::<Vec<_>>(),
1443 "pid": std::process::id(),
1444 })
1445 }
1446 }
1447 }
1448
1449 fn snapshot(&self) -> Published {
1450 let q = &self.state.queue;
1451 Published {
1452 image: (q.current().map(Path::to_path_buf), q.index(), q.len()),
1453 marks: q.marks_version(),
1454 mode: self.state.mode,
1455 }
1456 }
1457
1458 /// Tell subscribers what changed since they last heard.
1459 fn publish(&mut self) {
1460 if self.subscribers.is_empty() {
1461 self.published = None;
1462 return;
1463 }
1464 let now = self.snapshot();
1465 let Some(before) = self.published.replace(now.clone()) else {
1466 return;
1467 };
1468 let q = &self.state.queue;
1469 let mut events = Vec::new();
1470 if before.image != now.image {
1471 let data = json!({
1472 "path": q.current().map(ipc::path_json),
1473 "index": (!q.is_empty()).then(|| q.index() + 1),
1474 "count": q.len(),
1475 });
1476 events.push((Topic::Image, data));
1477 }
1478 if before.marks != now.marks {
1479 let files: Vec<Value> = q.marked().into_iter().map(ipc::path_json).collect();
1480 events.push((
1481 Topic::Marks,
1482 json!({ "count": files.len(), "files": files }),
1483 ));
1484 }
1485 if before.mode != now.mode {
1486 events.push((Topic::Mode, json!(now.mode.name())));
1487 }
1488 for (topic, data) in events {
1489 let line = ipc::event(topic, data);
1490 self.subscribers
1491 .retain(|s| !s.topics.contains(&topic) || s.tx.send(line.clone()).is_ok());
1492 }
1493 }
1494
1495 /// Marked files for `-o`, unless the user quit with `quit-no-output`.
1496 pub fn marked_output(&self) -> Option<Vec<PathBuf>> {
1497 (!self.no_output).then(|| {
1498 self.state
1499 .queue
1500 .marked()
1501 .into_iter()
1502 .map(Path::to_path_buf)
1503 .collect()
1504 })
1505 }
1506
1507 fn effects(&mut self, fx: Effects, ctx: &Ctx, event_loop: &ActiveEventLoop) {
1508 if fx.quit {
1509 event_loop.exit();
1510 return;
1511 }
1512 if let Some(s) = fx.fullscreen
1513 && let Some(g) = &self.gfx
1514 {
1515 let on = g.window.fullscreen().is_some();
1516 let want = match s {
1517 Switch::Toggle => !on,
1518 Switch::On => true,
1519 Switch::Off => false,
1520 };
1521 if want != on {
1522 g.window
1523 .set_fullscreen(want.then_some(Fullscreen::Borderless(None)));
1524 }
1525 }
1526 if fx.reload {
1527 self.reload();
1528 }
1529 if fx.palette {
1530 let items = self.palette_items();
1531 self.keymap.cancel();
1532 self.state.open_palette(items);
1533 }
1534 if fx.help {
1535 let rows = self.help_rows();
1536 self.state.help = Some(Help { rows, scroll: 0 });
1537 }
1538 if fx.info {
1539 self.request_info();
1540 }
1541 if fx.open
1542 && let Some(p) = self.state.open.take()
1543 {
1544 self.open(&p);
1545 }
1546 if fx.reload_config {
1547 self.reload_requested = true;
1548 self.watch.reload();
1549 }
1550 if fx.refresh {
1551 self.refresh(ctx.targets.clone());
1552 }
1553 if let Some(key) = fx.sort {
1554 let mut files = self.state.queue.files().to_vec();
1555 let proxy = self.proxy.clone();
1556 std::thread::spawn(move || {
1557 scan::sort_paths(&mut files, key, false);
1558 let _ = proxy.send_event(UserEvent::Sorted(files));
1559 });
1560 }
1561 if fx.mode {
1562 // Opening an image from the grid loads it like navigating to it.
1563 self.sync(self.state.mode == Mode::Image || fx.navigated);
1564 } else if fx.navigated {
1565 self.sync(true);
1566 } else if fx.reordered {
1567 self.sync(false);
1568 }
1569 // Scrolling, a new size or the bar can bring new thumbnails into view.
1570 self.sync_thumbs();
1571 if fx.redraw || fx.layout {
1572 self.request_redraw();
1573 }
1574 }
1575
1576 /// Mirror the keymap's pending keys on the status line and arm the ambiguity timeout.
1577 fn update_pending(&mut self, now: Instant) {
1578 let pending = self.keymap.pending_display();
1579 if pending != self.state.pending {
1580 self.state.pending = pending;
1581 self.request_redraw();
1582 if !self.keymap.is_pending() {
1583 self.which_key_since = None;
1584 self.state.which_key = None;
1585 } else if self.state.which_key.is_some() {
1586 // Already showing: follow the sequence at once.
1587 self.state.which_key = Some(self.which_key_rows());
1588 } else {
1589 self.which_key_since = Some(now);
1590 }
1591 }
1592 self.ambiguous_since = self
1593 .keymap
1594 .is_ambiguous(self.state.mode)
1595 .then(|| self.ambiguous_since.unwrap_or(now));
1596 }
1597
1598 fn on_key(&mut self, event: winit::event::KeyEvent, event_loop: &ActiveEventLoop) {
1599 let now = Instant::now();
1600 if event.state == ElementState::Released {
1601 if let Some(dir) = self.held.remove(&event.physical_key) {
1602 self.state.hold_pan(dir, false, now);
1603 self.request_redraw();
1604 }
1605 if self
1606 .repeat
1607 .as_ref()
1608 .is_some_and(|(k, _)| *k == event.physical_key)
1609 {
1610 self.repeat = None;
1611 }
1612 return;
1613 }
1614 if self.confirm.is_some() {
1615 // The prompt takes the next key: `y` runs the command, anything else cancels.
1616 if !event.repeat
1617 && let Some(key) = input::key(&event.logical_key, self.modifiers)
1618 {
1619 self.answer(key == Key::plain('y') || key == Key::plain('Y'));
1620 }
1621 return;
1622 }
1623 if self.state.input.is_some() {
1624 self.on_input_key(&event, event_loop);
1625 return;
1626 }
1627 if self.state.help.is_some()
1628 && !event.repeat
1629 && let Some(key) = input::key(&event.logical_key, self.modifiers)
1630 && !self.keymap.is_pending()
1631 && self.help_key(key)
1632 {
1633 return;
1634 }
1635 if event.repeat {
1636 // Repeats bypass the trie: only a completed, repeatable binding repeats.
1637 if let Some((k, actions)) = &self.repeat
1638 && *k == event.physical_key
1639 {
1640 let actions = actions.clone();
1641 let _ = self.run(actions, &Ctx::default(), event_loop);
1642 }
1643 return;
1644 }
1645 let Some(key) = input::key(&event.logical_key, self.modifiers) else {
1646 return;
1647 };
1648 let runs = self.keymap.feed(self.state.mode, key);
1649 self.update_pending(now);
1650 self.repeat = None;
1651 for run in runs {
1652 let actions = run.counted();
1653 if let [Action::Pan(dir)] = actions[..]
1654 && self.state.mode == Mode::Image
1655 {
1656 if self.state.shown.is_some() {
1657 self.held.insert(event.physical_key, dir);
1658 self.state.hold_pan(dir, true, now);
1659 self.request_redraw();
1660 }
1661 continue;
1662 }
1663 if !actions.is_empty() && actions.iter().all(Action::repeats) {
1664 self.repeat = Some((event.physical_key, actions.clone()));
1665 }
1666 let _ = self.run(actions, &Ctx::default(), event_loop);
1667 }
1668 }
1669
1670 /// A key while the palette or search line is open: it edits the line.
1671 fn on_input_key(&mut self, event: &winit::event::KeyEvent, el: &ActiveEventLoop) {
1672 let now = Instant::now();
1673 let Some(key) = input::key(&event.logical_key, self.modifiers) else {
1674 return;
1675 };
1676 let ctrl = |c: char| {
1677 Key::new(
1678 input::keys::Mods {
1679 ctrl: true,
1680 ..Default::default()
1681 },
1682 Code::Char(c),
1683 )
1684 };
1685 let named = |n: Named| Key::new(Default::default(), Code::Named(n));
1686 let shift_tab = Key::new(
1687 input::keys::Mods {
1688 shift: true,
1689 ..Default::default()
1690 },
1691 Code::Named(Named::Tab),
1692 );
1693 let palette = self
1694 .state
1695 .input
1696 .as_ref()
1697 .is_some_and(|i| i.kind == InputKind::Palette);
1698 let empty = self.state.input.as_ref().is_some_and(|i| i.text.is_empty());
1699 let edit = if key == named(Named::Esc) || key == ctrl('c') || key == ctrl('[') {
1700 let fx = self.state.input_cancel(now);
1701 self.effects(fx, &Ctx::default(), el);
1702 return;
1703 } else if key == named(Named::Enter) {
1704 if event.repeat {
1705 return;
1706 }
1707 let (fx, run) = self.state.input_submit(now);
1708 self.effects(fx, &Ctx::default(), el);
1709 if let Some(text) = run {
1710 match text.parse::<Action>() {
1711 Ok(a) => {
1712 let _ = self.run(vec![a], &Ctx::default(), el);
1713 }
1714 Err(e) => self
1715 .state
1716 .notify(format!("error: {e}"), MsgKind::Error, now),
1717 }
1718 }
1719 self.request_redraw();
1720 return;
1721 } else if key == named(Named::Backspace) && empty {
1722 let fx = self.state.input_cancel(now);
1723 self.effects(fx, &Ctx::default(), el);
1724 return;
1725 } else if key == named(Named::Backspace) || key == ctrl('h') {
1726 Edit::Backspace
1727 } else if key == ctrl('w') {
1728 Edit::DeleteWord
1729 } else if key == ctrl('u') {
1730 Edit::Clear
1731 } else if key == named(Named::Tab) && palette {
1732 Edit::Complete
1733 } else if key == named(Named::Up) || key == ctrl('p') || key == shift_tab {
1734 Edit::Up
1735 } else if key == named(Named::Down) || key == ctrl('n') || key == named(Named::Tab) {
1736 Edit::Down
1737 } else if key == named(Named::Left) {
1738 Edit::Left
1739 } else if key == named(Named::Right) {
1740 Edit::Right
1741 } else if key == named(Named::Space) && key.mods.is_empty() {
1742 Edit::Insert(" ".into())
1743 } else {
1744 let m = self.modifiers;
1745 match &event.text {
1746 Some(t) if !m.control_key() && !m.alt_key() && !m.super_key() => {
1747 Edit::Insert(t.to_string())
1748 }
1749 _ => return,
1750 }
1751 };
1752 let fx = self.state.input_edit(edit, now);
1753 self.effects(fx, &Ctx::default(), el);
1754 }
1755
1756 /// Scrolling and closing keys while help is open. Returns whether the key was used.
1757 fn help_key(&mut self, key: Key) -> bool {
1758 let page = self.state.help_page();
1759 let Some(help) = &mut self.state.help else {
1760 return false;
1761 };
1762 let ctrl = |c: char| {
1763 Key::new(
1764 input::keys::Mods {
1765 ctrl: true,
1766 ..Default::default()
1767 },
1768 Code::Char(c),
1769 )
1770 };
1771 let named = |n: Named| Key::new(Default::default(), Code::Named(n));
1772 let half = (page / 2).max(1) as isize;
1773 let delta = match key {
1774 k if k == Key::plain('j') || k == named(Named::Down) => 1,
1775 k if k == Key::plain('k') || k == named(Named::Up) => -1,
1776 k if k == ctrl('d') || k == ctrl('f') || k == named(Named::PageDown) => half,
1777 k if k == ctrl('u') || k == ctrl('b') || k == named(Named::PageUp) => -half,
1778 k if k == Key::plain('g') || k == named(Named::Home) => isize::MIN / 2,
1779 k if k == Key::plain('G') || k == named(Named::End) => isize::MAX / 2,
1780 k if k == Key::plain('q') || k == Key::plain('?') || k == named(Named::Esc) => {
1781 self.state.help = None;
1782 self.request_redraw();
1783 return true;
1784 }
1785 _ => return false,
1786 };
1787 help.scroll_by(delta, page);
1788 self.request_redraw();
1789 true
1790 }
1791
1792 /// A mouse button or wheel notch: bound actions run at the cursor, bypassing sequences.
1793 fn on_mouse_key(&mut self, key: Key, event_loop: &ActiveEventLoop) {
1794 self.keymap.cancel();
1795 self.update_pending(Instant::now());
1796 if let Some(actions) = self.keymap.lookup(self.state.mode, &[key]) {
1797 let actions = actions.to_vec();
1798 let ctx = Ctx {
1799 anchor: Some(self.cursor),
1800 ..Ctx::default()
1801 };
1802 let _ = self.run(actions, &ctx, event_loop);
1803 }
1804 }
1805
1806 /// `pixels` is set for touchpads, whose deltas are pixels divided by [`PIXELS_PER_STEP`].
1807 fn on_wheel(&mut self, delta: [f32; 2], pixels: bool, event_loop: &ActiveEventLoop) {
1808 let now = Instant::now();
1809 let [dx, dy] = delta;
1810 if self.state.help.is_some() {
1811 let page = self.state.help_page();
1812 self.scroll[1] += dy;
1813 let rows = self.scroll[1].trunc();
1814 self.scroll[1] -= rows;
1815 if let Some(h) = &mut self.state.help {
1816 h.scroll_by(-(rows as isize) * 3, page);
1817 }
1818 self.request_redraw();
1819 return;
1820 }
1821 if dy != 0.0 {
1822 let dir = if dy > 0.0 { Scroll::Up } else { Scroll::Down };
1823 let key = input::scroll(dir, self.modifiers);
1824 // Wheel zoom and grid scrolling stay continuous for touchpads rather than stepping
1825 // per notch.
1826 let in_grid = self.state.in_grid();
1827 match self.keymap.lookup(self.state.mode, &[key]) {
1828 Some([Action::Scroll(d @ (Dir::Up | Dir::Down), n)]) if in_grid => {
1829 let sign = if *d == Dir::Up { -1.0 } else { 1.0 };
1830 let amount = dy.abs() * *n as f32 * sign;
1831 let moved = if pixels {
1832 self.state.grid_drag(-amount * PIXELS_PER_STEP, now)
1833 } else {
1834 self.state.grid_scroll(amount, now)
1835 };
1836 self.after_grid_scroll(moved);
1837 return;
1838 }
1839 Some([Action::Zoom(Zoom::In(n))]) => {
1840 let steps = dy.abs() * *n as f32;
1841 self.state.zoom_at(steps, self.cursor, now);
1842 self.request_redraw();
1843 return;
1844 }
1845 Some([Action::Zoom(Zoom::Out(n))]) => {
1846 let steps = -dy.abs() * *n as f32;
1847 self.state.zoom_at(steps, self.cursor, now);
1848 self.request_redraw();
1849 return;
1850 }
1851 _ => {}
1852 }
1853 }
1854 for (axis, d, pos, neg) in [
1855 (0, dx, Scroll::Left, Scroll::Right),
1856 (1, dy, Scroll::Up, Scroll::Down),
1857 ] {
1858 if d == 0.0 {
1859 continue;
1860 }
1861 // Reversing direction drops the leftover fraction.
1862 if self.scroll[axis] * d < 0.0 {
1863 self.scroll[axis] = 0.0;
1864 }
1865 self.scroll[axis] += d;
1866 while self.scroll[axis].abs() >= 1.0 {
1867 let up = self.scroll[axis] > 0.0;
1868 self.scroll[axis] -= self.scroll[axis].signum();
1869 let key = input::scroll(if up { pos } else { neg }, self.modifiers);
1870 self.on_mouse_key(key, event_loop);
1871 }
1872 }
1873 }
1874
1875 /// The grid scrolled; `moved` says whether it pulled the cursor along.
1876 fn after_grid_scroll(&mut self, moved: bool) {
1877 if moved {
1878 self.sync(true);
1879 } else {
1880 self.sync_thumbs();
1881 }
1882 self.request_redraw();
1883 }
1884
1885 /// A click (press and release in place) in the grid selects; a second one opens.
1886 fn grid_click(&mut self, event_loop: &ActiveEventLoop) {
1887 let now = Instant::now();
1888 let Some(i) = self.state.grid_hit(self.cursor, now) else {
1889 return;
1890 };
1891 let double = self
1892 .last_click
1893 .is_some_and(|(t, j)| j == i && now < t + DOUBLE_CLICK);
1894 self.last_click = (!double).then_some((now, i));
1895 let mut actions = vec![Action::Goto(i + 1)];
1896 if double {
1897 actions.push(Action::Mode(ModeArg::Image));
1898 }
1899 let _ = self.run(actions, &Ctx::default(), event_loop);
1900 }
1901
1902 fn redraw(&mut self) {
1903 let now = Instant::now();
1904 let ready = self.state.shown.is_some()
1905 || self.state.queue.is_empty()
1906 || (self.state.in_grid() && self.grid_settled())
1907 || now >= self.started + SHOW_TIMEOUT;
1908 if !self.shown_window && !ready {
1909 return; // about_to_wait wakes us at the timeout
1910 }
1911 self.state.advance(now);
1912 self.state.tick(now);
1913 let settled = if self.state.in_grid() {
1914 self.thumbs.arrivals.is_empty() && self.grid_settled()
1915 } else {
1916 self.state.loading_since.is_none()
1917 };
1918 // An animation moved on (or a page turned): put that frame in the texture.
1919 let frame = self.state.frame();
1920 if let Some(path) = self
1921 .state
1922 .shown
1923 .as_ref()
1924 .filter(|_| self.state.mode == Mode::Image)
1925 .map(|s| s.path.clone())
1926 && let Some(g) = &self.gfx
1927 && g.renderer.frame(&path).is_some_and(|f| f != frame)
1928 && let Some(img) = self.cache.get(&path)
1929 && let Some(g) = &mut self.gfx
1930 {
1931 g.renderer.show_frame(&path, &img, frame);
1932 }
1933 let Some(g) = &mut self.gfx else { return };
1934 if !self.shown_window {
1935 g.window.set_visible(true);
1936 }
1937 let theme = self.state.theme_at(now);
1938 let scene = ui::scene(&self.state, &theme, now, &mut |t| g.renderer.measure(t));
1939 let result = g.renderer.render(&scene);
1940 if let Some(path) = std::env::var_os("REMAGE_SNAPSHOT")
1941 && !self.state.is_animating(now)
1942 && settled
1943 {
1944 match g.renderer.snapshot(&scene, Path::new(&path)) {
1945 Ok(()) => log::info!("snapshot written to {}", Path::new(&path).display()),
1946 Err(e) => log::error!("snapshot: {e:#}"),
1947 }
1948 }
1949 drop(scene);
1950 if let Err(e) = result {
1951 log::error!("render: {e:#}");
1952 self.state
1953 .notify(format!("error: {e}"), MsgKind::Error, now);
1954 }
1955 if !self.shown_window {
1956 self.shown_window = true;
1957 log::debug!("first frame after {:.1?}", self.started.elapsed());
1958 let proxy = self.proxy.clone();
1959 std::thread::spawn(move || {
1960 let _ = proxy.send_event(UserEvent::Fonts(text::all_fonts()));
1961 });
1962 }
1963 if self.state.is_animating(Instant::now()) {
1964 g.window.request_redraw();
1965 }
1966 }
1967
1968 /// Upload thumbnails, then preloaded images a slice at a time. Returns whether work remains.
1969 fn pump_uploads(&mut self) -> bool {
1970 let Some(g) = &mut self.gfx else { return false };
1971 let now = Instant::now();
1972 let mut bytes = 0;
1973 while bytes < UPLOAD_BUDGET
1974 && let Some((path, t)) = self.thumbs.arrivals.pop_front()
1975 {
1976 bytes += t.pixels.len();
1977 let wanted = &self.thumbs.wanted;
1978 for gone in g.renderer.insert_thumb(&path, &t, |p| wanted.contains(p)) {
1979 self.state.thumbs.remove(&gone);
1980 }
1981 if g.renderer.has_thumb(&path) {
1982 self.state.thumb_ready(path, t.size, now);
1983 }
1984 g.window.request_redraw();
1985 }
1986 if !self.thumbs.arrivals.is_empty() {
1987 return true;
1988 }
1989 if let Some((path, img)) = self.uploads.front()
1990 && g.renderer.upload(path, img, UPLOAD_BUDGET)
1991 {
1992 self.uploads.pop_front();
1993 }
1994 !self.uploads.is_empty()
1995 }
1996}
1997
1998/// A config error trimmed for the status line: `config.toml:2:1: unknown field `fitt`; did you
1999/// mean `fit`?` without the directory or the list of every valid field.
2000fn short_error(message: &str) -> String {
2001 let mut s = match message.split_once(": ") {
2002 Some((loc, rest)) if loc.contains('/') => {
2003 format!("{}: {rest}", loc.rsplit('/').next().unwrap_or(loc))
2004 }
2005 _ => message.to_string(),
2006 };
2007 if let Some(start) = s.find(", expected one of") {
2008 let end = s[start..]
2009 .find("; did you mean")
2010 .map_or(s.len(), |i| start + i);
2011 s.replace_range(start..end, "");
2012 }
2013 s
2014}
2015
2016fn keymap(layers: &[KeyTables]) -> Keymap {
2017 let refs: Vec<&KeyTables> = layers.iter().collect();
2018 Keymap::new(&refs)
2019}
2020
2021fn font_families(c: &Config) -> [String; 3] {
2022 [
2023 c.ui.font_ui.clone(),
2024 c.ui.font_mono.clone(),
2025 c.ui.font_display.clone(),
2026 ]
2027}
2028
2029fn mtime(path: &Path) -> Option<SystemTime> {
2030 std::fs::metadata(path).and_then(|m| m.modified()).ok()
2031}
2032
2033fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
2034 m.lock().unwrap_or_else(|e| e.into_inner())
2035}
2036
2037impl ApplicationHandler<UserEvent> for App {
2038 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
2039 if self.gfx.is_some() {
2040 return;
2041 }
2042 let (w, h) = self.opts.geometry.unwrap_or((1280, 800));
2043 let mut attrs = Window::default_attributes()
2044 .with_title("remage")
2045 .with_inner_size(LogicalSize::new(w, h))
2046 .with_decorations(self.config.window.decorations)
2047 .with_visible(false);
2048 if self.opts.fullscreen {
2049 attrs = attrs.with_fullscreen(Some(Fullscreen::Borderless(None)));
2050 }
2051 let class = self.opts.class.clone();
2052 attrs = winit::platform::wayland::WindowAttributesExtWayland::with_name(
2053 attrs,
2054 class.clone(),
2055 class.clone(),
2056 );
2057 attrs =
2058 winit::platform::x11::WindowAttributesExtX11::with_name(attrs, class.clone(), class);
2059 let window = match event_loop.create_window(attrs) {
2060 Ok(w) => Arc::new(w),
2061 Err(e) => {
2062 self.error = Some(anyhow::anyhow!("create window: {e}"));
2063 event_loop.exit();
2064 return;
2065 }
2066 };
2067 let size = window.inner_size();
2068 self.state.set_window(
2069 size.width as f32,
2070 size.height as f32,
2071 window.scale_factor() as f32,
2072 );
2073 // The grid's size is known now: thumbnails load while the GPU starts up.
2074 self.sync_thumbs();
2075 let renderer = match Renderer::new(window.clone(), event_loop.owned_display_handle()) {
2076 Ok(mut r) => {
2077 r.set_font_families(font_families(&self.config));
2078 r.reset_thumbs(self.thumbs.cell);
2079 r
2080 }
2081 Err(e) => {
2082 self.error = Some(e.context("initialise GPU"));
2083 event_loop.exit();
2084 return;
2085 }
2086 };
2087 log::debug!("GPU ready after {:.1?}", self.started.elapsed());
2088 self.gfx = Some(Gfx { renderer, window });
2089 self.sync(false);
2090 }
2091
2092 fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
2093 // The window goes at once; whatever else is still winding down does so unseen.
2094 self.gfx = None;
2095 }
2096
2097 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
2098 match event {
2099 UserEvent::Decoded {
2100 path,
2101 mtime,
2102 result,
2103 } => self.on_decoded(path, mtime, result),
2104 UserEvent::Found(paths) => {
2105 log::debug!("sniffing found {} more images", paths.len());
2106 let was_empty = self.state.queue.is_empty();
2107 self.state.queue.insert_sorted(paths);
2108 self.sync(was_empty);
2109 }
2110 UserEvent::Fonts(db) => {
2111 if let Some(g) = &mut self.gfx {
2112 g.renderer.set_fonts(db);
2113 g.window.request_redraw();
2114 }
2115 }
2116 UserEvent::Config(result) => match *result {
2117 Ok(loaded) => {
2118 log::info!("config reloaded");
2119 self.apply_config(loaded);
2120 }
2121 Err(e) => {
2122 log::error!("{e}");
2123 self.config_failed(&e.message);
2124 }
2125 },
2126 UserEvent::Sorted(files) => {
2127 self.state.queue.reorder(files);
2128 self.sync(false);
2129 }
2130 UserEvent::Thumb {
2131 path,
2132 px,
2133 mtime,
2134 result,
2135 } => self.on_thumb(path, px, mtime, result),
2136 UserEvent::Refined { path, result } => self.on_refined(path, result),
2137 UserEvent::Command { id, event } => self.on_command(id, event, event_loop),
2138 UserEvent::Refreshed(found) => self.on_refreshed(found),
2139 UserEvent::Ipc(inc) => self.on_ipc(inc, event_loop),
2140 UserEvent::Info(info) => {
2141 if self.info_loading.as_ref() == Some(&info.path) {
2142 self.info_loading = None;
2143 }
2144 if self.state.queue.current() == Some(info.path.as_path()) {
2145 self.state.info = Some(info);
2146 self.request_redraw();
2147 }
2148 }
2149 UserEvent::InfoLine { path, line } => {
2150 self.state.info_line = Some((path, line));
2151 self.request_redraw();
2152 }
2153 UserEvent::Dir(changes) => self.on_dir(changes),
2154 UserEvent::Opened { arg, scan } => self.on_opened(arg, scan),
2155 }
2156 }
2157
2158 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
2159 match event {
2160 WindowEvent::CloseRequested => event_loop.exit(),
2161 WindowEvent::Resized(size) => {
2162 let Some(g) = &mut self.gfx else { return };
2163 g.renderer.resize(size.width, size.height);
2164 let sf = g.window.scale_factor() as f32;
2165 self.state
2166 .set_window(size.width as f32, size.height as f32, sf);
2167 self.sync_thumbs();
2168 self.request_redraw();
2169 }
2170 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2171 let [w, h] = self.state.window();
2172 self.state.set_window(w, h, scale_factor as f32);
2173 self.sync_thumbs();
2174 self.request_redraw();
2175 }
2176 WindowEvent::RedrawRequested => self.redraw(),
2177 WindowEvent::ModifiersChanged(m) => self.modifiers = m.state(),
2178 WindowEvent::Focused(false) => {
2179 self.held.clear();
2180 self.state.stop_all_pans();
2181 self.dragging = false;
2182 }
2183 WindowEvent::KeyboardInput {
2184 event,
2185 is_synthetic: false,
2186 ..
2187 } => {
2188 self.on_key(event, event_loop);
2189 }
2190 WindowEvent::CursorMoved { position, .. } => {
2191 let p = [position.x as f32, position.y as f32];
2192 let delta = [p[0] - self.cursor[0], p[1] - self.cursor[1]];
2193 self.cursor = p;
2194 if self.dragging && self.state.in_grid() {
2195 let moved = self.state.grid_drag(delta[1], Instant::now());
2196 self.after_grid_scroll(moved);
2197 } else if self.dragging && self.state.shown.is_some() {
2198 self.state.view.pan_pixels(delta);
2199 self.request_redraw();
2200 }
2201 }
2202 WindowEvent::MouseInput { state, button, .. } => {
2203 let pressed = state == ElementState::Pressed;
2204 if button == MouseButton::Left {
2205 // Drag pans; a press and release in place is a `<LeftMouse>` click.
2206 self.dragging = pressed;
2207 if pressed {
2208 self.press = Some(self.cursor);
2209 return;
2210 }
2211 let Some(p) = self.press.take() else { return };
2212 let moved = (self.cursor[0] - p[0]).hypot(self.cursor[1] - p[1]);
2213 if moved > CLICK_SLOP * self.state.scale_factor() {
2214 return;
2215 }
2216 if self.state.in_grid() {
2217 self.grid_click(event_loop);
2218 }
2219 } else if !pressed {
2220 return;
2221 }
2222 if let Some(key) = input::button(button, self.modifiers) {
2223 self.on_mouse_key(key, event_loop);
2224 }
2225 }
2226 WindowEvent::MouseWheel { delta, .. } => {
2227 let (d, pixels) = match delta {
2228 MouseScrollDelta::LineDelta(x, y) => ([x, y], false),
2229 MouseScrollDelta::PixelDelta(p) => (
2230 [p.x as f32 / PIXELS_PER_STEP, p.y as f32 / PIXELS_PER_STEP],
2231 true,
2232 ),
2233 };
2234 self.on_wheel(d, pixels, event_loop);
2235 }
2236 _ => {}
2237 }
2238 }
2239
2240 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
2241 if matches!(cause, StartCause::ResumeTimeReached { .. }) {
2242 self.request_redraw();
2243 }
2244 }
2245
2246 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
2247 let now = Instant::now();
2248 let busy = self.pump_uploads();
2249 if let Some(t) = self.ambiguous_since
2250 && now >= t + SEQUENCE_TIMEOUT
2251 {
2252 let runs = self.keymap.timeout(self.state.mode);
2253 self.update_pending(now);
2254 for run in runs {
2255 let _ = self.run(run.counted(), &Ctx::default(), event_loop);
2256 }
2257 }
2258 if let Some(t) = self.grid_rest
2259 && now >= t + GRID_REST
2260 {
2261 self.grid_rest = None;
2262 if self.state.in_grid()
2263 && let Some(cur) = self.state.queue.current().map(Path::to_path_buf)
2264 && !self.cache_has(&cur)
2265 {
2266 self.decode(&cur, Priority::Preload);
2267 }
2268 }
2269 if let Some(t) = self.which_key_since
2270 && now >= t + self.state.settings.which_key_delay
2271 {
2272 self.which_key_since = None;
2273 if self.keymap.is_pending() && self.state.input.is_none() {
2274 self.state.which_key = Some(self.which_key_rows());
2275 self.request_redraw();
2276 }
2277 }
2278 if self.state.slideshow_due(now) {
2279 let fx = self.state.apply(Action::Next(1), now);
2280 self.state.slideshow_advanced(fx.navigated);
2281 if !fx.navigated {
2282 self.state.notify("slideshow done", MsgKind::Info, now);
2283 }
2284 self.effects(fx, &Ctx::default(), event_loop);
2285 }
2286 self.publish();
2287 let (redraw, mut next) = self.state.tick(now);
2288 if let Some(t) = self.which_key_since {
2289 let at = t + self.state.settings.which_key_delay;
2290 next = Some(next.map_or(at, |n| n.min(at)));
2291 }
2292 if let Some(t) = self.grid_rest {
2293 let at = t + GRID_REST;
2294 next = Some(next.map_or(at, |n| n.min(at)));
2295 }
2296 if let Some(t) = self.ambiguous_since {
2297 let at = t + SEQUENCE_TIMEOUT;
2298 next = Some(next.map_or(at, |n| n.min(at)));
2299 }
2300 if let Some(at) = self.check_refine(now) {
2301 next = Some(next.map_or(at, |n| n.min(at)));
2302 }
2303 if redraw {
2304 self.request_redraw();
2305 }
2306 if !self.shown_window {
2307 let at = self.started + SHOW_TIMEOUT;
2308 next = Some(next.map_or(at, |n| n.min(at)));
2309 }
2310 event_loop.set_control_flow(match (busy, next) {
2311 (true, _) => ControlFlow::Poll,
2312 (false, Some(t)) => ControlFlow::WaitUntil(t),
2313 (false, None) => ControlFlow::Wait,
2314 });
2315 }
2316}
2317
2318#[cfg(test)]
2319mod tests {
2320 use super::*;
2321
2322 #[test]
2323 fn config_errors_are_shortened_for_the_bar() {
2324 assert_eq!(
2325 short_error(
2326 "/home/u/.config/remage/config.toml:2:1: unknown field `fitt`, expected one of \
2327 `fit`, `zoom_step`; did you mean `fit`?"
2328 ),
2329 "config.toml:2:1: unknown field `fitt`; did you mean `fit`?"
2330 );
2331 assert_eq!(
2332 short_error("a.toml:1:1: unknown field `x`, expected `y`"),
2333 "a.toml:1:1: unknown field `x`, expected `y`"
2334 );
2335 }
2336}