Krolyxon
Commit 4654f72

Branches

Tags

Refs →

remage4654f72

Add packaging: man page, completions, README, PKGBUILD, CI

- remage completions <SHELL> and a hidden remage man; the man page's keys
  and actions are generated from the defaults and the action catalog
- contrib: desktop file, example config with the old key-handler's
  delete/wallpaper/copy as commands, remage-git PKGBUILD
- README with logo, badges and screenshots; AGPL-3.0 licence
- CI on Arch: fmt, clippy, tests, and a pure-Rust feature build
- Require libheif 1.17 rather than 1.23
- Never show [LOADING] in the grid
Author
krolyxon
Date
2026-11-05 00:16 +0530
Parent
a44bdf7
Commit
4654f729bb6bba5fe5de089a50f664a15ac16150

+1655 −13

19 files changed

  1. A .github/workflows/ci.yml +39 −0
  2. M Cargo.lock +17 −0
  3. M Cargo.toml +7 −1
  4. A LICENSE +661 −0
  5. A README.md +218 −0
  6. A contrib/PKGBUILD +72 −0
  7. A contrib/config.example.toml +86 −0
  8. A contrib/remage.desktop +12 −0
  9. A docs/logo.svg +254 −0
  10. A docs/screenshots/grid.jpg
  11. A docs/screenshots/image.jpg
  12. A docs/screenshots/info.jpg
  13. A docs/screenshots/palette.jpg
  14. M src/cli.rs +24 −0
  15. M src/decode/color.rs +1 −0
  16. M src/decode/mod.rs +4 −1
  17. M src/main.rs +21 −9
  18. A src/man.rs +226 −0
  19. M src/state/mod.rs +13 −2
A .github/workflows/ci.yml View
@@ -0,0 +1,39 @@
1+name: CI
2+
3+on:
4+ push:
5+ branches: [master]
6+ pull_request:
7+
8+# A newer push to the same branch or PR supersedes the running check.
9+concurrency:
10+ group: ci-${{ github.ref }}
11+ cancel-in-progress: true
12+
13+env:
14+ CARGO_TERM_COLOR: always
15+
16+jobs:
17+ check:
18+ name: fmt, clippy, test
19+ runs-on: ubuntu-latest
20+ # Arch has current dav1d and libheif, the two system libraries the default features need.
21+ container: archlinux:latest
22+ steps:
23+ - name: Install system dependencies
24+ run: |
25+ pacman -Syu --noconfirm --needed base-devel git pkgconf rustup \
26+ dav1d libheif libxkbcommon wayland
27+ rustup default stable
28+ rustup component add rustfmt clippy
29+ - uses: actions/checkout@v7
30+ - uses: Swatinem/rust-cache@v2
31+ - run: cargo fmt --check
32+ - run: cargo clippy --all-targets --locked -- -D warnings
33+ - run: cargo test --locked
34+ - name: Build without C dependencies
35+ run: cargo clippy --all-targets --locked --no-default-features --features jxl,svg,raw -- -D warnings
36+ - name: Man page and completions render
37+ run: |
38+ cargo run --locked -q -- man | grep -q '^.SH KEYS'
39+ cargo run --locked -q -- completions zsh | grep -q '^#compdef remage'
M Cargo.lock View
@@ -505,6 +505,16 @@ version = "1.1.1"
505505 source = "registry+https://github.com/rust-lang/crates.io-index"
506506 checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
507507
508+[[package]]
509+name = "clap_mangen"
510+version = "0.3.3"
511+source = "registry+https://github.com/rust-lang/crates.io-index"
512+checksum = "211d617eaa4b735c96c9e0228fcbdb5120ef623f2b8cb67ffb84c3e02dbc28a4"
513+dependencies = [
514+ "clap",
515+ "roff",
516+]
517+
508518 [[package]]
509519 name = "codespan-reporting"
510520 version = "0.13.1"
@@ -3053,6 +3063,7 @@ dependencies = [
30533063 "bytemuck",
30543064 "clap",
30553065 "clap_complete",
3066+ "clap_mangen",
30563067 "crossbeam-channel",
30573068 "dirs",
30583069 "env_logger",
@@ -3121,6 +3132,12 @@ version = "1.0.3"
31213132 source = "registry+https://github.com/rust-lang/crates.io-index"
31223133 checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
31233134
3135+[[package]]
3136+name = "roff"
3137+version = "1.1.1"
3138+source = "registry+https://github.com/rust-lang/crates.io-index"
3139+checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189"
3140+
31243141 [[package]]
31253142 name = "roxmltree"
31263143 version = "0.20.0"
M Cargo.toml View
@@ -4,12 +4,18 @@ version = "0.1.0"
44 edition = "2024"
55 description = "Keyboard-centric image viewer with a Nothing-inspired look"
66 rust-version = "1.96"
7+license = "AGPL-3.0-only"
8+repository = "https://github.com/krolyxon/remage"
9+readme = "README.md"
10+keywords = ["image-viewer", "wayland", "wgpu", "vim", "nsxiv"]
11+categories = ["multimedia::images", "graphics"]
712
813 [dependencies]
914 anyhow = "1.0.104"
1015 bytemuck = { version = "1.25.2", features = ["derive"] }
1116 clap = { version = "4.6.7", features = ["derive"] }
1217 clap_complete = "4.6.11"
18+clap_mangen = "0.3.3"
1319 crossbeam-channel = "0.5.17"
1420 dirs = "7.0.0"
1521 env_logger = "0.11.11"
@@ -19,7 +25,7 @@ image = { version = "0.25.10", default-features = false, features = ["png", "jpe
1925 jpeg-decoder = "0.3.2"
2026 jxl-oxide = { version = "0.12.6", optional = true, features = ["moxcms"] }
2127 kamadak-exif = "0.6.1"
22−libheif-rs = { version = "3.0.0", optional = true }
28+libheif-rs = { version = "3.0.0", optional = true, default-features = false, features = ["v1_17"] } # libheif ≥ 1.17
2329 log = "0.4.35"
2430 md-5 = "0.11.0"
2531 moxcms = "0.9.1"
A LICENSE View
@@ -0,0 +1,661 @@
1+ GNU AFFERO GENERAL PUBLIC LICENSE
2+ Version 3, 19 November 2007
3+
4+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5+ Everyone is permitted to copy and distribute verbatim copies
6+ of this license document, but changing it is not allowed.
7+
8+ Preamble
9+
10+ The GNU Affero General Public License is a free, copyleft license for
11+software and other kinds of works, specifically designed to ensure
12+cooperation with the community in the case of network server software.
13+
14+ The licenses for most software and other practical works are designed
15+to take away your freedom to share and change the works. By contrast,
16+our General Public Licenses are intended to guarantee your freedom to
17+share and change all versions of a program--to make sure it remains free
18+software for all its users.
19+
20+ When we speak of free software, we are referring to freedom, not
21+price. Our General Public Licenses are designed to make sure that you
22+have the freedom to distribute copies of free software (and charge for
23+them if you wish), that you receive source code or can get it if you
24+want it, that you can change the software or use pieces of it in new
25+free programs, and that you know you can do these things.
26+
27+ Developers that use our General Public Licenses protect your rights
28+with two steps: (1) assert copyright on the software, and (2) offer
29+you this License which gives you legal permission to copy, distribute
30+and/or modify the software.
31+
32+ A secondary benefit of defending all users' freedom is that
33+improvements made in alternate versions of the program, if they
34+receive widespread use, become available for other developers to
35+incorporate. Many developers of free software are heartened and
36+encouraged by the resulting cooperation. However, in the case of
37+software used on network servers, this result may fail to come about.
38+The GNU General Public License permits making a modified version and
39+letting the public access it on a server without ever releasing its
40+source code to the public.
41+
42+ The GNU Affero General Public License is designed specifically to
43+ensure that, in such cases, the modified source code becomes available
44+to the community. It requires the operator of a network server to
45+provide the source code of the modified version running there to the
46+users of that server. Therefore, public use of a modified version, on
47+a publicly accessible server, gives the public access to the source
48+code of the modified version.
49+
50+ An older license, called the Affero General Public License and
51+published by Affero, was designed to accomplish similar goals. This is
52+a different license, not a version of the Affero GPL, but Affero has
53+released a new version of the Affero GPL which permits relicensing under
54+this license.
55+
56+ The precise terms and conditions for copying, distribution and
57+modification follow.
58+
59+ TERMS AND CONDITIONS
60+
61+ 0. Definitions.
62+
63+ "This License" refers to version 3 of the GNU Affero General Public License.
64+
65+ "Copyright" also means copyright-like laws that apply to other kinds of
66+works, such as semiconductor masks.
67+
68+ "The Program" refers to any copyrightable work licensed under this
69+License. Each licensee is addressed as "you". "Licensees" and
70+"recipients" may be individuals or organizations.
71+
72+ To "modify" a work means to copy from or adapt all or part of the work
73+in a fashion requiring copyright permission, other than the making of an
74+exact copy. The resulting work is called a "modified version" of the
75+earlier work or a work "based on" the earlier work.
76+
77+ A "covered work" means either the unmodified Program or a work based
78+on the Program.
79+
80+ To "propagate" a work means to do anything with it that, without
81+permission, would make you directly or secondarily liable for
82+infringement under applicable copyright law, except executing it on a
83+computer or modifying a private copy. Propagation includes copying,
84+distribution (with or without modification), making available to the
85+public, and in some countries other activities as well.
86+
87+ To "convey" a work means any kind of propagation that enables other
88+parties to make or receive copies. Mere interaction with a user through
89+a computer network, with no transfer of a copy, is not conveying.
90+
91+ An interactive user interface displays "Appropriate Legal Notices"
92+to the extent that it includes a convenient and prominently visible
93+feature that (1) displays an appropriate copyright notice, and (2)
94+tells the user that there is no warranty for the work (except to the
95+extent that warranties are provided), that licensees may convey the
96+work under this License, and how to view a copy of this License. If
97+the interface presents a list of user commands or options, such as a
98+menu, a prominent item in the list meets this criterion.
99+
100+ 1. Source Code.
101+
102+ The "source code" for a work means the preferred form of the work
103+for making modifications to it. "Object code" means any non-source
104+form of a work.
105+
106+ A "Standard Interface" means an interface that either is an official
107+standard defined by a recognized standards body, or, in the case of
108+interfaces specified for a particular programming language, one that
109+is widely used among developers working in that language.
110+
111+ The "System Libraries" of an executable work include anything, other
112+than the work as a whole, that (a) is included in the normal form of
113+packaging a Major Component, but which is not part of that Major
114+Component, and (b) serves only to enable use of the work with that
115+Major Component, or to implement a Standard Interface for which an
116+implementation is available to the public in source code form. A
117+"Major Component", in this context, means a major essential component
118+(kernel, window system, and so on) of the specific operating system
119+(if any) on which the executable work runs, or a compiler used to
120+produce the work, or an object code interpreter used to run it.
121+
122+ The "Corresponding Source" for a work in object code form means all
123+the source code needed to generate, install, and (for an executable
124+work) run the object code and to modify the work, including scripts to
125+control those activities. However, it does not include the work's
126+System Libraries, or general-purpose tools or generally available free
127+programs which are used unmodified in performing those activities but
128+which are not part of the work. For example, Corresponding Source
129+includes interface definition files associated with source files for
130+the work, and the source code for shared libraries and dynamically
131+linked subprograms that the work is specifically designed to require,
132+such as by intimate data communication or control flow between those
133+subprograms and other parts of the work.
134+
135+ The Corresponding Source need not include anything that users
136+can regenerate automatically from other parts of the Corresponding
137+Source.
138+
139+ The Corresponding Source for a work in source code form is that
140+same work.
141+
142+ 2. Basic Permissions.
143+
144+ All rights granted under this License are granted for the term of
145+copyright on the Program, and are irrevocable provided the stated
146+conditions are met. This License explicitly affirms your unlimited
147+permission to run the unmodified Program. The output from running a
148+covered work is covered by this License only if the output, given its
149+content, constitutes a covered work. This License acknowledges your
150+rights of fair use or other equivalent, as provided by copyright law.
151+
152+ You may make, run and propagate covered works that you do not
153+convey, without conditions so long as your license otherwise remains
154+in force. You may convey covered works to others for the sole purpose
155+of having them make modifications exclusively for you, or provide you
156+with facilities for running those works, provided that you comply with
157+the terms of this License in conveying all material for which you do
158+not control copyright. Those thus making or running the covered works
159+for you must do so exclusively on your behalf, under your direction
160+and control, on terms that prohibit them from making any copies of
161+your copyrighted material outside their relationship with you.
162+
163+ Conveying under any other circumstances is permitted solely under
164+the conditions stated below. Sublicensing is not allowed; section 10
165+makes it unnecessary.
166+
167+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168+
169+ No covered work shall be deemed part of an effective technological
170+measure under any applicable law fulfilling obligations under article
171+11 of the WIPO copyright treaty adopted on 20 December 1996, or
172+similar laws prohibiting or restricting circumvention of such
173+measures.
174+
175+ When you convey a covered work, you waive any legal power to forbid
176+circumvention of technological measures to the extent such circumvention
177+is effected by exercising rights under this License with respect to
178+the covered work, and you disclaim any intention to limit operation or
179+modification of the work as a means of enforcing, against the work's
180+users, your or third parties' legal rights to forbid circumvention of
181+technological measures.
182+
183+ 4. Conveying Verbatim Copies.
184+
185+ You may convey verbatim copies of the Program's source code as you
186+receive it, in any medium, provided that you conspicuously and
187+appropriately publish on each copy an appropriate copyright notice;
188+keep intact all notices stating that this License and any
189+non-permissive terms added in accord with section 7 apply to the code;
190+keep intact all notices of the absence of any warranty; and give all
191+recipients a copy of this License along with the Program.
192+
193+ You may charge any price or no price for each copy that you convey,
194+and you may offer support or warranty protection for a fee.
195+
196+ 5. Conveying Modified Source Versions.
197+
198+ You may convey a work based on the Program, or the modifications to
199+produce it from the Program, in the form of source code under the
200+terms of section 4, provided that you also meet all of these conditions:
201+
202+ a) The work must carry prominent notices stating that you modified
203+ it, and giving a relevant date.
204+
205+ b) The work must carry prominent notices stating that it is
206+ released under this License and any conditions added under section
207+ 7. This requirement modifies the requirement in section 4 to
208+ "keep intact all notices".
209+
210+ c) You must license the entire work, as a whole, under this
211+ License to anyone who comes into possession of a copy. This
212+ License will therefore apply, along with any applicable section 7
213+ additional terms, to the whole of the work, and all its parts,
214+ regardless of how they are packaged. This License gives no
215+ permission to license the work in any other way, but it does not
216+ invalidate such permission if you have separately received it.
217+
218+ d) If the work has interactive user interfaces, each must display
219+ Appropriate Legal Notices; however, if the Program has interactive
220+ interfaces that do not display Appropriate Legal Notices, your
221+ work need not make them do so.
222+
223+ A compilation of a covered work with other separate and independent
224+works, which are not by their nature extensions of the covered work,
225+and which are not combined with it such as to form a larger program,
226+in or on a volume of a storage or distribution medium, is called an
227+"aggregate" if the compilation and its resulting copyright are not
228+used to limit the access or legal rights of the compilation's users
229+beyond what the individual works permit. Inclusion of a covered work
230+in an aggregate does not cause this License to apply to the other
231+parts of the aggregate.
232+
233+ 6. Conveying Non-Source Forms.
234+
235+ You may convey a covered work in object code form under the terms
236+of sections 4 and 5, provided that you also convey the
237+machine-readable Corresponding Source under the terms of this License,
238+in one of these ways:
239+
240+ a) Convey the object code in, or embodied in, a physical product
241+ (including a physical distribution medium), accompanied by the
242+ Corresponding Source fixed on a durable physical medium
243+ customarily used for software interchange.
244+
245+ b) Convey the object code in, or embodied in, a physical product
246+ (including a physical distribution medium), accompanied by a
247+ written offer, valid for at least three years and valid for as
248+ long as you offer spare parts or customer support for that product
249+ model, to give anyone who possesses the object code either (1) a
250+ copy of the Corresponding Source for all the software in the
251+ product that is covered by this License, on a durable physical
252+ medium customarily used for software interchange, for a price no
253+ more than your reasonable cost of physically performing this
254+ conveying of source, or (2) access to copy the
255+ Corresponding Source from a network server at no charge.
256+
257+ c) Convey individual copies of the object code with a copy of the
258+ written offer to provide the Corresponding Source. This
259+ alternative is allowed only occasionally and noncommercially, and
260+ only if you received the object code with such an offer, in accord
261+ with subsection 6b.
262+
263+ d) Convey the object code by offering access from a designated
264+ place (gratis or for a charge), and offer equivalent access to the
265+ Corresponding Source in the same way through the same place at no
266+ further charge. You need not require recipients to copy the
267+ Corresponding Source along with the object code. If the place to
268+ copy the object code is a network server, the Corresponding Source
269+ may be on a different server (operated by you or a third party)
270+ that supports equivalent copying facilities, provided you maintain
271+ clear directions next to the object code saying where to find the
272+ Corresponding Source. Regardless of what server hosts the
273+ Corresponding Source, you remain obligated to ensure that it is
274+ available for as long as needed to satisfy these requirements.
275+
276+ e) Convey the object code using peer-to-peer transmission, provided
277+ you inform other peers where the object code and Corresponding
278+ Source of the work are being offered to the general public at no
279+ charge under subsection 6d.
280+
281+ A separable portion of the object code, whose source code is excluded
282+from the Corresponding Source as a System Library, need not be
283+included in conveying the object code work.
284+
285+ A "User Product" is either (1) a "consumer product", which means any
286+tangible personal property which is normally used for personal, family,
287+or household purposes, or (2) anything designed or sold for incorporation
288+into a dwelling. In determining whether a product is a consumer product,
289+doubtful cases shall be resolved in favor of coverage. For a particular
290+product received by a particular user, "normally used" refers to a
291+typical or common use of that class of product, regardless of the status
292+of the particular user or of the way in which the particular user
293+actually uses, or expects or is expected to use, the product. A product
294+is a consumer product regardless of whether the product has substantial
295+commercial, industrial or non-consumer uses, unless such uses represent
296+the only significant mode of use of the product.
297+
298+ "Installation Information" for a User Product means any methods,
299+procedures, authorization keys, or other information required to install
300+and execute modified versions of a covered work in that User Product from
301+a modified version of its Corresponding Source. The information must
302+suffice to ensure that the continued functioning of the modified object
303+code is in no case prevented or interfered with solely because
304+modification has been made.
305+
306+ If you convey an object code work under this section in, or with, or
307+specifically for use in, a User Product, and the conveying occurs as
308+part of a transaction in which the right of possession and use of the
309+User Product is transferred to the recipient in perpetuity or for a
310+fixed term (regardless of how the transaction is characterized), the
311+Corresponding Source conveyed under this section must be accompanied
312+by the Installation Information. But this requirement does not apply
313+if neither you nor any third party retains the ability to install
314+modified object code on the User Product (for example, the work has
315+been installed in ROM).
316+
317+ The requirement to provide Installation Information does not include a
318+requirement to continue to provide support service, warranty, or updates
319+for a work that has been modified or installed by the recipient, or for
320+the User Product in which it has been modified or installed. Access to a
321+network may be denied when the modification itself materially and
322+adversely affects the operation of the network or violates the rules and
323+protocols for communication across the network.
324+
325+ Corresponding Source conveyed, and Installation Information provided,
326+in accord with this section must be in a format that is publicly
327+documented (and with an implementation available to the public in
328+source code form), and must require no special password or key for
329+unpacking, reading or copying.
330+
331+ 7. Additional Terms.
332+
333+ "Additional permissions" are terms that supplement the terms of this
334+License by making exceptions from one or more of its conditions.
335+Additional permissions that are applicable to the entire Program shall
336+be treated as though they were included in this License, to the extent
337+that they are valid under applicable law. If additional permissions
338+apply only to part of the Program, that part may be used separately
339+under those permissions, but the entire Program remains governed by
340+this License without regard to the additional permissions.
341+
342+ When you convey a copy of a covered work, you may at your option
343+remove any additional permissions from that copy, or from any part of
344+it. (Additional permissions may be written to require their own
345+removal in certain cases when you modify the work.) You may place
346+additional permissions on material, added by you to a covered work,
347+for which you have or can give appropriate copyright permission.
348+
349+ Notwithstanding any other provision of this License, for material you
350+add to a covered work, you may (if authorized by the copyright holders of
351+that material) supplement the terms of this License with terms:
352+
353+ a) Disclaiming warranty or limiting liability differently from the
354+ terms of sections 15 and 16 of this License; or
355+
356+ b) Requiring preservation of specified reasonable legal notices or
357+ author attributions in that material or in the Appropriate Legal
358+ Notices displayed by works containing it; or
359+
360+ c) Prohibiting misrepresentation of the origin of that material, or
361+ requiring that modified versions of such material be marked in
362+ reasonable ways as different from the original version; or
363+
364+ d) Limiting the use for publicity purposes of names of licensors or
365+ authors of the material; or
366+
367+ e) Declining to grant rights under trademark law for use of some
368+ trade names, trademarks, or service marks; or
369+
370+ f) Requiring indemnification of licensors and authors of that
371+ material by anyone who conveys the material (or modified versions of
372+ it) with contractual assumptions of liability to the recipient, for
373+ any liability that these contractual assumptions directly impose on
374+ those licensors and authors.
375+
376+ All other non-permissive additional terms are considered "further
377+restrictions" within the meaning of section 10. If the Program as you
378+received it, or any part of it, contains a notice stating that it is
379+governed by this License along with a term that is a further
380+restriction, you may remove that term. If a license document contains
381+a further restriction but permits relicensing or conveying under this
382+License, you may add to a covered work material governed by the terms
383+of that license document, provided that the further restriction does
384+not survive such relicensing or conveying.
385+
386+ If you add terms to a covered work in accord with this section, you
387+must place, in the relevant source files, a statement of the
388+additional terms that apply to those files, or a notice indicating
389+where to find the applicable terms.
390+
391+ Additional terms, permissive or non-permissive, may be stated in the
392+form of a separately written license, or stated as exceptions;
393+the above requirements apply either way.
394+
395+ 8. Termination.
396+
397+ You may not propagate or modify a covered work except as expressly
398+provided under this License. Any attempt otherwise to propagate or
399+modify it is void, and will automatically terminate your rights under
400+this License (including any patent licenses granted under the third
401+paragraph of section 11).
402+
403+ However, if you cease all violation of this License, then your
404+license from a particular copyright holder is reinstated (a)
405+provisionally, unless and until the copyright holder explicitly and
406+finally terminates your license, and (b) permanently, if the copyright
407+holder fails to notify you of the violation by some reasonable means
408+prior to 60 days after the cessation.
409+
410+ Moreover, your license from a particular copyright holder is
411+reinstated permanently if the copyright holder notifies you of the
412+violation by some reasonable means, this is the first time you have
413+received notice of violation of this License (for any work) from that
414+copyright holder, and you cure the violation prior to 30 days after
415+your receipt of the notice.
416+
417+ Termination of your rights under this section does not terminate the
418+licenses of parties who have received copies or rights from you under
419+this License. If your rights have been terminated and not permanently
420+reinstated, you do not qualify to receive new licenses for the same
421+material under section 10.
422+
423+ 9. Acceptance Not Required for Having Copies.
424+
425+ You are not required to accept this License in order to receive or
426+run a copy of the Program. Ancillary propagation of a covered work
427+occurring solely as a consequence of using peer-to-peer transmission
428+to receive a copy likewise does not require acceptance. However,
429+nothing other than this License grants you permission to propagate or
430+modify any covered work. These actions infringe copyright if you do
431+not accept this License. Therefore, by modifying or propagating a
432+covered work, you indicate your acceptance of this License to do so.
433+
434+ 10. Automatic Licensing of Downstream Recipients.
435+
436+ Each time you convey a covered work, the recipient automatically
437+receives a license from the original licensors, to run, modify and
438+propagate that work, subject to this License. You are not responsible
439+for enforcing compliance by third parties with this License.
440+
441+ An "entity transaction" is a transaction transferring control of an
442+organization, or substantially all assets of one, or subdividing an
443+organization, or merging organizations. If propagation of a covered
444+work results from an entity transaction, each party to that
445+transaction who receives a copy of the work also receives whatever
446+licenses to the work the party's predecessor in interest had or could
447+give under the previous paragraph, plus a right to possession of the
448+Corresponding Source of the work from the predecessor in interest, if
449+the predecessor has it or can get it with reasonable efforts.
450+
451+ You may not impose any further restrictions on the exercise of the
452+rights granted or affirmed under this License. For example, you may
453+not impose a license fee, royalty, or other charge for exercise of
454+rights granted under this License, and you may not initiate litigation
455+(including a cross-claim or counterclaim in a lawsuit) alleging that
456+any patent claim is infringed by making, using, selling, offering for
457+sale, or importing the Program or any portion of it.
458+
459+ 11. Patents.
460+
461+ A "contributor" is a copyright holder who authorizes use under this
462+License of the Program or a work on which the Program is based. The
463+work thus licensed is called the contributor's "contributor version".
464+
465+ A contributor's "essential patent claims" are all patent claims
466+owned or controlled by the contributor, whether already acquired or
467+hereafter acquired, that would be infringed by some manner, permitted
468+by this License, of making, using, or selling its contributor version,
469+but do not include claims that would be infringed only as a
470+consequence of further modification of the contributor version. For
471+purposes of this definition, "control" includes the right to grant
472+patent sublicenses in a manner consistent with the requirements of
473+this License.
474+
475+ Each contributor grants you a non-exclusive, worldwide, royalty-free
476+patent license under the contributor's essential patent claims, to
477+make, use, sell, offer for sale, import and otherwise run, modify and
478+propagate the contents of its contributor version.
479+
480+ In the following three paragraphs, a "patent license" is any express
481+agreement or commitment, however denominated, not to enforce a patent
482+(such as an express permission to practice a patent or covenant not to
483+sue for patent infringement). To "grant" such a patent license to a
484+party means to make such an agreement or commitment not to enforce a
485+patent against the party.
486+
487+ If you convey a covered work, knowingly relying on a patent license,
488+and the Corresponding Source of the work is not available for anyone
489+to copy, free of charge and under the terms of this License, through a
490+publicly available network server or other readily accessible means,
491+then you must either (1) cause the Corresponding Source to be so
492+available, or (2) arrange to deprive yourself of the benefit of the
493+patent license for this particular work, or (3) arrange, in a manner
494+consistent with the requirements of this License, to extend the patent
495+license to downstream recipients. "Knowingly relying" means you have
496+actual knowledge that, but for the patent license, your conveying the
497+covered work in a country, or your recipient's use of the covered work
498+in a country, would infringe one or more identifiable patents in that
499+country that you have reason to believe are valid.
500+
501+ If, pursuant to or in connection with a single transaction or
502+arrangement, you convey, or propagate by procuring conveyance of, a
503+covered work, and grant a patent license to some of the parties
504+receiving the covered work authorizing them to use, propagate, modify
505+or convey a specific copy of the covered work, then the patent license
506+you grant is automatically extended to all recipients of the covered
507+work and works based on it.
508+
509+ A patent license is "discriminatory" if it does not include within
510+the scope of its coverage, prohibits the exercise of, or is
511+conditioned on the non-exercise of one or more of the rights that are
512+specifically granted under this License. You may not convey a covered
513+work if you are a party to an arrangement with a third party that is
514+in the business of distributing software, under which you make payment
515+to the third party based on the extent of your activity of conveying
516+the work, and under which the third party grants, to any of the
517+parties who would receive the covered work from you, a discriminatory
518+patent license (a) in connection with copies of the covered work
519+conveyed by you (or copies made from those copies), or (b) primarily
520+for and in connection with specific products or compilations that
521+contain the covered work, unless you entered into that arrangement,
522+or that patent license was granted, prior to 28 March 2007.
523+
524+ Nothing in this License shall be construed as excluding or limiting
525+any implied license or other defenses to infringement that may
526+otherwise be available to you under applicable patent law.
527+
528+ 12. No Surrender of Others' Freedom.
529+
530+ If conditions are imposed on you (whether by court order, agreement or
531+otherwise) that contradict the conditions of this License, they do not
532+excuse you from the conditions of this License. If you cannot convey a
533+covered work so as to satisfy simultaneously your obligations under this
534+License and any other pertinent obligations, then as a consequence you may
535+not convey it at all. For example, if you agree to terms that obligate you
536+to collect a royalty for further conveying from those to whom you convey
537+the Program, the only way you could satisfy both those terms and this
538+License would be to refrain entirely from conveying the Program.
539+
540+ 13. Remote Network Interaction; Use with the GNU General Public License.
541+
542+ Notwithstanding any other provision of this License, if you modify the
543+Program, your modified version must prominently offer all users
544+interacting with it remotely through a computer network (if your version
545+supports such interaction) an opportunity to receive the Corresponding
546+Source of your version by providing access to the Corresponding Source
547+from a network server at no charge, through some standard or customary
548+means of facilitating copying of software. This Corresponding Source
549+shall include the Corresponding Source for any work covered by version 3
550+of the GNU General Public License that is incorporated pursuant to the
551+following paragraph.
552+
553+ Notwithstanding any other provision of this License, you have
554+permission to link or combine any covered work with a work licensed
555+under version 3 of the GNU General Public License into a single
556+combined work, and to convey the resulting work. The terms of this
557+License will continue to apply to the part which is the covered work,
558+but the work with which it is combined will remain governed by version
559+3 of the GNU General Public License.
560+
561+ 14. Revised Versions of this License.
562+
563+ The Free Software Foundation may publish revised and/or new versions of
564+the GNU Affero General Public License from time to time. Such new versions
565+will be similar in spirit to the present version, but may differ in detail to
566+address new problems or concerns.
567+
568+ Each version is given a distinguishing version number. If the
569+Program specifies that a certain numbered version of the GNU Affero General
570+Public License "or any later version" applies to it, you have the
571+option of following the terms and conditions either of that numbered
572+version or of any later version published by the Free Software
573+Foundation. If the Program does not specify a version number of the
574+GNU Affero General Public License, you may choose any version ever published
575+by the Free Software Foundation.
576+
577+ If the Program specifies that a proxy can decide which future
578+versions of the GNU Affero General Public License can be used, that proxy's
579+public statement of acceptance of a version permanently authorizes you
580+to choose that version for the Program.
581+
582+ Later license versions may give you additional or different
583+permissions. However, no additional obligations are imposed on any
584+author or copyright holder as a result of your choosing to follow a
585+later version.
586+
587+ 15. Disclaimer of Warranty.
588+
589+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597+
598+ 16. Limitation of Liability.
599+
600+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608+SUCH DAMAGES.
609+
610+ 17. Interpretation of Sections 15 and 16.
611+
612+ If the disclaimer of warranty and limitation of liability provided
613+above cannot be given local legal effect according to their terms,
614+reviewing courts shall apply local law that most closely approximates
615+an absolute waiver of all civil liability in connection with the
616+Program, unless a warranty or assumption of liability accompanies a
617+copy of the Program in return for a fee.
618+
619+ END OF TERMS AND CONDITIONS
620+
621+ How to Apply These Terms to Your New Programs
622+
623+ If you develop a new program, and you want it to be of the greatest
624+possible use to the public, the best way to achieve this is to make it
625+free software which everyone can redistribute and change under these terms.
626+
627+ To do so, attach the following notices to the program. It is safest
628+to attach them to the start of each source file to most effectively
629+state the exclusion of warranty; and each file should have at least
630+the "copyright" line and a pointer to where the full notice is found.
631+
632+ <one line to give the program's name and a brief idea of what it does.>
633+ Copyright (C) 2023 krolyxon
634+
635+ This program is free software: you can redistribute it and/or modify
636+ it under the terms of the GNU Affero General Public License as published
637+ by the Free Software Foundation, either version 3 of the License, or
638+ (at your option) any later version.
639+
640+ This program is distributed in the hope that it will be useful,
641+ but WITHOUT ANY WARRANTY; without even the implied warranty of
642+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643+ GNU Affero General Public License for more details.
644+
645+ You should have received a copy of the GNU Affero General Public License
646+ along with this program. If not, see <https://www.gnu.org/licenses/>.
647+
648+Also add information on how to contact you by electronic and paper mail.
649+
650+ If your software can interact with users remotely through a computer
651+network, you should also make sure that it provides a way for users to
652+get its source. For example, if your program is a web application, its
653+interface could display a "Source" link that leads users to an archive
654+of the code. There are many ways you could offer source, and different
655+solutions will be better for different programs; see section 13 for the
656+specific requirements.
657+
658+ You should also get your employer (if you work as a programmer) or school,
659+if any, to sign a "copyright disclaimer" for the program, if necessary.
660+For more information on this, and how to apply and follow the GNU AGPL, see
661+<https://www.gnu.org/licenses/>.
A README.md View
@@ -0,0 +1,218 @@
1+<div align="center">
2+ <img src="docs/logo.svg" width="360" alt="remage">
3+ <p>A keyboard-centric image viewer for Linux. Fast, quiet and vim-shaped.</p>
4+
5+[![CI](https://img.shields.io/github/actions/workflow/status/krolyxon/remage/ci.yml?branch=master&style=flat-square&label=CI&labelColor=000000)](https://github.com/krolyxon/remage/actions/workflows/ci.yml)
6+[![Top language](https://img.shields.io/github/languages/top/krolyxon/remage?style=flat-square&logo=rust&logoColor=white&labelColor=000000&color=333333)](https://github.com/krolyxon/remage)
7+[![Lines of code](https://img.shields.io/badge/lines%20of%20code-17.5k-333333?style=flat-square&labelColor=000000)](src)
8+[![License](https://img.shields.io/github/license/krolyxon/remage?style=flat-square&labelColor=000000&color=333333)](LICENSE)
9+[![Platform](https://img.shields.io/badge/platform-Linux%20(Wayland%20%7C%20X11)-333333?style=flat-square&labelColor=000000)](#install)
10+[![Renderer](https://img.shields.io/badge/GPU-wgpu-D71921?style=flat-square&labelColor=000000)](https://wgpu.rs)
11+[![nsxiv](https://img.shields.io/badge/replaces-nsxiv-333333?style=flat-square&labelColor=000000)](https://codeberg.org/nsxiv/nsxiv)
12+[![Last commit](https://img.shields.io/github/last-commit/krolyxon/remage?style=flat-square&labelColor=000000&color=333333)](https://github.com/krolyxon/remage/commits)
13+
14+</div>
15+
16+remage aims for nsxiv's feature set with vim-style control and a Nothing OS look. It draws with the
17+GPU, so zooming, panning and scrolling stay smooth, and it uses no CPU at all while the picture
18+stands still. It can take its colours from your wallpaper through matugen or pywal, and you script
19+it with plain shell commands and an IPC socket.
20+
21+<p align="center">
22+ <img src="docs/screenshots/image.jpg" width="820" alt="remage showing a painting of a torii gate under cherry blossoms, with the filename on the left of the status bar and format, size, zoom and position on the right">
23+</p>
24+
25+<table>
26+ <tr>
27+ <td width="50%"><img src="docs/screenshots/grid.jpg" alt="The thumbnail grid with three marked images and the cursor on one"></td>
28+ <td width="50%"><img src="docs/screenshots/palette.jpg" alt="The command palette fuzzy-matching 'fit' over actions and their keys"></td>
29+ </tr>
30+ <tr>
31+ <td align="center"><sub>Thumbnail grid with marks</sub></td>
32+ <td align="center"><sub><code>:</code> command palette</sub></td>
33+ </tr>
34+</table>
35+
36+-----
37+
38+## Features
39+
40+- **Fast.** The first image is on screen in about 90 ms. Neighbours are decoded and uploaded ahead
41+ of time, so `n` and `p` take a single frame. Rendering happens only on demand.
42+- **Keyboard first.** Vim key notation, sequences (`gg`, `<Space>w`) and counts (`5n`, `12G`).
43+ Every feature is an action you can bind, run from the `:` palette or send over IPC. Which-key
44+ shows what a pending prefix can do, and `?` lists the live keymap.
45+- **Thumbnail grid.** Shares the freedesktop thumbnail cache with your file manager, so thumbnails
46+ are often there already. Visual mode marks ranges.
47+- **Many formats.** JPEG, PNG/APNG, GIF, WebP, JPEG XL, AVIF, HEIC, SVG, TIFF (multi-page), camera
48+ RAW, OpenEXR/HDR and more. Animations play with their own timing. ICC colour profiles and EXIF
49+ orientation are applied, and images larger than the GPU's texture limit are tiled. Anything
50+ else can go through an external decoder such as ImageMagick.
51+- **Scripting.** User commands replace nsxiv's key-handler: placeholders, marks as targets, inline
52+ confirmation and output fed back as actions. An old key-handler works unchanged.
53+- **IPC.** `remage msg next`, `remage msg --query marked` and `remage msg --subscribe image` from
54+ Hyprland binds or scripts.
55+- **Themeable.** Three colours are enough, and the rest are derived. matugen and pywal recolour
56+ open windows live when the wallpaper changes.
57+- **Configurable.** One TOML file with live reload, `file:line:col` errors and "did you mean"
58+ suggestions.
59+
60+## Install
61+
62+### Arch Linux
63+
64+A `remage-git` PKGBUILD lives in [`contrib/PKGBUILD`](contrib/PKGBUILD):
65+
66+```bash
67+git clone https://github.com/krolyxon/remage
68+cd remage/contrib
69+makepkg -si
70+```
71+
72+It installs the binary, the man page, zsh/bash/fish completions and a desktop file.
73+
74+### From source
75+
76+You need stable Rust (1.96 or newer). AVIF and HEIC use two system libraries:
77+
78+```bash
79+sudo pacman -S dav1d libheif # Arch
80+sudo apt install libdav1d-dev libheif-dev # Debian/Ubuntu (libheif ≥ 1.17)
81+
82+cargo install --locked --git https://github.com/krolyxon/remage
83+```
84+
85+Without those libraries, build with the pure-Rust formats only:
86+
87+```bash
88+cargo install --locked --git https://github.com/krolyxon/remage --no-default-features --features jxl,svg,raw
89+```
90+
91+Shell completions and the man page are generated by the binary:
92+
93+```bash
94+remage completions zsh > ~/.local/share/zsh/site-functions/_remage # also bash, fish, elvish
95+remage man > ~/.local/share/man/man1/remage.1
96+```
97+
98+## Usage
99+
100+```bash
101+remage # the current directory
102+remage photo.jpg # photo.jpg and the rest of its directory
103+remage -rt ~/Pictures # a whole tree, starting in the grid
104+remage -S=3 wallpapers/ # slideshow, 3 seconds per image
105+find . -name '*.png' | remage -i -o > picked.txt # pick images, print the marked ones on exit
106+remage --dump-config # the complete commented default config
107+```
108+
109+`remage --help` lists every option, and `man remage` covers keys, actions and IPC.
110+
111+## Keys
112+
113+The defaults below can all be changed. `?` shows the live keymap inside remage.
114+
115+| | Keys |
116+|---|---|
117+| **Everywhere** | `n`/`p` next/previous · `[`/`]` ±10 · `gg`/`G`/`12G` first/last/goto · `<CR>` image ⇄ grid · `m` mark · `M` invert marks · `u` unmark all · `N`/`P` next/previous mark · `f` fullscreen · `b` status bar · `i` info · `s` slideshow · `r` reload · `D` remove from queue · `:` palette · `/` search · `?` help · `q` quit |
118+| **Image** | `hjkl` pan · `HJKL` jump to edge · `+`/`-`/`=` zoom in/out/100% · `w`/`W`/`e`/`E` fit/fill/width/height · `z` centre · `<`/`>` rotate · `\|`/`_` flip · `a` pixel filter · `A` transparency background · `{`/`}` gamma · `<C-Space>` play/pause · `<C-n>`/`<C-p>` frames · wheel zooms at the cursor, drag pans |
119+| **Grid** | `hjkl` move · `<C-d>`/`<C-u>` half page · `<C-f>`/`<C-b>` page · `0`/`$` row start/end · `+`/`-` thumbnail size · `v` visual mode, then `m` to mark the range · click selects, double-click opens |
120+
121+## Configuration
122+
123+remage reads `~/.config/remage/config.toml`. The file is merged over the built-in defaults, so put
124+in only what you change. Edits apply as soon as you save, and `remage --check-config` validates
125+the file. [`contrib/config.example.toml`](contrib/config.example.toml) is a good start.
126+
127+```toml
128+[image]
129+fit = "down" # down | fit | fill | width | height | original
130+alpha_background = "dots" # dots | checker | solid
131+
132+[grid]
133+size = 160
134+
135+[keys.image]
136+"<C-ScrollUp>" = "next"
137+"D" = "none" # remove a default binding
138+```
139+
140+### Commands
141+
142+Commands run asynchronously and act on the marked files, or on the current one when nothing is
143+marked. Bind them as `run <name>`; unbound ones still show up in the `:` palette.
144+
145+```toml
146+[commands.delete]
147+desc = "Delete files"
148+run = ["rm", "--", "{files}"] # array: run directly, {files} becomes one arg per file
149+confirm = "Delete {count} file(s)?" # asks inline in the status bar: y / n
150+
151+[commands.copy]
152+desc = "Copy image"
153+run = "wl-copy --type image/png < {file}" # string: run through sh, placeholders are quoted
154+
155+[commands.legacy] # an nsxiv key-handler works as it is
156+run = ["~/.config/nsxiv/exec/key-handler", "w"]
157+stdin = "files"
158+
159+[keys.global]
160+"<Space>d" = "run delete"
161+"<Space>c" = "run copy"
162+```
163+
164+After a command succeeds, remage re-checks the files it touched by default. Deleted files leave
165+the queue and edited ones are decoded again, so delete and rotate scripts need no extra setup.
166+With `output = "actions"`, each line the command prints runs as an action.
167+
168+### Theming
169+
170+`nothing-dark` (the default) and `nothing-light` are built in. Any theme needs only `background`,
171+`foreground` and `accent`; the other tokens are derived.
172+
173+- **matugen:** copy [`contrib/matugen/remage.toml`](contrib/matugen/remage.toml) into your matugen
174+ templates (the file says how), then add `include = ["~/.cache/matugen/remage.toml"]` to your
175+ config. Every wallpaper change recolours open windows.
176+- **pywal:** set `[theme] pywal = "~/.cache/wal/colors.json"`.
177+
178+## Scripting over IPC
179+
180+Each instance listens on `$XDG_RUNTIME_DIR/remage/<pid>.sock` and speaks newline-delimited JSON.
181+`remage msg` talks to the newest instance (or `$REMAGE_SOCKET`, or `--pid`).
182+
183+```bash
184+remage msg next 5 # any action
185+remage msg --query marked # current | marked | queue | state
186+remage msg --subscribe image # one JSON line per change
187+
188+# Hyprland
189+bind = SUPER, bracketright, exec, remage msg next
190+```
191+
192+## Formats
193+
194+| Format | Through |
195+|---|---|
196+| JPEG, PNG/APNG, GIF, WebP, BMP, ICO, TIFF, TGA, PNM, QOI, DDS, HDR, OpenEXR | `image` |
197+| JPEG XL | `jxl-oxide` |
198+| AVIF | `dav1d` (system library) |
199+| HEIC / HEIF | `libheif` (system library) |
200+| SVG / SVGZ | `resvg`, re-rendered sharp at every zoom level |
201+| Camera RAW (CR2, CR3, NEF, ARW, DNG, RAF, ORF, RW2 …) | `rawler`, embedded preview first |
202+| Anything else | `[[decoders]]`, e.g. `magick "{file}[0]" png:-` for PSD and XCF |
203+
204+## Development
205+
206+```bash
207+cargo run --release -- ~/Pictures
208+cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test
209+RUST_LOG=remage=debug cargo run --release # startup and first-frame timings
210+REMAGE_SNAPSHOT=/tmp/x.png remage … # also write each settled frame to a PNG
211+```
212+
213+`tests/fixtures/make.sh` regenerates the decoder fixtures.
214+
215+## License
216+
217+remage is licensed under the [GNU AGPL v3](LICENSE). The embedded fonts (Space Grotesk, Space
218+Mono and Doto) are under the [SIL Open Font License](assets/fonts/OFL.txt).
A contrib/PKGBUILD View
@@ -0,0 +1,72 @@
1+# Maintainer: krolyxon <me@krolyxon.com>
2+pkgname=remage-git
3+_pkgname=remage
4+pkgver=0.1.0.r0.g0000000
5+pkgrel=1
6+pkgdesc='Keyboard-centric, GPU-rendered image viewer with vim keys and wallpaper theming'
7+arch=('x86_64' 'aarch64')
8+url='https://github.com/krolyxon/remage'
9+license=('AGPL-3.0-only' 'OFL-1.1')
10+depends=('gcc-libs' 'glibc' 'dav1d' 'libheif' 'libxkbcommon' 'wayland' 'vulkan-icd-loader')
11+makedepends=('cargo' 'git' 'pkgconf')
12+optdepends=(
13+ 'libx11: X11 support'
14+ 'libxcursor: X11 cursors'
15+ 'libxi: X11 input'
16+ 'libxrandr: X11 scaling'
17+ 'mesa: OpenGL fallback'
18+ 'imagemagick: [[decoders]] for PSD, XCF, JP2 and others'
19+ 'wl-clipboard: the example copy command'
20+ 'matugen: wallpaper theming'
21+ 'python-pywal: wallpaper theming'
22+)
23+provides=("$_pkgname")
24+conflicts=("$_pkgname")
25+options=('!lto')
26+source=("git+$url.git")
27+sha256sums=('SKIP')
28+
29+pkgver() {
30+ cd "$_pkgname"
31+ printf '%s.r%s.g%s' \
32+ "$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2)" \
33+ "$(git rev-list --count HEAD)" \
34+ "$(git rev-parse --short HEAD)"
35+}
36+
37+prepare() {
38+ cd "$_pkgname"
39+ export RUSTUP_TOOLCHAIN=stable
40+ cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
41+}
42+
43+build() {
44+ cd "$_pkgname"
45+ export RUSTUP_TOOLCHAIN=stable
46+ export CARGO_TARGET_DIR=target
47+ cargo build --frozen --release
48+ target/release/remage man > remage.1
49+ target/release/remage completions zsh > _remage
50+ target/release/remage completions bash > remage.bash
51+ target/release/remage completions fish > remage.fish
52+}
53+
54+check() {
55+ cd "$_pkgname"
56+ export RUSTUP_TOOLCHAIN=stable
57+ cargo test --frozen --release
58+}
59+
60+package() {
61+ cd "$_pkgname"
62+ install -Dm755 target/release/remage -t "$pkgdir/usr/bin/"
63+ install -Dm644 remage.1 -t "$pkgdir/usr/share/man/man1/"
64+ install -Dm644 _remage -t "$pkgdir/usr/share/zsh/site-functions/"
65+ install -Dm644 remage.bash "$pkgdir/usr/share/bash-completion/completions/remage"
66+ install -Dm644 remage.fish -t "$pkgdir/usr/share/fish/vendor_completions.d/"
67+ install -Dm644 contrib/remage.desktop -t "$pkgdir/usr/share/applications/"
68+ install -Dm644 README.md contrib/config.example.toml -t "$pkgdir/usr/share/doc/$_pkgname/"
69+ cp -r contrib/matugen contrib/pywal "$pkgdir/usr/share/doc/$_pkgname/"
70+ install -Dm644 LICENSE -t "$pkgdir/usr/share/licenses/$pkgname/"
71+ install -Dm644 assets/fonts/OFL.txt "$pkgdir/usr/share/licenses/$pkgname/OFL-fonts.txt"
72+}
A contrib/config.example.toml View
@@ -0,0 +1,86 @@
1+# Example remage config: copy to ~/.config/remage/config.toml and trim to taste.
2+#
3+# Your file is merged over the compiled-in defaults, so only list what you change.
4+# `remage --dump-config` prints every option with its default, and
5+# `remage --check-config` validates this file. Changes apply live.
6+
7+# Theme from the wallpaper. See contrib/matugen/ (or use [theme] pywal = … below).
8+include = ["~/.cache/matugen/remage.toml"]
9+
10+[general]
11+sort = "natural"
12+start_mode = "image"
13+
14+[image]
15+fit = "down"
16+alpha_background = "dots"
17+slideshow_delay = 4.0
18+
19+[grid]
20+size = 160
21+names = false
22+
23+[ui]
24+statusbar_right = "{format} {width}×{height} {zoom}% {index}/{count}"
25+
26+[theme]
27+preset = "nothing-dark"
28+# pywal = "~/.cache/wal/colors.json"
29+
30+# ── User commands ─────────────────────────────────────────────────────────────────────────
31+# These replace nsxiv's key-handler (`d`, `w` and `c` used to run through C-x). They act on
32+# the marked files if there are any, otherwise on the current one.
33+
34+[commands.delete]
35+desc = "Delete files"
36+run = ["rm", "--", "{files}"]
37+confirm = "Delete {count} file(s)?" # asks inline: y deletes, any other key cancels
38+# after = "refresh" is the default, so deleted files drop out of the queue at once.
39+
40+[commands.wallpaper]
41+desc = "Set as wallpaper"
42+run = ["setwall", "-p", "{file}"]
43+targets = "current"
44+output = "ignore"
45+
46+[commands.copy]
47+desc = "Copy image"
48+run = "wl-copy --type image/png < {file}" # X11: xclip -sel c -t image/png < {file}
49+targets = "current"
50+
51+[commands.copy-path]
52+desc = "Copy file paths"
53+run = "printf '%s\\n' {files} | wl-copy"
54+
55+[commands.rotate]
56+desc = "Rotate files 90° clockwise (re-encodes them)"
57+run = ["magick", "mogrify", "-rotate", "90", "{files}"]
58+# The default `after = "refresh"` re-decodes the rotated files.
59+
60+[commands.gimp]
61+desc = "Open in GIMP"
62+run = ["gimp", "{files}"]
63+output = "ignore"
64+after = []
65+
66+# An unmodified nsxiv key-handler still works if you'd rather keep it:
67+# [commands.legacy-delete]
68+# run = ["~/.config/nsxiv/exec/key-handler", "d"]
69+# stdin = "files"
70+
71+[keys.global]
72+"<Space>d" = "run delete"
73+"<Space>w" = "run wallpaper"
74+"<Space>c" = "run copy"
75+"<Space>y" = "run copy-path"
76+"<Space>r" = "run rotate"
77+"<Space>g" = "run gimp"
78+
79+[keys.image]
80+"<C-ScrollUp>" = "next"
81+"<C-ScrollDown>" = "prev"
82+
83+# Anything without a built-in decoder, via ImageMagick.
84+[[decoders]]
85+match = ["*.psd", "*.xcf", "*.jp2"]
86+run = ["magick", "{file}[0]", "png:-"]
A contrib/remage.desktop View
@@ -0,0 +1,12 @@
1+[Desktop Entry]
2+Type=Application
3+Name=remage
4+GenericName=Image Viewer
5+Comment=Keyboard-centric image viewer
6+Exec=remage %F
7+Icon=image-viewer
8+Terminal=false
9+StartupWMClass=remage
10+Categories=Graphics;Viewer;2DGraphics;RasterGraphics;
11+Keywords=image;picture;photo;viewer;gallery;
12+MimeType=image/jpeg;image/png;image/apng;image/gif;image/webp;image/bmp;image/x-bmp;image/x-ms-bmp;image/vnd.microsoft.icon;image/x-icon;image/tiff;image/x-tga;image/x-targa;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/qoi;image/x-qoi;image/vnd.ms-dds;image/x-dds;image/vnd.radiance;image/x-hdr;image/x-exr;image/jxl;image/avif;image/heic;image/heif;image/heic-sequence;image/heif-sequence;image/svg+xml;image/svg+xml-compressed;image/x-dcraw;image/x-adobe-dng;image/x-canon-cr2;image/x-canon-cr3;image/x-canon-crw;image/x-nikon-nef;image/x-nikon-nrw;image/x-sony-arw;image/x-sony-sr2;image/x-sony-srf;image/x-fuji-raf;image/x-olympus-orf;image/x-panasonic-rw2;image/x-panasonic-raw;image/x-pentax-pef;image/x-samsung-srw;image/x-sigma-x3f;image/x-kodak-dcr;image/x-kodak-kdc;image/x-minolta-mrw;image/x-epson-erf;image/x-hasselblad-3fr;image/x-leaf-mos;image/x-mamiya-mef;image/x-phaseone-iiq;
A docs/logo.svg View
@@ -0,0 +1,254 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 286" width="1080" height="286">
2+<!-- remage: a dot-matrix display showing a landscape (one red dot: the sun) and the name in dot-matrix capitals. -->
3+<rect width="1080" height="286" rx="24" fill="#000000"/>
4+<circle cx="48.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
5+<circle cx="48.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
6+<circle cx="48.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
7+<circle cx="48.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
8+<circle cx="48.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
9+<circle cx="48.0" cy="143.0" r="6.50" fill="#3A3A3E"/>
10+<circle cx="48.0" cy="160.0" r="6.50" fill="#3A3A3E"/>
11+<circle cx="48.0" cy="177.0" r="6.50" fill="#3A3A3E"/>
12+<circle cx="48.0" cy="194.0" r="6.50" fill="#3A3A3E"/>
13+<circle cx="48.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
14+<circle cx="48.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
15+<circle cx="65.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
16+<circle cx="65.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
17+<circle cx="65.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
18+<circle cx="65.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
19+<circle cx="65.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
20+<circle cx="65.0" cy="143.0" r="6.50" fill="#3A3A3E"/>
21+<circle cx="65.0" cy="160.0" r="6.50" fill="#3A3A3E"/>
22+<circle cx="65.0" cy="177.0" r="6.50" fill="#3A3A3E"/>
23+<circle cx="65.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
24+<circle cx="65.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
25+<circle cx="65.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
26+<circle cx="82.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
27+<circle cx="82.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
28+<circle cx="82.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
29+<circle cx="82.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
30+<circle cx="82.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
31+<circle cx="82.0" cy="143.0" r="6.50" fill="#3A3A3E"/>
32+<circle cx="82.0" cy="160.0" r="6.50" fill="#3A3A3E"/>
33+<circle cx="82.0" cy="177.0" r="6.50" fill="#F2F2F2"/>
34+<circle cx="82.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
35+<circle cx="82.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
36+<circle cx="82.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
37+<circle cx="99.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
38+<circle cx="99.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
39+<circle cx="99.0" cy="92.0" r="7.00" fill="#D71921"/>
40+<circle cx="99.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
41+<circle cx="99.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
42+<circle cx="99.0" cy="143.0" r="6.50" fill="#3A3A3E"/>
43+<circle cx="99.0" cy="160.0" r="6.50" fill="#F2F2F2"/>
44+<circle cx="99.0" cy="177.0" r="6.50" fill="#F2F2F2"/>
45+<circle cx="99.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
46+<circle cx="99.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
47+<circle cx="99.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
48+<circle cx="116.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
49+<circle cx="116.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
50+<circle cx="116.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
51+<circle cx="116.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
52+<circle cx="116.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
53+<circle cx="116.0" cy="143.0" r="6.50" fill="#3A3A3E"/>
54+<circle cx="116.0" cy="160.0" r="6.50" fill="#3A3A3E"/>
55+<circle cx="116.0" cy="177.0" r="6.50" fill="#F2F2F2"/>
56+<circle cx="116.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
57+<circle cx="116.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
58+<circle cx="116.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
59+<circle cx="133.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
60+<circle cx="133.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
61+<circle cx="133.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
62+<circle cx="133.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
63+<circle cx="133.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
64+<circle cx="133.0" cy="143.0" r="6.50" fill="#3A3A3E"/>
65+<circle cx="133.0" cy="160.0" r="6.50" fill="#3A3A3E"/>
66+<circle cx="133.0" cy="177.0" r="6.50" fill="#3A3A3E"/>
67+<circle cx="133.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
68+<circle cx="133.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
69+<circle cx="133.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
70+<circle cx="150.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
71+<circle cx="150.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
72+<circle cx="150.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
73+<circle cx="150.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
74+<circle cx="150.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
75+<circle cx="150.0" cy="143.0" r="6.50" fill="#3A3A3E"/>
76+<circle cx="150.0" cy="160.0" r="6.50" fill="#3A3A3E"/>
77+<circle cx="150.0" cy="177.0" r="6.50" fill="#F2F2F2"/>
78+<circle cx="150.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
79+<circle cx="150.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
80+<circle cx="150.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
81+<circle cx="167.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
82+<circle cx="167.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
83+<circle cx="167.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
84+<circle cx="167.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
85+<circle cx="167.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
86+<circle cx="167.0" cy="143.0" r="6.50" fill="#F2F2F2"/>
87+<circle cx="167.0" cy="160.0" r="6.50" fill="#F2F2F2"/>
88+<circle cx="167.0" cy="177.0" r="6.50" fill="#F2F2F2"/>
89+<circle cx="167.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
90+<circle cx="167.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
91+<circle cx="167.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
92+<circle cx="184.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
93+<circle cx="184.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
94+<circle cx="184.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
95+<circle cx="184.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
96+<circle cx="184.0" cy="126.0" r="6.50" fill="#F2F2F2"/>
97+<circle cx="184.0" cy="143.0" r="6.50" fill="#F2F2F2"/>
98+<circle cx="184.0" cy="160.0" r="6.50" fill="#F2F2F2"/>
99+<circle cx="184.0" cy="177.0" r="6.50" fill="#F2F2F2"/>
100+<circle cx="184.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
101+<circle cx="184.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
102+<circle cx="184.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
103+<circle cx="201.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
104+<circle cx="201.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
105+<circle cx="201.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
106+<circle cx="201.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
107+<circle cx="201.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
108+<circle cx="201.0" cy="143.0" r="6.50" fill="#F2F2F2"/>
109+<circle cx="201.0" cy="160.0" r="6.50" fill="#F2F2F2"/>
110+<circle cx="201.0" cy="177.0" r="6.50" fill="#F2F2F2"/>
111+<circle cx="201.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
112+<circle cx="201.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
113+<circle cx="201.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
114+<circle cx="218.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
115+<circle cx="218.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
116+<circle cx="218.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
117+<circle cx="218.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
118+<circle cx="218.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
119+<circle cx="218.0" cy="143.0" r="6.50" fill="#3A3A3E"/>
120+<circle cx="218.0" cy="160.0" r="6.50" fill="#F2F2F2"/>
121+<circle cx="218.0" cy="177.0" r="6.50" fill="#F2F2F2"/>
122+<circle cx="218.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
123+<circle cx="218.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
124+<circle cx="218.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
125+<circle cx="235.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
126+<circle cx="235.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
127+<circle cx="235.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
128+<circle cx="235.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
129+<circle cx="235.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
130+<circle cx="235.0" cy="143.0" r="6.50" fill="#3A3A3E"/>
131+<circle cx="235.0" cy="160.0" r="6.50" fill="#3A3A3E"/>
132+<circle cx="235.0" cy="177.0" r="6.50" fill="#3A3A3E"/>
133+<circle cx="235.0" cy="194.0" r="6.50" fill="#F2F2F2"/>
134+<circle cx="235.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
135+<circle cx="235.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
136+<circle cx="252.0" cy="58.0" r="6.50" fill="#3A3A3E"/>
137+<circle cx="252.0" cy="75.0" r="6.50" fill="#3A3A3E"/>
138+<circle cx="252.0" cy="92.0" r="6.50" fill="#3A3A3E"/>
139+<circle cx="252.0" cy="109.0" r="6.50" fill="#3A3A3E"/>
140+<circle cx="252.0" cy="126.0" r="6.50" fill="#3A3A3E"/>
141+<circle cx="252.0" cy="143.0" r="6.50" fill="#3A3A3E"/>
142+<circle cx="252.0" cy="160.0" r="6.50" fill="#3A3A3E"/>
143+<circle cx="252.0" cy="177.0" r="6.50" fill="#3A3A3E"/>
144+<circle cx="252.0" cy="194.0" r="6.50" fill="#3A3A3E"/>
145+<circle cx="252.0" cy="211.0" r="6.50" fill="#F2F2F2"/>
146+<circle cx="252.0" cy="228.0" r="6.50" fill="#F2F2F2"/>
147+<circle cx="320.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
148+<circle cx="340.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
149+<circle cx="360.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
150+<circle cx="380.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
151+<circle cx="320.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
152+<circle cx="400.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
153+<circle cx="320.0" cy="123.0" r="7.00" fill="#F2F2F2"/>
154+<circle cx="400.0" cy="123.0" r="7.00" fill="#F2F2F2"/>
155+<circle cx="320.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
156+<circle cx="340.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
157+<circle cx="360.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
158+<circle cx="380.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
159+<circle cx="320.0" cy="163.0" r="7.00" fill="#F2F2F2"/>
160+<circle cx="360.0" cy="163.0" r="7.00" fill="#F2F2F2"/>
161+<circle cx="320.0" cy="183.0" r="7.00" fill="#F2F2F2"/>
162+<circle cx="380.0" cy="183.0" r="7.00" fill="#F2F2F2"/>
163+<circle cx="320.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
164+<circle cx="400.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
165+<circle cx="440.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
166+<circle cx="460.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
167+<circle cx="480.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
168+<circle cx="500.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
169+<circle cx="520.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
170+<circle cx="440.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
171+<circle cx="440.0" cy="123.0" r="7.00" fill="#F2F2F2"/>
172+<circle cx="440.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
173+<circle cx="460.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
174+<circle cx="480.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
175+<circle cx="500.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
176+<circle cx="440.0" cy="163.0" r="7.00" fill="#F2F2F2"/>
177+<circle cx="440.0" cy="183.0" r="7.00" fill="#F2F2F2"/>
178+<circle cx="440.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
179+<circle cx="460.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
180+<circle cx="480.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
181+<circle cx="500.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
182+<circle cx="520.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
183+<circle cx="560.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
184+<circle cx="640.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
185+<circle cx="560.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
186+<circle cx="580.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
187+<circle cx="620.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
188+<circle cx="640.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
189+<circle cx="560.0" cy="123.0" r="7.00" fill="#F2F2F2"/>
190+<circle cx="600.0" cy="123.0" r="7.00" fill="#F2F2F2"/>
191+<circle cx="640.0" cy="123.0" r="7.00" fill="#F2F2F2"/>
192+<circle cx="560.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
193+<circle cx="600.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
194+<circle cx="640.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
195+<circle cx="560.0" cy="163.0" r="7.00" fill="#F2F2F2"/>
196+<circle cx="640.0" cy="163.0" r="7.00" fill="#F2F2F2"/>
197+<circle cx="560.0" cy="183.0" r="7.00" fill="#F2F2F2"/>
198+<circle cx="640.0" cy="183.0" r="7.00" fill="#F2F2F2"/>
199+<circle cx="560.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
200+<circle cx="640.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
201+<circle cx="700.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
202+<circle cx="720.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
203+<circle cx="740.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
204+<circle cx="680.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
205+<circle cx="760.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
206+<circle cx="680.0" cy="123.0" r="7.00" fill="#F2F2F2"/>
207+<circle cx="760.0" cy="123.0" r="7.00" fill="#F2F2F2"/>
208+<circle cx="680.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
209+<circle cx="700.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
210+<circle cx="720.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
211+<circle cx="740.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
212+<circle cx="760.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
213+<circle cx="680.0" cy="163.0" r="7.00" fill="#F2F2F2"/>
214+<circle cx="760.0" cy="163.0" r="7.00" fill="#F2F2F2"/>
215+<circle cx="680.0" cy="183.0" r="7.00" fill="#F2F2F2"/>
216+<circle cx="760.0" cy="183.0" r="7.00" fill="#F2F2F2"/>
217+<circle cx="680.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
218+<circle cx="760.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
219+<circle cx="820.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
220+<circle cx="840.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
221+<circle cx="860.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
222+<circle cx="880.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
223+<circle cx="800.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
224+<circle cx="800.0" cy="123.0" r="7.00" fill="#F2F2F2"/>
225+<circle cx="800.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
226+<circle cx="840.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
227+<circle cx="860.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
228+<circle cx="880.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
229+<circle cx="800.0" cy="163.0" r="7.00" fill="#F2F2F2"/>
230+<circle cx="880.0" cy="163.0" r="7.00" fill="#F2F2F2"/>
231+<circle cx="800.0" cy="183.0" r="7.00" fill="#F2F2F2"/>
232+<circle cx="880.0" cy="183.0" r="7.00" fill="#F2F2F2"/>
233+<circle cx="820.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
234+<circle cx="840.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
235+<circle cx="860.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
236+<circle cx="920.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
237+<circle cx="940.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
238+<circle cx="960.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
239+<circle cx="980.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
240+<circle cx="1000.0" cy="83.0" r="7.00" fill="#F2F2F2"/>
241+<circle cx="920.0" cy="103.0" r="7.00" fill="#F2F2F2"/>
242+<circle cx="920.0" cy="123.0" r="7.00" fill="#F2F2F2"/>
243+<circle cx="920.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
244+<circle cx="940.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
245+<circle cx="960.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
246+<circle cx="980.0" cy="143.0" r="7.00" fill="#F2F2F2"/>
247+<circle cx="920.0" cy="163.0" r="7.00" fill="#F2F2F2"/>
248+<circle cx="920.0" cy="183.0" r="7.00" fill="#F2F2F2"/>
249+<circle cx="920.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
250+<circle cx="940.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
251+<circle cx="960.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
252+<circle cx="980.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
253+<circle cx="1000.0" cy="203.0" r="7.00" fill="#F2F2F2"/>
254+</svg>
A docs/screenshots/grid.jpg View

[Binary file]

A docs/screenshots/image.jpg View

[Binary file]

A docs/screenshots/info.jpg View

[Binary file]

A docs/screenshots/palette.jpg View

[Binary file]

M src/cli.rs View
@@ -12,6 +12,9 @@ use crate::state::view::FitMode;
1212 name = "remage",
1313 version,
1414 about = "Keyboard-centric image viewer",
15+ long_about = "A keyboard-centric, GPU-rendered image viewer with vim-style keys, a thumbnail \
16+ grid, user commands and an IPC socket. With no paths it opens the current \
17+ directory; a single file opens its whole directory, starting on that file.",
1518 args_conflicts_with_subcommands = true
1619 )]
1720 pub struct Cli {
@@ -132,6 +135,16 @@ pub enum Sub {
132135 )]
133136 action: Vec<String>,
134137 },
138+
139+ /// Print a shell completion script
140+ Completions {
141+ #[arg(value_enum)]
142+ shell: clap_complete::Shell,
143+ },
144+
145+ /// Print the man page (roff)
146+ #[command(hide = true)]
147+ Man,
135148 }
136149
137150 fn parse_geometry(s: &str) -> Result<(u32, u32), String> {
@@ -181,6 +194,17 @@ mod tests {
181194 assert_eq!(Cli::try_parse_from(["remage"]).unwrap().slideshow, None);
182195 }
183196
197+ #[test]
198+ fn completions_subcommand() {
199+ let c = Cli::try_parse_from(["remage", "completions", "zsh"]).unwrap();
200+ assert!(matches!(
201+ c.command,
202+ Some(Sub::Completions {
203+ shell: clap_complete::Shell::Zsh
204+ })
205+ ));
206+ }
207+
184208 #[test]
185209 fn geometry() {
186210 assert_eq!(parse_geometry("800x600"), Ok((800, 600)));
M src/decode/color.rs View
@@ -27,6 +27,7 @@ impl ToSrgb {
2727 }
2828
2929 /// From H.273 code points (AVIF/HEIF `nclx`). sRGB/BT.709 and unspecified need nothing.
30+ #[cfg_attr(not(feature = "heif"), allow(dead_code))]
3031 pub fn from_cicp(primaries: u8, transfer: u8) -> Option<Self> {
3132 let primaries = CicpColorPrimaries::try_from(primaries).ok()?;
3233 if matches!(
M src/decode/mod.rs View
@@ -20,7 +20,9 @@ mod svg;
2020 use std::fs::File;
2121 use std::io::Read;
2222 use std::path::Path;
23−use std::sync::{Arc, LazyLock};
23+#[cfg(feature = "svg")]
24+use std::sync::Arc;
25+use std::sync::LazyLock;
2426 use std::time::Duration;
2527
2628 use anyhow::anyhow;
@@ -148,6 +150,7 @@ impl std::fmt::Debug for DecodedImage {
148150 pub struct Options {
149151 /// About how many pixels the longer side needs, for sources that can be rendered at any
150152 /// size (SVG) or offer a preview (RAW).
153+ #[cfg_attr(not(any(feature = "svg", feature = "raw")), allow(dead_code))]
151154 pub target: u32,
152155 pub auto_orient: bool,
153156 pub color: bool,
M src/main.rs View
@@ -10,6 +10,7 @@ mod decode;
1010 mod exec;
1111 mod input;
1212 mod ipc;
13+mod man;
1314 mod render;
1415 mod scan;
1516 mod state;
@@ -35,19 +36,30 @@ fn main() -> anyhow::Result<()> {
3536 let started = Instant::now();
3637 env_logger::init();
3738 let cli = cli::Cli::parse();
38− if let Some(cli::Sub::Msg {
39− pid,
40− query,
41− subscribe,
42− action,
43− }) = cli.command
44− {
45− std::process::exit(ipc::client(ipc::ClientArgs {
39+ match cli.command {
40+ Some(cli::Sub::Msg {
4641 pid,
4742 query,
4843 subscribe,
4944 action,
50− }));
45+ }) => {
46+ std::process::exit(ipc::client(ipc::ClientArgs {
47+ pid,
48+ query,
49+ subscribe,
50+ action,
51+ }));
52+ }
53+ Some(cli::Sub::Completions { shell }) => {
54+ let mut cmd = <cli::Cli as clap::CommandFactory>::command();
55+ clap_complete::generate(shell, &mut cmd, "remage", &mut std::io::stdout());
56+ return Ok(());
57+ }
58+ Some(cli::Sub::Man) => {
59+ man::render(&mut std::io::stdout().lock())?;
60+ return Ok(());
61+ }
62+ None => {}
5163 }
5264 if cli.dump_config {
5365 print!("{}", config::dump());
A src/man.rs View
@@ -0,0 +1,226 @@
1+//! The man page: clap's sections plus keys and actions generated from the compiled-in defaults.
2+
3+use std::io::Write;
4+
5+use clap::CommandFactory;
6+use clap_mangen::Man;
7+use clap_mangen::roff::{Roff, bold, italic, roman};
8+
9+use crate::action::CATALOG;
10+use crate::cli::Cli;
11+use crate::config;
12+use crate::input::keymap::Bindings;
13+
14+pub fn render(w: &mut dyn Write) -> std::io::Result<()> {
15+ let man = Man::new(Cli::command()).manual("remage manual");
16+ man.render_title(w)?;
17+ man.render_name_section(w)?;
18+ man.render_synopsis_section(w)?;
19+ man.render_description_section(w)?;
20+ man.render_options_section(w)?;
21+ man.render_subcommands_section(w)?;
22+
23+ let mut r = Roff::new();
24+ keys(&mut r);
25+ actions(&mut r);
26+ prose(&mut r);
27+ r.to_writer(w)?;
28+
29+ man.render_version_section(w)?;
30+ Ok(())
31+}
32+
33+fn section(r: &mut Roff, title: &str) {
34+ r.control("SH", [title]);
35+}
36+
37+fn item(r: &mut Roff, term: Vec<clap_mangen::roff::Inline>, body: &str) {
38+ r.control("TP", []);
39+ r.text(term);
40+ r.text([roman(body)]);
41+}
42+
43+fn para(r: &mut Roff, text: &str) {
44+ r.control("PP", []);
45+ r.text([roman(text)]);
46+}
47+
48+/// The default bindings per mode table, keys for the same action merged as in `?` help.
49+fn keys(r: &mut Roff) {
50+ section(r, "KEYS");
51+ para(
52+ r,
53+ "The default bindings. Mode tables override global ones, and visual mode also inherits \
54+ the grid table. Notation follows vim. A count typed first applies to the action \
55+ (5n, 12G, 3+). Press ? in remage for the live keymap, including your own bindings.",
56+ );
57+ let Some(tables) = config::defaults().keys.into_iter().next() else {
58+ return;
59+ };
60+ let modes: [(&str, &Bindings); 4] = [
61+ ("Global", &tables.global),
62+ ("Image mode", &tables.image),
63+ ("Grid mode", &tables.grid),
64+ ("Visual mode", &tables.visual),
65+ ];
66+ for (title, table) in modes {
67+ r.control("SS", [title]);
68+ // (catalog rank, action, keys, description)
69+ let mut rows: Vec<(usize, String, String, String)> = Vec::new();
70+ for (seq, actions) in table {
71+ let Some(actions) = actions else { continue };
72+ let Some(first) = actions.first() else {
73+ continue;
74+ };
75+ let desc = actions
76+ .iter()
77+ .map(|a| a.description())
78+ .collect::<Vec<_>>()
79+ .join(", then ");
80+ let k = crate::ui::pretty_keys(&seq.to_string());
81+ match rows.iter_mut().find(|row| row.3 == desc) {
82+ Some(row) => {
83+ row.2.push_str(", ");
84+ row.2.push_str(&k);
85+ }
86+ None => {
87+ let rank = CATALOG
88+ .iter()
89+ .position(|e| e.name == first.name())
90+ .unwrap_or(usize::MAX);
91+ rows.push((rank, first.to_string(), k, desc));
92+ }
93+ }
94+ }
95+ // Catalog order, then by action (`next` before `next 10`), as in `?` help.
96+ rows.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
97+ for (_, _, k, desc) in rows {
98+ item(r, vec![bold(k)], &desc);
99+ }
100+ }
101+}
102+
103+/// Every action in the grammar shared by keys, the : palette, IPC and command output.
104+fn actions(r: &mut Roff) {
105+ section(r, "ACTIONS");
106+ para(
107+ r,
108+ "Keys, the : palette, remage msg and commands with output = \"actions\" all speak the \
109+ same grammar: a name and its arguments.",
110+ );
111+ for e in CATALOG {
112+ let mut term = vec![bold(e.name)];
113+ if !e.args.is_empty() {
114+ term.push(roman(" "));
115+ term.push(italic(e.args));
116+ }
117+ item(r, term, e.desc);
118+ }
119+}
120+
121+fn prose(r: &mut Roff) {
122+ section(r, "CONFIGURATION");
123+ para(
124+ r,
125+ "remage reads $XDG_CONFIG_HOME/remage/config.toml (or ~/.config/remage/config.toml, or \
126+ the file given with -c). The file is merged over the compiled-in defaults, so a partial \
127+ file is fine; remage --dump-config prints the complete commented default and \
128+ remage --check-config validates a file. Changes apply live.",
129+ );
130+ para(
131+ r,
132+ "User commands live in [commands.NAME] tables and are bound as \"run NAME\". Theme \
133+ colours can come from matugen through include = [...] or from pywal with \
134+ [theme] pywal = \"~/.cache/wal/colors.json\".",
135+ );
136+
137+ section(r, "IPC");
138+ para(
139+ r,
140+ "Each instance listens on $XDG_RUNTIME_DIR/remage/PID.sock for newline-delimited JSON: \
141+ {\"action\":\"goto 5\"}, {\"query\":\"current\"} (or marked, queue, state) and \
142+ {\"subscribe\":[\"image\",\"marks\",\"mode\"]}. remage msg is a client for it.",
143+ );
144+
145+ section(r, "ENVIRONMENT");
146+ for (name, desc) in [
147+ (
148+ "REMAGE_SOCKET",
149+ "The socket remage msg talks to. Set for user commands.",
150+ ),
151+ (
152+ "REMAGE_FILE, REMAGE_INDEX, REMAGE_COUNT, REMAGE_MODE, REMAGE_PID",
153+ "Set for user commands: the current file, its 1-based index, the number of targets, \
154+ the mode and the viewer's pid.",
155+ ),
156+ (
157+ "XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_RUNTIME_DIR",
158+ "Where the config, the shared thumbnail cache and the IPC sockets live.",
159+ ),
160+ (
161+ "WGPU_BACKEND, WGPU_POWER_PREF",
162+ "Override the GPU backend (vulkan, gl) and power preference (low, high).",
163+ ),
164+ (
165+ "RUST_LOG",
166+ "Logging, e.g. remage=debug for startup and first-frame timings.",
167+ ),
168+ ] {
169+ item(r, vec![bold(name)], desc);
170+ }
171+
172+ section(r, "SIGNALS");
173+ item(r, vec![bold("SIGUSR1")], "Reload the config.");
174+
175+ section(r, "FILES");
176+ for (name, desc) in [
177+ ("~/.config/remage/config.toml", "The config file."),
178+ (
179+ "~/.cache/thumbnails/",
180+ "The freedesktop thumbnail cache, shared with file managers.",
181+ ),
182+ (
183+ "/usr/share/doc/remage/config.example.toml",
184+ "An example config with user commands.",
185+ ),
186+ ] {
187+ item(r, vec![bold(name)], desc);
188+ }
189+
190+ section(r, "EXAMPLES");
191+ for (cmd, desc) in [
192+ ("remage", "Open the current directory."),
193+ (
194+ "remage photo.jpg",
195+ "Open photo.jpg with the rest of its directory.",
196+ ),
197+ ("remage -rt ~/Pictures", "Browse a tree in the grid."),
198+ (
199+ "find . -name '*.png' | remage -i -o",
200+ "Pick images from a list; print the marked ones on exit.",
201+ ),
202+ ("remage msg next", "Advance the newest instance."),
203+ (
204+ "remage msg --subscribe image",
205+ "Print a JSON line whenever the image changes.",
206+ ),
207+ ] {
208+ item(r, vec![bold(cmd)], desc);
209+ }
210+
211+ section(r, "SEE ALSO");
212+ r.text([roman("nsxiv(1), matugen(1), wal(1)")]);
213+}
214+
215+#[cfg(test)]
216+mod tests {
217+ #[test]
218+ fn renders_keys_and_actions() {
219+ let mut out = Vec::new();
220+ super::render(&mut out).unwrap();
221+ let page = String::from_utf8(out).unwrap();
222+ for needle in [".SH KEYS", ".SH ACTIONS", "slideshow\\-delay", "Next image"] {
223+ assert!(page.contains(needle), "missing {needle}");
224+ }
225+ }
226+}
M src/state/mod.rs View
@@ -1216,8 +1216,9 @@ impl AppState {
12161216 .unwrap_or_else(|| self.view.display_scale(now) > view::NEAREST_ABOVE)
12171217 }
12181218
1219+ /// The grid never waits on a full decode, so a load left over from image mode stays quiet.
12191220 pub fn loading_visible(&self, now: Instant) -> bool {
1220− self.loading_since.is_some_and(|t| now >= t + LOADING_DELAY)
1221+ !self.in_grid() && self.loading_since.is_some_and(|t| now >= t + LOADING_DELAY)
12211222 }
12221223
12231224 /// Whether frames must keep coming (animations, held keys).
@@ -1249,7 +1250,7 @@ impl AppState {
12491250 wake(m.until);
12501251 }
12511252 }
1252− if let Some(t) = self.loading_since {
1253+ if let Some(t) = self.loading_since.filter(|_| !self.in_grid()) {
12531254 let at = t + LOADING_DELAY;
12541255 if now < at {
12551256 wake(at);
@@ -1328,6 +1329,16 @@ mod tests {
13281329 assert_eq!(s.queue.index(), 0);
13291330 }
13301331
1332+ #[test]
1333+ fn grid_hides_a_pending_load() {
1334+ let (mut s, now) = state(3);
1335+ s.loading_since = Some(now);
1336+ let later = now + LOADING_DELAY;
1337+ assert!(s.loading_visible(later));
1338+ s.set_mode(Mode::Grid, now);
1339+ assert!(!s.loading_visible(later));
1340+ }
1341+
13311342 #[test]
13321343 fn bar_shrinks_the_image_area() {
13331344 let (mut s, now) = state(1);