├── .github
├── FUNDING.yml
└── workflows
│ └── cbuild.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── cfm.1
├── cfm.c
├── config.def.h
└── screenshot.png
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: ['willeccles']
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: ['https://www.warchild.org/', 'https://twloha.com']
13 |
--------------------------------------------------------------------------------
/.github/workflows/cbuild.yml:
--------------------------------------------------------------------------------
1 | name: CFM Build
2 |
3 | on:
4 | push:
5 | paths:
6 | - "*.c"
7 | - "*.h"
8 | pull_request:
9 | paths:
10 | - "*.c"
11 | - "*.h"
12 |
13 | jobs:
14 | build:
15 | strategy:
16 | matrix:
17 | os: [ubuntu-18.04, ubuntu-16.04, ubuntu-latest, macOS-10.15, macOS-latest]
18 |
19 | runs-on: ${{ matrix.os }}
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: make
24 | run: make
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | cfm
2 | config.h
3 | *.swp
4 | *.o
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TARGET = cfm
2 | SRC = cfm.c
3 | CONF = config.h
4 | DEFCONF = config.def.h
5 | MANPAGE = cfm.1
6 | PREFIX ?= /usr/local
7 |
8 | CFLAGS += -O3 -s -std=c11 -Wall -W -pedantic
9 | CPPFLAGS += -D_XOPEN_SOURCE=700
10 |
11 | .PHONY: all install uninstall clean
12 |
13 | all: $(TARGET)
14 |
15 | $(TARGET): $(CONF) $(SRC)
16 | $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(SRC) -o $@
17 |
18 | $(CONF):
19 | @cp -v $(DEFCONF) $(CONF)
20 |
21 | install: $(TARGET)
22 | mkdir -p $(DESTDIR)$(PREFIX)/bin
23 | mkdir -p $(DESTDIR)$(PREFIX)/share/man/man1
24 | install -m755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(TARGET)
25 | install -m644 $(MANPAGE) $(DESTDIR)$(PREFIX)/share/man/man1/$(MANPAGE)
26 |
27 | uninstall:
28 | $(RM) $(DESTDIR)$(PREFIX)/bin/$(TARGET)
29 | $(RM) $(DESTDIR)$(PREFIX)/share/man/man1/$(MANPAGE)
30 |
31 | clean:
32 | $(RM) $(TARGET)
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cactus File Manager
2 |
3 | 
4 |
5 | Cactus File Manager (cfm) is a TUI file manager with the goal of being simple,
6 | easy, and bloat-free, utilizing Vi-inspired keybinds. Whether or not
7 | you should use it depends on whether or not you like the name, dev, or
8 | screenshot, just like with all software.
9 |
10 | 
11 |
12 | *Note: the screenshot above has a non-default pointer and no alternative
13 | views enabled.*
14 |
15 | [**Demo**](https://asciinema.org/a/297087) showing off deletion, undo, marks, and basic
16 | navigation.
17 |
18 | ## Configuration
19 |
20 | To configure cfm before building, you should copy `config.def.h` to `config.h`
21 | and then modify it to suit your needs. Each option is explained within the file.
22 | If you build cfm without creating a config, it will create a default one for
23 | you.
24 |
25 | There are some options which cfm will attempt to use environment variables for.
26 | These are `$EDITOR`, `$SHELL`, and `$OPENER`. If you want to specify a specific
27 | option just for cfm, it will first try to find these variables prefixed with
28 | `$CFM_`. For example, if your `$EDITOR` is set to vim but you want to tell
29 | cfm to use emacs, you could use `export CFM_EDITOR='emacs'`. If you installed
30 | cfm via a package manager, or if you are using the default configuration, you
31 | can specify these environment variables to configure cfm without rebuilding.
32 | cfm uses a temporary directory for its deleted files (to enable undo and
33 | cut/paste). If it's not set in `config.h`, then cfm will attempt to use the
34 | `$CFM_TMP` environment variable. If this is not set either, then `/tmp/cfmtmp`
35 | will be used. If a temporary directory is not specified in any way or it cannot
36 | create the directory it is attempting to use, cfm will disable undo and
37 | cut/paste. If `CD_ON_CLOSE` is not enabled at compile-time, cfm will look for
38 | the `$CFM_CD_ON_CLOSE` environment variable, which should contain the path to a
39 | file where cfm should write its current working directory when quit with
40 | Q.
41 |
42 | ## Building
43 |
44 | When building from source, you should get the source for the [latest
45 | release](https://github.com/WillEccles/cfm/releases) and then run `make` inside
46 | the extracted source.
47 |
48 | ## Installing
49 |
50 | ### From Source
51 |
52 | First, download the [latest release](https://github.com/WillEccles/cfm/releases)'s source.
53 | Then, use `sudo make install`. You can specify a `PREFIX` or `DESTDIR` like with many
54 | makefiles. By default, `PREFIX` is `/usr/local/`, but if you wish to install
55 | into `/usr`, you can do `sudo make install PREFIX=/usr`.
56 |
57 | ### With a Package Manager
58 |
59 | At the moment, cfm is available from the following sources (not all maintained
60 | by me):
61 |
62 | [](https://repology.org/project/cfm/versions)
63 |
64 | ## Bugs
65 |
66 | If you find a bug, please create an issue on GitHub.
67 |
68 | ## Usage
69 |
70 | The functions of some keys may be dependent on values set in `config.h`.
71 |
72 | | Key(s) | Function |
73 | | ------ | -------- |
74 | | q, Esc | Quit cfm |
75 | | Q | Quit cfm, saving its working directory to the file specified in `CD_ON_CLOSE`, if enabled. Disabled by default. |
76 | | h | Go up a directory[1](#1) |
77 | | j | Move down[1](#1) |
78 | | PgDn, J | Move down by one full screen |
79 | | k | Move up[1](#1) |
80 | | PgUp, K | Mode down by one full screen |
81 | | ~ | Navigate to user home directory |
82 | | / | Navigate to the system root directory |
83 | | l | Enter directory, or open file in `EDITOR`[1](#1) |
84 | | dd | Delete currently selected file or directory (there is no confirmation, be careful), backing it up to the `CFM_TMP` directory if one exists, such that it can be undone with u |
85 | | Alt+dd | Works the same as dd, but is always permanent, even if a `CFM_TMP` directory exists. This is useful for huge files/directories that would take a while to copy. Be careful! |
86 | | T | Creates a new file, opening `EDITOR` to obtain a filename[2](#2) |
87 | | M | Creates a new directory, opening `EDITOR` to obtain a directory name[2](#2) |
88 | | R | Renames a file, opening `EDITOR` to edit the filename[2](#2) |
89 | | gg | Move to top |
90 | | G | Move to bottom |
91 | | m, Space | Mark for deletion |
92 | | D | Delete marked files (does not touch unmarked files) |
93 | | u | Undo the last deletion operation (if cfm was unable to access/create its trash directory `~/.cfmtrash`, deletion is permanent and this will not work) |
94 | | X | Cut the current file or directory (to be pasted again with p) |
95 | | yy | Copy the current file or directory (to be pasted again with p) |
96 | | p | Paste the previously copied or cut file or directory |
97 | | e | Open file or directory in `EDITOR` |
98 | | o | Open file or directory in `OPENER` |
99 | | S | Spawns a `SHELL` in the current directory |
100 | | r | Reload directory |
101 | | . | Toggle visibility of hidden files (dotfiles) |
102 | | Return | Works like o if `ENTER_OPENS` was enabled at compile-time, else works like l |
103 | | Tab | Switches to the next view |
104 | | \` | Switches to the previous view |
105 | | 1...0 | Switches to view N, up to the number specified by `VIEW_COUNT` (default 2) |
106 |
107 | ---
108 |
109 | 1 The arrow keys work the same as
110 | hjkl.
111 |
112 | 2 The available characters for filenames are `A-Za-z
113 | ._-` by default, which is POSIX "fully portable filenames" plus spaces. If
114 | you wish, you can disable spaces by setting `ALLOW_SPACES` to 0.
115 |
116 | ## Scripting
117 |
118 | If `stdin` or `stdout` are not attached to a TTY, cfm will read commands from
119 | `stdin` until either EOF is reached or it does not read any more data. This can
120 | be used to script operations. All errors will be printed to `stderr` and are
121 | fatal. In scripting mode, cfm will never draw to the screen. Note: in
122 | non-interactive mode, cfm will NOT backup files on deletion. This means that you
123 | cannot use dd followed by u! All deletions made in
124 | non-interactive mode will be final.
125 |
126 | Example:
127 |
128 | ```
129 | $ cat script.txt
130 | jjljjjkyyhp
131 | $ cfm j
138 | 2. Go into the currently selected directory with l
139 | 3. Go down three times with j and up once with k
140 | 4. Yank the current file with yy
141 | 5. Go back to the parent directory with h
142 | 6. Paste the yanked file with p
143 |
--------------------------------------------------------------------------------
/cfm.1:
--------------------------------------------------------------------------------
1 | .TH CFM 1 "2022 April 28"
2 | .
3 | .SH NAME
4 | cfm \- cactus file manager
5 | .
6 | .SH SYNOPSIS
7 | .B cfm
8 | .RI [ DIR ]
9 | .
10 | .SH DESCRIPTION
11 | .B cfm
12 | is a simple, clean, fast TUI file manager.
13 | .
14 | .SH OPTIONS
15 | .TP
16 | .I DIR
17 | Directory to open
18 | .B cfm
19 | in, or the working directory if not specified.
20 | .
21 | .SH CONFIGURATION
22 | Copy
23 | .I config.def.h
24 | to
25 | .I config.h
26 | and modify as needed to configure at build-time.
27 | By default,
28 | .B cfm
29 | will attempt to read the
30 | .BR EDITOR , SHELL ", and " OPENER
31 | environment variables.
32 | If it is preferable for
33 | .B cfm
34 | to use a different value for any of these, it can be specified by prepending
35 | .B CFM_
36 | to any of the variables.
37 | .B cfm
38 | will also check for the
39 | .B CFM_TMP
40 | environment variable for a location for its temp directory.
41 | If the variable is not set and
42 | .B CFM_TMP
43 | was not set in
44 | .IR config.h ,
45 | it will attempt to use
46 | .IR /tmp/cfmtmp .
47 | If the temporary directory cannot be created, then
48 | .B cfm
49 | will disable cut/paste and deletions will be permanent (no undo).
50 | If the
51 | .B CD_ON_CLOSE
52 | option is not enabled at compile-time (default off),
53 | .B cfm
54 | will also check for the presence of the
55 | .B CFM_CD_ON_CLOSE
56 | environment variable, which should contain the path to a file into which it can
57 | save its current working directory when quit with
58 | .BR Q .
59 | .
60 | .SH USAGE
61 | Note: Arrow keys work the same as hjkl.
62 | .TP
63 | .B j k
64 | Move up/down.
65 | .
66 | .TP
67 | .B PgUp PgDn
68 | Move up/down by one full screen.
69 | .
70 | .TP
71 | .B h l
72 | Move left/right.
73 | .
74 | .TP
75 | .B gg
76 | Move to top.
77 | .
78 | .TP
79 | .B G
80 | Move to bottom.
81 | .
82 | .TP
83 | .B ~
84 | Move to user home directory.
85 | .
86 | .TP
87 | .B /
88 | Move to system root directory.
89 | .
90 | .TP
91 | .B l
92 | Open file using
93 | .BR EDITOR ,
94 | or change into directory.
95 | .
96 | .TP
97 | .B o
98 | Open file or directory with
99 | .BR OPENER .
100 | .
101 | .TP
102 | .B RET
103 | Works like
104 | .B o
105 | if
106 | .I ENTER_OPENS
107 | is enabled, else works like
108 | .BR l .
109 | .
110 | .TP
111 | .B .
112 | Toggles visibility of dotfiles.
113 | .
114 | .TP
115 | .B e
116 | Open file or directory in
117 | .IR EDITOR .
118 | .
119 | .TP
120 | .B r
121 | Reload directory.
122 | .
123 | .TP
124 | .B S
125 | Spawn a shell (defined by
126 | .BR SHELL
127 | in a directory.
128 | .
129 | .TP
130 | .B T
131 | Create a new file, opening
132 | .B EDITOR
133 | to enter a filename.
134 | .
135 | .TP
136 | .B M
137 | Create a new directory, opening
138 | .B EDITOR
139 | to enter a directory name.
140 | .
141 | .TP
142 | .B R
143 | Rename a file by opening
144 | .B EDITOR
145 | to edit the filename.
146 | .
147 | .IP
148 | Note: For
149 | .BR T ,
150 | .BR M ,
151 | and
152 | .BR R ,
153 | the allowed characters default to POSIX "fully portable filename" standards
154 | with the addition of spaces:
155 | .IR "[A-Za-z0-9 ._-]" .
156 | Spaces can be disabled by setting
157 | .B ALLOW_SPACES
158 | to 0 in
159 | .IR config.h .
160 | .
161 | .TP
162 | .B dd
163 | Delete current selection (does not touch marked files).
164 | This will copy the file/directory into the
165 | .B CFM_TMP
166 | directory and can be undone with
167 | .BR u .
168 | If the
169 | .B CFM_TMP
170 | directory does not exist, deletions will be permanent.
171 | .
172 | .TP
173 | .B alt+dd
174 | Permanently delete current selection (does not touch marked files) without
175 | backing up for undo like
176 | .B dd
177 | does.
178 | This is useful for large directories where a copy will take a long time.
179 | .
180 | .TP
181 | .B m Space
182 | Toggle mark for deletion.
183 | .
184 | .TP
185 | .B D
186 | Delete all marked files (does not touch unmarked files).
187 | .
188 | .TP
189 | .B u
190 | Undo the last deletion operation (does not work if
191 | .B cfm
192 | was unable to create or access its trash directory, in which case deletion is permanent).
193 | .
194 | .TP
195 | .B X
196 | Cut the selected file or directory (can be pasted again with
197 | .BR p ).
198 | Not available if the tmp directory is not available.
199 | .
200 | .TP
201 | .B yy
202 | Copy the selected file or directory (can be pasted again with
203 | .BR p ).
204 | .
205 | .TP
206 | .B p
207 | Paste the previously cut or copied file or directory; shared between all views.
208 | .
209 | .TP
210 | .B TAB
211 | Switches to the next view.
212 | .
213 | .TP
214 | .B \`
215 | Switches to the previous view.
216 | .
217 | .TP
218 | .B 1\(en0
219 | Switches to view
220 | .IR N ,
221 | up to the number specified by
222 | .B VIEW_COUNT
223 | (2 by default).
224 | .
225 | .TP
226 | .B q ESC
227 | Quit
228 | .BR cfm .
229 | .
230 | .TP
231 | .B Q
232 | Quit
233 | .BR cfm ,
234 | saving its working directory to the file specified in
235 | .BR CD_ON_CLOSE ,
236 | if enabled, or the
237 | .B CFM_CD_ON_CLOSE
238 | environment variable if not.
239 | .
240 | .SH SCRIPTING
241 | If stdin or stdout are not attached to a TTY,
242 | .B cfm
243 | will read commands from stdin and quit at EOF, skipping all drawing.
244 | Any errors are fatal and will be printed to stderr.
245 | Note that in non-interactive mode,
246 | .B cfm
247 | will
248 | .B NOT
249 | backup files when deleting.
250 | All deletions will be permanent.
251 | .
252 | .SH AUTHOR
253 | Will Eccles \(lawill@eccles.dev\(ra.
254 | For more information, see the
255 | .UR https://github.com/willeccles/cfm
256 | GitHub repo
257 | .UE .
258 |
--------------------------------------------------------------------------------
/cfm.c:
--------------------------------------------------------------------------------
1 | /* vim: set ai ts=4 et sw=4 tw=80 cino=ws,l1: */
2 | /* Copyright (c) Will Eccles
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | #ifdef __APPLE__
10 | # define _DARWIN_C_SOURCE
11 | #elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
12 | # define __BSD_VISIBLE 1
13 | #endif
14 |
15 | #include
16 | #include
17 | #include
18 | #include
19 | #include
20 | #include
21 | #include
22 | #include
23 | #include
24 | #include
25 | #include
26 | #include
27 | #include
28 | #include
29 | #include
30 | #include
31 | #include
32 | #include
33 | #include
34 | #include
35 | #include
36 |
37 | // include user config
38 | #include "config.h"
39 |
40 | #ifdef __GNUC__
41 | # define UNUSED(x) UNUSED_##x __attribute__((unused))
42 | #else
43 | # define UNUSED(x) UNUSED_##x
44 | #endif
45 |
46 | #define K_ESC '\033'
47 | #define ESC_UP 'A'
48 | #define ESC_DOWN 'B'
49 | #define ESC_LEFT 'D'
50 | #define ESC_RIGHT 'C'
51 | #define K_ALT(k) ((int)(k) | (int)0xFFFFFF00)
52 |
53 | // arbitrary values for keys
54 | #define KEY_PGUP 'K'
55 | #define KEY_PGDN 'J'
56 |
57 | #define LIST_ALLOC_SIZE 64
58 |
59 | #ifndef POINTER
60 | # define POINTER "->"
61 | #endif /* POINTER */
62 |
63 | #ifndef BOLD_POINTER
64 | # define BOLD_POINTER 1
65 | #endif
66 |
67 | #ifndef INVERT_SELECTION
68 | # define INVERT_SELECTION 1
69 | #endif
70 |
71 | #ifndef INVERT_FULL_SELECTION
72 | # define INVERT_FULL_SELECTION 1
73 | #endif
74 |
75 | #ifndef INDENT_SELECTION
76 | # define INDENT_SELECTION 1
77 | #endif
78 |
79 | #ifdef OPENER
80 | # if defined ENTER_OPENS && ENTER_OPENS
81 | # define ENTER_OPEN
82 | # else
83 | # undef ENTER_OPEN
84 | # endif
85 | #else
86 | # undef ENTER_OPENS
87 | # undef ENTER_OPEN
88 | #endif
89 |
90 | #ifndef MARK_SYMBOL
91 | # define MARK_SYMBOL '^'
92 | #endif
93 |
94 | #ifndef ALLOW_SPACES
95 | # define ALLOW_SPACES 1
96 | #endif
97 |
98 | #ifndef ABBREVIATE_HOME
99 | # define ABBREVIATE_HOME 1
100 | #endif
101 |
102 | #ifdef VIEW_COUNT
103 | # if VIEW_COUNT > 10
104 | # undef VIEW_COUNT
105 | # define VIEW_COUNT 10
106 | # elif VIEW_COUNT <= 0
107 | # undef VIEW_COUNT
108 | # define VIEW_COUNT 1
109 | # endif
110 | #else
111 | # define VIEW_COUNT 2
112 | #endif
113 |
114 | #define COPYFLAGS (COPYFILE_ALL | COPYFILE_EXCL | COPYFILE_NOFOLLOW | COPYFILE_RECURSIVE)
115 |
116 | enum elemtype {
117 | ELEM_DIR,
118 | ELEM_LINK,
119 | ELEM_DIRLINK,
120 | ELEM_EXEC,
121 | ELEM_FILE,
122 | };
123 |
124 | static const char* elemtypestrings[] = {
125 | "dir",
126 | "@file",
127 | "@dir",
128 | "exec",
129 | "file",
130 | };
131 |
132 | struct listelem {
133 | enum elemtype type;
134 | char name[NAME_MAX+1];
135 | bool marked;
136 | };
137 |
138 | #define E_DIR(t) ((t)==ELEM_DIR || (t)==ELEM_DIRLINK)
139 |
140 | static int del_id = 0;
141 | static int mdel_id = 0;
142 |
143 | struct deletedfile {
144 | char* original;
145 | int id;
146 | bool mass;
147 | int massid;
148 | struct deletedfile* prev;
149 | };
150 |
151 | struct savedpos {
152 | size_t pos, sel;
153 | struct savedpos* prev;
154 | };
155 |
156 | static struct termios old_term;
157 | static atomic_bool redraw = false;
158 | static atomic_bool resize = false;
159 | static int rows, cols;
160 | static int pointerwidth = 2;
161 | static char editor[PATH_MAX+1];
162 | static char opener[PATH_MAX+1];
163 | static char shell[PATH_MAX+1];
164 | static char tmpdir[PATH_MAX+1];
165 | static char cdonclosefile[PATH_MAX+1];
166 |
167 | static atomic_bool interactive = true;
168 |
169 | /*
170 | * Hash table for files created by the copy function.
171 | * The source for this was heavily inspired by libbb since I am too
172 | * lazy to make my own entirely from scratch.
173 | */
174 | #define HASH_SIZE 311U // prime number
175 | #define hashfn(ino) ((unsigned)(ino) % HASH_SIZE)
176 |
177 | struct hashed_file {
178 | ino_t ino;
179 | dev_t dev;
180 | struct hashed_file* next;
181 | bool isdir;
182 | };
183 |
184 | static struct hashed_file** file_table;
185 |
186 | /*
187 | * Returns whether or not a file is in the hash table.
188 | */
189 | static bool is_file_hashed(const struct stat* st) {
190 | if (!file_table) {
191 | return false;
192 | }
193 | struct hashed_file* elem = file_table[hashfn(st->st_ino)];
194 | while (elem != NULL) {
195 | if (elem->ino == st->st_ino
196 | && elem->dev == st->st_dev
197 | && elem->isdir == !!S_ISDIR(st->st_mode)) {
198 | return true;
199 | }
200 | elem = elem->next;
201 | }
202 | return false;
203 | }
204 |
205 | /*
206 | * Hashes a file and adds it to the hash table.
207 | */
208 | static void add_file_hash(const struct stat* st) {
209 | struct hashed_file* elem = malloc(sizeof(struct hashed_file));
210 | elem->ino = st->st_ino;
211 | elem->dev = st->st_dev;
212 | elem->isdir = !!S_ISDIR(st->st_mode);
213 | if (!file_table) {
214 | file_table = calloc(HASH_SIZE, sizeof(*file_table));
215 | }
216 | int i = hashfn(st->st_ino);
217 | elem->next = file_table[i];
218 | file_table[i] = elem;
219 | }
220 |
221 | /*
222 | * Cleans up the file table.
223 | */
224 | static void reset_file_table(void) {
225 | if (!file_table) {
226 | return;
227 | }
228 |
229 | struct hashed_file* elem;
230 | struct hashed_file* next;
231 | for (size_t i = 0; i < HASH_SIZE; i++) {
232 | elem = file_table[i];
233 | while (elem != NULL) {
234 | next = elem->next;
235 | free(elem);
236 | elem = next;
237 | }
238 | }
239 | free(file_table);
240 | file_table = NULL;
241 | }
242 |
243 | /*
244 | * Unlinks a file. Used for deldir.
245 | */
246 | static int rmFiles(const char *pathname, const struct stat *sbuf, int type, struct FTW *ftwb) {
247 | (void)sbuf; (void)type; (void)ftwb;
248 | return remove(pathname);
249 | }
250 |
251 | /*
252 | * Deletes a directory, even if it contains files.
253 | */
254 | static int deldir(const char* dir) {
255 | return nftw(dir, rmFiles, 512, FTW_DEPTH | FTW_MOUNT | FTW_PHYS);
256 | }
257 |
258 | /*
259 | * If the path pointed to by f is a file, unlinks the file.
260 | * Else, deletes the directory.
261 | *
262 | * Returns 0 on success.
263 | */
264 | static int del(const char* f) {
265 | struct stat fst;
266 | if (0 != lstat(f, &fst)) {
267 | return -1;
268 | }
269 |
270 | if (S_ISDIR(fst.st_mode)) {
271 | return deldir(f);
272 | } else {
273 | return unlink(f);
274 | }
275 | }
276 |
277 | /*
278 | * Stats a file and returns 1 if it exists or 0 if
279 | * it doesn't exist. It returns -1 if there was an error.
280 | */
281 | static int exists(const char* path) {
282 | struct stat fst;
283 | if (0 != lstat(path, &fst)) {
284 | if (errno == ENOENT) {
285 | return 0;
286 | } else {
287 | return -1;
288 | }
289 | } else {
290 | return 1;
291 | }
292 | }
293 |
294 | /*
295 | * Get the base name of a file.
296 | * This is the same thing as running `basename x/y/z` at
297 | * the command line.
298 | */
299 | static char* basename(const char* path) {
300 | char* r = strrchr(path, '/');
301 | if (r) {
302 | r++;
303 | }
304 | return r;
305 | }
306 |
307 | /*
308 | * Gets the working directory the same way as getcwd,
309 | * but checks $PWD first. If it is valid, then it will be
310 | * stored in buf, else getcwd will be called. This fixes
311 | * paths with one or more symlinks in them.
312 | *
313 | * This is a *little bit* identical to the GNU extension,
314 | * get_current_dir_name().
315 | */
316 | static void getrealcwd(char* buf, size_t size) {
317 | char* pwd;
318 | pwd = getenv("PWD");
319 | struct stat dotstat, pwdstat;
320 | if (pwd != NULL
321 | && stat(".", &dotstat) == 0
322 | && stat(pwd, &pwdstat) == 0
323 | && pwdstat.st_dev == dotstat.st_dev
324 | && pwdstat.st_ino == dotstat.st_ino) {
325 | strncpy(buf, pwd, size);
326 | } else {
327 | (void)getcwd(buf, size);
328 | }
329 | }
330 |
331 | /*
332 | * This is the internal portion. Use the cpfile function instead.
333 | * Copy file or directory recursively.
334 | * Returns 0 on success, -1 on error.
335 | */
336 | static int cpfile_inner(const char* src, const char* dst) {
337 | char* b = basename(src);
338 | if (b && (0 == strcmp(b, ".") || 0 == strcmp(b, ".."))) {
339 | return 0;
340 | }
341 |
342 | struct stat srcstat, dststat;
343 | bool dstexist = false;
344 | int s = 0;
345 |
346 | if (lstat(src, &srcstat) < 0) {
347 | // couldn't stat source
348 | return -1;
349 | }
350 |
351 | // if we already created this file we don't want to create it again
352 | if (is_file_hashed(&srcstat)) {
353 | return 0;
354 | }
355 |
356 | if (lstat(dst, &dststat) < 0) {
357 | // couldn't stat target
358 | if (errno != ENOENT) {
359 | return -1;
360 | }
361 | } else {
362 | if (srcstat.st_dev == dststat.st_dev && srcstat.st_ino == dststat.st_ino) {
363 | // same file
364 | return -1;
365 | }
366 | dstexist = true;
367 | }
368 |
369 | if (dstexist) {
370 | return -1;
371 | }
372 |
373 | if (S_ISDIR(srcstat.st_mode)) {
374 | mode_t smask = umask(0);
375 | mode_t mode = srcstat.st_mode & ~smask;
376 | mode |= S_IRWXU;
377 | if (mkdir(dst, mode) < 0) {
378 | umask(smask);
379 | return -1;
380 | }
381 | umask(smask);
382 |
383 | struct stat newst;
384 | if (lstat(dst, &newst) < 0) {
385 | return -1;
386 | }
387 | add_file_hash(&newst);
388 |
389 | DIR* d = opendir(src);
390 | if (d == NULL) {
391 | s = -1;
392 | goto preserve;
393 | }
394 |
395 | struct dirent* de;
396 | while ((de = readdir(d)) != NULL) {
397 | char *ns = malloc(PATH_MAX);
398 | if (ns == NULL) {
399 | s = -1;
400 | }
401 |
402 | char *nd = malloc(PATH_MAX);
403 | if (nd == NULL) {
404 | free(ns);
405 | s = -1;
406 | }
407 |
408 | snprintf(ns, PATH_MAX, "%s/%s", src, de->d_name);
409 | snprintf(nd, PATH_MAX, "%s/%s", dst, de->d_name);
410 | if (cpfile_inner(ns, nd) != 0) {
411 | s = -1;
412 | }
413 |
414 | if (ns) free(ns);
415 | if (nd) free(nd);
416 | }
417 |
418 | closedir(d);
419 |
420 | chmod(dst, srcstat.st_mode & ~smask);
421 | goto preserve;
422 | }
423 |
424 | if (S_ISREG(srcstat.st_mode)) {
425 | int sfd, dfd;
426 | mode_t nmode;
427 |
428 | if (S_ISLNK(srcstat.st_mode)) {
429 | goto notreg;
430 | }
431 |
432 | sfd = open(src, O_RDONLY);
433 | if (sfd == -1) {
434 | return -1;
435 | }
436 |
437 | nmode = srcstat.st_mode;
438 | if (!S_ISREG(srcstat.st_mode)) {
439 | nmode = 0666;
440 | }
441 |
442 | dfd = open(dst, O_WRONLY|O_CREAT|O_EXCL, nmode);
443 | if (dfd == -1) {
444 | close(sfd);
445 | return -1;
446 | }
447 |
448 | struct stat newst;
449 | if (fstat(dfd, &newst) < 0) {
450 | close(dfd);
451 | return -1;
452 | }
453 | add_file_hash(&newst);
454 |
455 | // copy the file
456 | char cbuf[4096] = {0};
457 | while (1) {
458 | ssize_t r, w;
459 | r = read(sfd, cbuf, 4096);
460 | if (!r) {
461 | break;
462 | }
463 |
464 | if (r < 0) {
465 | s = -1;
466 | break;
467 | }
468 |
469 | w = write(dfd, cbuf, r);
470 | if (w < r) {
471 | s = -1;
472 | break;
473 | }
474 | }
475 |
476 | if (close(dfd) < 0) {
477 | return -1;
478 | }
479 | close(sfd);
480 |
481 | if (!S_ISREG(srcstat.st_mode)) {
482 | return s;
483 | }
484 |
485 | goto preserve;
486 | }
487 |
488 | notreg:
489 | {
490 | // source is not a regular file (it's a symlink or special)
491 | if (S_ISLNK(srcstat.st_mode)) {
492 | char lbuf[PATH_MAX+1] = {0};
493 | ssize_t ls = readlink(src, lbuf, PATH_MAX);
494 | if (ls == -1) {
495 | return -1;
496 | }
497 | lbuf[ls] = '\0';
498 |
499 | int r = symlink(lbuf, dst);
500 | if (r != 0) {
501 | return -1;
502 | }
503 |
504 | // can't preserve stuff for symlinks
505 | return 0;
506 | } else if (S_ISBLK(srcstat.st_mode) || S_ISCHR(srcstat.st_mode)
507 | || S_ISSOCK(srcstat.st_mode) || S_ISFIFO(srcstat.st_mode)) {
508 | if (mknod(dst, srcstat.st_mode, srcstat.st_rdev) < 0) {
509 | return -1;
510 | }
511 | } else {
512 | return -1;
513 | }
514 | }
515 |
516 | preserve:
517 | {
518 | // preserve mode, owner, attributes, etc. here
519 | struct timeval t[2];
520 | t[1].tv_sec = t[0].tv_sec = srcstat.st_mtime;
521 | t[1].tv_usec = t[0].tv_usec = 0;
522 |
523 | // we will fail silently if any of these don't work
524 | utimes(dst, t);
525 | (void)chown(dst, srcstat.st_uid, srcstat.st_gid);
526 | chmod(dst, srcstat.st_mode);
527 |
528 | return s;
529 | }
530 | }
531 |
532 | /*
533 | * Copies a file or directory. Returns 0 on success and -1 on failure.
534 | */
535 | static int cpfile(const char* src, const char* dst) {
536 | int s = cpfile_inner(src, dst);
537 | reset_file_table();
538 | return s;
539 | }
540 |
541 | static struct deletedfile* newdeleted(bool mass) {
542 | struct deletedfile* d = malloc(sizeof(struct deletedfile));
543 | if (!d) {
544 | return NULL;
545 | }
546 |
547 | d->original = malloc(NAME_MAX);
548 | if (!d->original) {
549 | free(d);
550 | return NULL;
551 | }
552 |
553 | d->id = del_id++;
554 | d->mass = mass;
555 | if (d->mass) {
556 | d->massid = mdel_id;
557 | }
558 | d->prev = NULL;
559 |
560 | return d;
561 | }
562 |
563 | static struct deletedfile* freedeleted(struct deletedfile* f) {
564 | struct deletedfile* d = f->prev;
565 | free(f->original);
566 | free(f);
567 | return d;
568 | }
569 |
570 | static int strnatcmp(const char *s1, const char *s2) {
571 | for (;;) {
572 | if (*s2 == '\0') {
573 | return *s1 != '\0';
574 | }
575 |
576 | if (*s1 == '\0') {
577 | return 1;
578 | }
579 |
580 | if (!(isdigit(*s1) && isdigit(*s2))) {
581 | if (toupper(*s1) != toupper(*s2)) {
582 | return toupper(*s1) - toupper(*s2);
583 | }
584 | ++s1;
585 | ++s2;
586 | } else {
587 | char *lim1;
588 | char *lim2;
589 | unsigned long n1 = strtoul(s1, &lim1, 10);
590 | unsigned long n2 = strtoul(s2, &lim2, 10);
591 | if (n1 > n2) {
592 | return 1;
593 | } else if (n1 < n2) {
594 | return -1;
595 | }
596 | s1 = lim1;
597 | s2 = lim2;
598 | }
599 | }
600 | }
601 |
602 | /*
603 | * Comparison function for list elements for qsort.
604 | */
605 | static int elemcmp(const void* a, const void* b) {
606 | const struct listelem* x = a;
607 | const struct listelem* y = b;
608 |
609 | if ((x->type == ELEM_DIR || x->type == ELEM_DIRLINK) &&
610 | (y->type != ELEM_DIR && y->type != ELEM_DIRLINK)) {
611 | return -1;
612 | }
613 |
614 | if ((y->type == ELEM_DIR || y->type == ELEM_DIRLINK) &&
615 | (x->type != ELEM_DIR && x->type != ELEM_DIRLINK)) {
616 | return 1;
617 | }
618 |
619 | return strnatcmp(x->name, y->name);
620 | }
621 |
622 | /*
623 | * Get editor.
624 | */
625 | static void geteditor(void) {
626 | #ifdef EDITOR
627 | strncpy(editor, EDITOR, PATH_MAX);
628 | #else
629 | const char* res = getenv("CFM_EDITOR");
630 | if (!res) {
631 | res = getenv("EDITOR");
632 | if (!res) {
633 | editor[0] = '\0';
634 | } else {
635 | strncpy(editor, res, PATH_MAX);
636 | }
637 | } else {
638 | strncpy(editor, res, PATH_MAX);
639 | }
640 | #endif /* EDITOR */
641 | }
642 |
643 | /*
644 | * Get shell.
645 | */
646 | static void getshell(void) {
647 | #ifdef SHELL
648 | strncpy(shell, SHELL, PATH_MAX);
649 | #else
650 | const char* res = getenv("CFM_SHELL");
651 | if (!res) {
652 | res = getenv("SHELL");
653 | if (!res) {
654 | shell[0] = '\0';
655 | } else {
656 | strncpy(shell, res, PATH_MAX);
657 | }
658 | } else {
659 | strncpy(shell, res, PATH_MAX);
660 | }
661 | #endif /* SHELL */
662 | }
663 |
664 | /*
665 | * Get the opener program.
666 | */
667 | static void getopener(void) {
668 | #ifdef OPENER
669 | strncpy(opener, OPENER, PATH_MAX);
670 | #else
671 | const char* res = getenv("CFM_OPENER");
672 | if (!res) {
673 | res = getenv("OPENER");
674 | if (!res) {
675 | opener[0] = '\0';
676 | } else {
677 | strncpy(opener, res, PATH_MAX);
678 | }
679 | } else {
680 | strncpy(opener, res, PATH_MAX);
681 | }
682 | #endif
683 | }
684 |
685 | /*
686 | * Get the tmp directory.
687 | */
688 | static void maketmpdir(void) {
689 | #ifdef TMP_DIR
690 | strncpy(tmpdir, TMP_DIR, PATH_MAX);
691 | #else
692 | const char* res = getenv("CFM_TMP");
693 | if (res) {
694 | strncpy(tmpdir, res, PATH_MAX);
695 | } else {
696 | strncpy(tmpdir, "/tmp/cfmtmp", PATH_MAX);
697 | }
698 | #endif
699 | if (mkdir(tmpdir, 0751)) {
700 | if (errno == EEXIST) {
701 | return;
702 | } else {
703 | tmpdir[0] = '\0';
704 | }
705 | }
706 | }
707 |
708 | static void rmtmp(void) {
709 | if (tmpdir[0]) {
710 | if (0 != deldir(tmpdir)) {
711 | perror("rmtmp: deldir");
712 | }
713 | }
714 | }
715 |
716 | /*
717 | * Write current working directory to CD_ON_CLOSE file.
718 | * Creates the file if it doesn't exist; does not report errors.
719 | */
720 | static void cdonclose(const char* wd) {
721 | // since we assume that rmpwdfile() has already been called before this,
722 | // cdonclosefile should have a valid path already if it's set
723 | if (*cdonclosefile) {
724 | FILE* outfile = fopen(cdonclosefile, "w");
725 | if (outfile) {
726 | fprintf(outfile, "%s\n", wd);
727 | fclose(outfile);
728 | }
729 | }
730 | }
731 |
732 | /*
733 | * Remove existing pwd file (CD_ON_CLOSE).
734 | * Does not report errors.
735 | */
736 | static void rmpwdfile(void) {
737 | #ifdef CD_ON_CLOSE
738 | if (NULL == realpath(CD_ON_CLOSE, cdonclosefile)) {
739 | *cdonclosefile = 0;
740 | }
741 | #else
742 | const char* cdocf = getenv("CFM_CD_ON_CLOSE");
743 | if (cdocf) {
744 | strncpy(cdonclosefile, cdocf, PATH_MAX);
745 | }
746 | #endif
747 | if (*cdonclosefile) {
748 | del(cdonclosefile);
749 | }
750 | }
751 |
752 | /*
753 | * Save the default terminal settings.
754 | * Returns 0 on success.
755 | */
756 | static int backupterm(void) {
757 | if (!interactive) return 0;
758 | if (tcgetattr(STDIN_FILENO, &old_term) < 0) {
759 | perror("tcgetattr");
760 | return 1;
761 | }
762 | return 0;
763 | }
764 |
765 | /*
766 | * Get the size of the terminal.
767 | * Returns 0 on success.
768 | */
769 | static int termsize(void) {
770 | if (!interactive) return 0;
771 | struct winsize ws;
772 | if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) {
773 | perror("ioctl");
774 | return 1;
775 | }
776 |
777 | rows = ws.ws_row;
778 | cols = ws.ws_col;
779 |
780 | return 0;
781 | }
782 |
783 | /*
784 | * Sets up the terminal for TUI.
785 | * Return 0 on success.
786 | */
787 | static int setupterm(void) {
788 | if (!interactive) return 0;
789 |
790 | setvbuf(stdout, NULL, _IOFBF, 0);
791 |
792 | struct termios new_term = old_term;
793 | new_term.c_oflag &= ~OPOST;
794 | new_term.c_lflag &= ~(ECHO | ICANON);
795 |
796 | if (tcsetattr(STDIN_FILENO, TCSANOW, &new_term) < 0) {
797 | perror("tcsetattr");
798 | return 1;
799 | }
800 |
801 | printf(
802 | "\033[?1049h" // use alternative screen buffer
803 | "\033[?7l" // disable line wrapping
804 | "\033[?25l" // hide cursor
805 | "\033[2J" // clear screen
806 | "\033[2;%dr", // limit scrolling to our rows
807 | rows-1);
808 |
809 | return 0;
810 | }
811 |
812 | /*
813 | * Resets the terminal to how it was before we ruined it.
814 | */
815 | static void resetterm(void) {
816 | if (!interactive) return;
817 |
818 | setvbuf(stdout, NULL, _IOLBF, 0);
819 |
820 | if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term) < 0) {
821 | perror("tcsetattr");
822 | return;
823 | }
824 |
825 | printf(
826 | "\033[?7h" // enable line wrapping
827 | "\033[?25h" // unhide cursor
828 | "\033[r" // reset scroll region
829 | "\033[?1049l" // restore main screen
830 | );
831 |
832 | fflush(stdout);
833 | }
834 |
835 | /*
836 | * Creates a child process.
837 | */
838 | static void execcmd(const char* path, const char* cmd, const char* arg) {
839 | pid_t pid = fork();
840 | if (pid < 0) {
841 | return;
842 | }
843 |
844 | resetterm();
845 |
846 | if (pid == 0) {
847 | if (chdir(path) < 0) {
848 | _exit(EXIT_FAILURE);
849 | }
850 | execlp(cmd, cmd, arg, NULL);
851 | _exit(EXIT_FAILURE);
852 | } else {
853 | int s;
854 | do {
855 | waitpid(pid, &s, WUNTRACED);
856 | } while (!WIFEXITED(s) && !WIFSIGNALED(s));
857 | }
858 |
859 | setupterm();
860 | fflush(stdout);
861 | }
862 |
863 | /*
864 | * Reads a directory into the list, returning the number of items in the dir.
865 | * This will return 0 on success.
866 | * On failure, 'opendir' will have set 'errno'.
867 | */
868 | static int listdir(const char* path, struct listelem** list, size_t* listsize, size_t* rcount, bool hidden) {
869 | DIR* d;
870 | struct dirent* dir;
871 | d = opendir(path);
872 | size_t count = 0;
873 | struct stat st;
874 | if (d) {
875 | int dfd = dirfd(d);
876 | while ((dir = readdir(d)) != NULL) {
877 | if (dir->d_name[0] == '.' && (dir->d_name[1] == '\0' || (dir->d_name[1] == '.' && dir->d_name[2] == '\0'))) {
878 | continue;
879 | }
880 |
881 | if (!hidden && dir->d_name[0] == '.') {
882 | continue;
883 | }
884 |
885 | if (count == *listsize) {
886 | *listsize += LIST_ALLOC_SIZE;
887 | *list = realloc(*list, *listsize * sizeof(**list));
888 | if (*list == NULL) {
889 | perror("realloc");
890 | exit(EXIT_FAILURE);
891 | }
892 | }
893 |
894 | strncpy((*list)[count].name, dir->d_name, NAME_MAX);
895 |
896 | (*list)[count].marked = false;
897 |
898 | if (0 != fstatat(dfd, dir->d_name, &st, AT_SYMLINK_NOFOLLOW)) {
899 | continue;
900 | }
901 |
902 | if (S_ISDIR(st.st_mode)) {
903 | (*list)[count].type = ELEM_DIR;
904 | } else if (S_ISLNK(st.st_mode)) {
905 | if (0 == fstatat(dfd, dir->d_name, &st, 0)) {
906 | if (S_ISDIR(st.st_mode)) {
907 | (*list)[count].type = ELEM_DIRLINK;
908 | } else {
909 | (*list)[count].type = ELEM_LINK;
910 | }
911 | } else {
912 | (*list)[count].type = ELEM_LINK;
913 | }
914 | } else {
915 | if (st.st_mode & S_IXUSR) {
916 | (*list)[count].type = ELEM_EXEC;
917 | } else {
918 | (*list)[count].type = ELEM_FILE;
919 | }
920 | }
921 |
922 | count++;
923 | }
924 |
925 | closedir(d);
926 | qsort(*list, count, sizeof(**list), elemcmp);
927 | } else {
928 | return -1;
929 | }
930 |
931 | *rcount = count;
932 | return 0;
933 | }
934 |
935 | /*
936 | * Get a filename from the user and store it in 'out'.
937 | * out must point to a buffer capable of containing
938 | * at least NAME_MAX bytes.
939 | *
940 | * initialstr is the text to write into the file before
941 | * the user edits it.
942 | *
943 | * Returns 0 on success, else:
944 | * -1 = no editor
945 | * -2 = other error (check errno)
946 | * -3 = invalid filename entered
947 | *
948 | * If an error occurs but the data in 'out'
949 | * is still usable, it will be there. Else, out will
950 | * be empty.
951 | */
952 | static int readfname(char* out, const char* initialstr) {
953 | if (editor[0]) {
954 | char template[] = "/tmp/cfmtmp.XXXXXXXXXX";
955 | int fd;
956 | if (-1 == (fd = mkstemp(template))) {
957 | return -2;
958 | }
959 |
960 | int rval = 0;
961 |
962 | if (initialstr) {
963 | if (-1 == write(fd, initialstr, strlen(initialstr))) {
964 | rval = -2;
965 | } else {
966 | if (-1 == lseek(fd, 0, SEEK_SET)) {
967 | rval = -2;
968 | }
969 | }
970 | }
971 |
972 | if (rval == 0) {
973 | execcmd("/tmp/", editor, template);
974 |
975 | ssize_t c;
976 | memset(out, 0, NAME_MAX);
977 | if (-1 == (c = read(fd, out, NAME_MAX - 1))) {
978 | rval = -2;
979 | out[0] = '\0';
980 | } else {
981 | if (out[0] == '\0') {
982 | rval = -3;
983 | }
984 | char* nl = strchr(out, '\n');
985 | if (nl != NULL) {
986 | *nl = '\0';
987 | }
988 |
989 | rval = 0;
990 |
991 | // validate the string
992 | // only allow POSIX portable paths and spaces if enabled
993 | // which is to say A-Za-z0-9._-
994 | for (char* x = out; *x; x++) {
995 | if (!(isalnum(*x) || *x == '.' || *x == '_' || *x == '-'
996 | #if ALLOW_SPACES
997 | || *x == ' '
998 | #endif
999 | )) {
1000 | rval = -3;
1001 | out[0] = '\0';
1002 | break;
1003 | }
1004 | }
1005 | }
1006 | }
1007 |
1008 | unlink(template);
1009 | close(fd);
1010 | return rval;
1011 | } else {
1012 | return -1;
1013 | }
1014 | }
1015 |
1016 | /*
1017 | * Get a key. Wraps getchar() and returns hjkl instead of arrow keys.
1018 | * Also, returns
1019 | */
1020 | static int getkey(void) {
1021 | char c[6];
1022 |
1023 | if (!interactive) {
1024 | ssize_t n = read(STDIN_FILENO, c, 1);
1025 | if (n == 0) {
1026 | return 'q';
1027 | }
1028 | return *c;
1029 | }
1030 |
1031 | ssize_t n = read(STDIN_FILENO, c, 6);
1032 | if (n <= 0) {
1033 | return -1;
1034 | }
1035 |
1036 | if (n == 2 && c[0] == '\033' && isalpha(c[1])) {
1037 | return K_ALT(c[1]);
1038 | }
1039 |
1040 | if (n < 3) {
1041 | return c[0];
1042 | }
1043 |
1044 | if (n == 3) {
1045 | switch (c[2]) {
1046 | case ESC_UP:
1047 | return 'k';
1048 | case ESC_DOWN:
1049 | return 'j';
1050 | case ESC_RIGHT:
1051 | return 'l';
1052 | case ESC_LEFT:
1053 | return 'h';
1054 | }
1055 | } else if (n == 4) {
1056 | if (!strncmp(c+2, "5~", 2)) {
1057 | return KEY_PGUP;
1058 | }
1059 | if (!strncmp(c+2, "6~", 2)) {
1060 | return KEY_PGDN;
1061 | }
1062 | } else if (n == 6) {
1063 | // shift-up
1064 | if (!strncmp(c+2, "1;2A", 4)) {
1065 | return KEY_PGUP;
1066 | }
1067 | // shift-down
1068 | if (!strncmp(c+2, "1;2B", 4)) {
1069 | return KEY_PGDN;
1070 | }
1071 | }
1072 |
1073 | return -1;
1074 | }
1075 |
1076 | /*
1077 | * Draws one element to the screen.
1078 | */
1079 | static void drawentry(struct listelem* e, bool selected) {
1080 | if (!interactive) return;
1081 | printf("\033[2K"); // clear line
1082 |
1083 | #if BOLD_POINTER
1084 | # define PBOLD printf("\033[1m")
1085 | #else
1086 | # define PBOLD
1087 | #endif
1088 | if (e->marked) {
1089 | printf("\033[35m");
1090 | PBOLD;
1091 | } else {
1092 | switch (e->type) {
1093 | case ELEM_EXEC:
1094 | printf("\033[33m");
1095 | PBOLD;
1096 | break;
1097 | case ELEM_DIR:
1098 | printf("\033[32m");
1099 | PBOLD;
1100 | break;
1101 | case ELEM_DIRLINK:
1102 | printf("\033[36m");
1103 | PBOLD;
1104 | break;
1105 | case ELEM_LINK:
1106 | printf("\033[36m");
1107 | break;
1108 | case ELEM_FILE:
1109 | default:
1110 | printf("\033[37m");
1111 | break;
1112 | }
1113 | }
1114 | #undef PBOLD
1115 |
1116 | #if INVERT_SELECTION && INVERT_FULL_SELECTION
1117 | if (selected) {
1118 | printf("\033[7m");
1119 | }
1120 | #endif
1121 |
1122 | #if INDENT_SELECTION
1123 | if (selected) {
1124 | printf("%s", POINTER);
1125 | }
1126 | #else
1127 | printf("%-*s", pointerwidth, selected ? POINTER : "");
1128 | #endif
1129 |
1130 | #if !BOLD_POINTER
1131 | if (e->marked) {
1132 | printf("\033[1m");
1133 | if (e->type == ELEM_EXEC
1134 | || e->type == ELEM_DIR
1135 | || e->type == ELEM_DIRLINK) {
1136 | printf("\033[1m");
1137 | }
1138 | }
1139 | #endif
1140 |
1141 | #if INVERT_SELECTION
1142 | if (selected) {
1143 | # if INVERT_FULL_SELECTION
1144 | printf(" %s%-*s", e->name, cols, E_DIR(e->type) ? "/" : "");
1145 | # else
1146 | printf(" \033[7m%s%s", e->name, E_DIR(e->type) ? "/" : "");
1147 | # endif
1148 | } else {
1149 | printf(" %s", e->name);
1150 | if (E_DIR(e->type)) {
1151 | printf("/");
1152 | }
1153 | }
1154 | #else
1155 | printf(" %s", e->name);
1156 | if (E_DIR(e->type)) {
1157 | printf("/");
1158 | }
1159 | #endif
1160 |
1161 | if (e->marked && !selected) {
1162 | printf("\r%c", MARK_SYMBOL);
1163 | }
1164 |
1165 | printf("\r\033[m"); // cursor to column 1
1166 | }
1167 |
1168 | /*
1169 | * Draws the status line at the bottom of the screen.
1170 | */
1171 | static void drawstatusline(struct listelem* l, size_t n, size_t s, size_t m, size_t p) {
1172 | if (!interactive) return;
1173 | printf("\033[%d;H" // go to the bottom row
1174 | //"\033[2K" // clear the row
1175 | "\033[37;7;1m", // inverse + bold
1176 | rows);
1177 |
1178 | int count;
1179 | if (!m) {
1180 | count = printf(" %zu/%zu", n ? s+1 : n, n);
1181 | } else {
1182 | count = printf(" %zu/%zu (%zu marked)", n ? s+1 : n, n, m);
1183 | }
1184 | // print the type of the file
1185 | printf("%*s \r", cols-count-1, elemtypestrings[l->type]);
1186 | printf("\033[m\n\033[%zu;H", p+2); // move cursor back and reset formatting
1187 | }
1188 |
1189 | /*
1190 | * Draws the statusline with an error message in it.
1191 | */
1192 | static void drawstatuslineerror(const char* prefix, const char* error, size_t p) {
1193 | if (!interactive) {
1194 | // instead print to stderr
1195 | fprintf(stderr, "%s: %s\n", prefix, error);
1196 | exit(EXIT_FAILURE);
1197 | return;
1198 | }
1199 |
1200 | printf("\033[%d;H"
1201 | //"\033[2K"
1202 | "\033[31;7;1m",
1203 | rows);
1204 | int count = printf(" %s: ", prefix);
1205 | printf("%-*s \r", cols-count-1, error);
1206 | printf("\033[m\033[%zu;H", p+2);
1207 | }
1208 |
1209 | /*
1210 | * Draws the whole screen (redraw).
1211 | * Use sparingly.
1212 | */
1213 | static void drawscreen(char* wd, struct listelem* l, size_t n, size_t s, size_t o, size_t m, int v) {
1214 | if (!interactive) return;
1215 |
1216 | // clear the screen except for the top and bottom lines
1217 | // this gets rid of the flashing when redrawing
1218 | for (int i = 2; i < rows; i++) {
1219 | printf("\033[%dH" // row i
1220 | "\033[m"
1221 | "\033[K", i); // clear row
1222 | }
1223 |
1224 | // go to the top and print the info bar
1225 | printf("\033[H" // top left
1226 | "\033[37;7;1m"); // style
1227 |
1228 | int count;
1229 | #if VIEW_COUNT > 1
1230 | count = printf(" %d: %s", v+1, wd);
1231 | #else
1232 | (void)v;
1233 | count = printf(" %s", wd);
1234 | #endif
1235 |
1236 | printf("%-*s", (int)(cols - count), (wd[1] == '\0') ? "" : "/");
1237 |
1238 | printf("\033[m"); // reset formatting
1239 |
1240 | for (size_t i = s - o; i < n && (int)(i - (s - o)) < rows - 2; i++) {
1241 | printf("\r\n");
1242 | drawentry(&(l[i]), (bool)(i == s));
1243 | }
1244 |
1245 | drawstatusline(&(l[s]), n, s, m, o);
1246 | }
1247 |
1248 | /*
1249 | * Writes back the parent directory of a path.
1250 | * Returns 1 if the path was changed, 0 if not (i.e. if
1251 | * the path was "/").
1252 | */
1253 | static int parentdir(char* path) {
1254 | char* last = strrchr(path, '/');
1255 | if (last == path && path[1] == '\0') {
1256 | return 0;
1257 | }
1258 |
1259 | if (last == path) {
1260 | path[1] = '\0';
1261 | } else {
1262 | *last = '\0';
1263 | }
1264 |
1265 | return 1;
1266 | }
1267 |
1268 | /*
1269 | * Returns a pointer to a string with the working directory after replacing
1270 | * a leading $HOME with ~, or the original wd if none was found.
1271 | */
1272 | static char* homesubstwd(char* wd, char* home, size_t homelen) {
1273 | #if ABBREVIATE_HOME
1274 | static char subbedpwd[PATH_MAX+1] = {0};
1275 | if (wd && !strncmp(wd, home, homelen)) {
1276 | snprintf(subbedpwd, PATH_MAX+1, "~%s", wd + homelen);
1277 | return subbedpwd;
1278 | }
1279 | #else
1280 | (void)home;
1281 | (void)homelen;
1282 | #endif
1283 | return wd;
1284 | }
1285 |
1286 | /*
1287 | * Signal handler for SIGINT/SIGTERM.
1288 | */
1289 | static void sigdie(int UNUSED(sig)) {
1290 | exit(EXIT_SUCCESS);
1291 | }
1292 |
1293 | /*
1294 | * Signal handler for window resize.
1295 | */
1296 | static void sigresize(int UNUSED(sig)) {
1297 | resize = true;
1298 | redraw = true;
1299 | }
1300 |
1301 | /*
1302 | * Signal handler for SIGTSTP, which is ^Z from the shell.
1303 | */
1304 | static void sigtstp(int UNUSED(sig)) {
1305 | resetterm();
1306 | kill(getpid(), SIGSTOP);
1307 | }
1308 |
1309 | /*
1310 | * Signal handler for SIGCONT which is used when using fg in the shell.
1311 | */
1312 | static void sigcont(int UNUSED(sig)) {
1313 | backupterm();
1314 | setupterm();
1315 | resize = true;
1316 | redraw = true;
1317 | }
1318 |
1319 | int main(int argc, char** argv) {
1320 | if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
1321 | interactive = false;
1322 | }
1323 |
1324 | char* wd = malloc(PATH_MAX);
1325 | memset(wd, 0, PATH_MAX);
1326 |
1327 | if (argc == 1) {
1328 | getrealcwd(wd, PATH_MAX);
1329 | } else {
1330 | if (NULL == realpath(argv[1], wd)) {
1331 | exit(EXIT_FAILURE);
1332 | }
1333 | }
1334 |
1335 | pointerwidth = strlen(POINTER);
1336 |
1337 | geteditor();
1338 | getshell();
1339 | getopener();
1340 | maketmpdir();
1341 | rmpwdfile();
1342 |
1343 | char* userhome = getenv("HOME");
1344 | size_t homelen = strlen(userhome);
1345 |
1346 | if (termsize()) {
1347 | exit(EXIT_FAILURE);
1348 | }
1349 |
1350 | struct sigaction sa_resize = {
1351 | .sa_handler = sigresize,
1352 | };
1353 | if (sigaction(SIGWINCH, &sa_resize, NULL) < 0) {
1354 | perror("sigaction");
1355 | exit(EXIT_FAILURE);
1356 | }
1357 |
1358 | struct sigaction sa_ded = {
1359 | .sa_handler = sigdie,
1360 | };
1361 | if (sigaction(SIGTERM, &sa_ded, NULL) < 0) {
1362 | perror("sigaction");
1363 | exit(EXIT_FAILURE);
1364 | }
1365 | if (sigaction(SIGINT, &sa_ded, NULL) < 0) {
1366 | perror("sigaction");
1367 | exit(EXIT_FAILURE);
1368 | }
1369 |
1370 | struct sigaction sa_tstp = {
1371 | .sa_handler = sigtstp,
1372 | };
1373 | if (sigaction(SIGTSTP, &sa_tstp, NULL) < 0) {
1374 | perror("sigaction");
1375 | exit(EXIT_FAILURE);
1376 | }
1377 |
1378 | struct sigaction sa_cont = {
1379 | .sa_handler = sigcont,
1380 | };
1381 | if (sigaction(SIGCONT, &sa_cont, NULL) < 0) {
1382 | perror("sigaction");
1383 | exit(EXIT_FAILURE);
1384 | }
1385 |
1386 | if (backupterm()) {
1387 | exit(EXIT_FAILURE);
1388 | }
1389 |
1390 | if (setupterm()) {
1391 | exit(EXIT_FAILURE);
1392 | }
1393 |
1394 | if (tmpdir[0]) {
1395 | atexit(rmtmp);
1396 | }
1397 | atexit(resetterm);
1398 |
1399 | size_t listsize = LIST_ALLOC_SIZE;
1400 | struct listelem* list = malloc(LIST_ALLOC_SIZE * sizeof(struct listelem));
1401 | if (!list) {
1402 | perror("malloc");
1403 | exit(EXIT_FAILURE);
1404 | }
1405 |
1406 | bool update = true;
1407 | bool showhidden = false;
1408 | size_t newdcount = 0;
1409 | size_t dcount = 0;
1410 |
1411 | struct deletedfile* delstack = NULL;
1412 |
1413 | struct view {
1414 | char* wd;
1415 | const char* eprefix;
1416 | const char* emsg;
1417 | bool errorshown;
1418 | size_t selection;
1419 | size_t pos;
1420 | size_t marks;
1421 | struct savedpos* backstack;
1422 | } views[VIEW_COUNT];
1423 |
1424 | for (int i = 0; i < VIEW_COUNT; i++) {
1425 | views[i] = (struct view){ NULL, NULL, NULL, false, 0, 0, 0, NULL };
1426 | }
1427 |
1428 | for (int i = 0; i < VIEW_COUNT; i++) {
1429 | views[i].wd = malloc(PATH_MAX+1);
1430 | if (!views[i].wd) {
1431 | perror("malloc");
1432 | exit(EXIT_FAILURE);
1433 | }
1434 | strncpy(views[i].wd, wd, PATH_MAX);
1435 | }
1436 |
1437 | free(wd);
1438 |
1439 | // selected view
1440 | int _view = 0;
1441 | struct view* view = views;
1442 |
1443 | if (!tmpdir[0]) {
1444 | view->errorshown = true;
1445 | view->eprefix = "Warning";
1446 | view->emsg = "Trash dir not available";
1447 | }
1448 |
1449 | int k = -1, pk = -1, status;
1450 | char tmpbuf[PATH_MAX+1] = {0};
1451 | char tmpbuf2[PATH_MAX+1] = {0};
1452 | char tmpnam[NAME_MAX+1] = {0};
1453 | char lastname[NAME_MAX+1] = {0};
1454 | char yankbuf[PATH_MAX+1] = {0};
1455 | char cutbuf[NAME_MAX+1] = {0};
1456 | bool hasyanked = false;
1457 | bool hascut = false;
1458 | int cutid = -1;
1459 | while (1&&1) {
1460 | if (update) {
1461 | update = false;
1462 | status = listdir(view->wd, &list, &listsize, &newdcount, showhidden);
1463 | if (0 != status) {
1464 | parentdir(view->wd);
1465 | view->errorshown = true;
1466 | view->eprefix = "Error";
1467 | view->emsg = strerror(errno);
1468 | if (view->backstack) {
1469 | view->pos = view->backstack->pos;
1470 | view->selection = view->backstack->sel;
1471 | struct savedpos* s = view->backstack;
1472 | view->backstack = s->prev;
1473 | free(s);
1474 | }
1475 | update = true;
1476 | continue;
1477 | }
1478 | if (!newdcount) {
1479 | view->pos = 0;
1480 | view->selection = 0;
1481 | } else {
1482 | // lock to bottom if deleted file at top
1483 | if (newdcount < dcount) {
1484 | if (view->pos == 0 && view->selection > 0) {
1485 | if (dcount - view->selection == (size_t)rows - 2) {
1486 | view->selection--;
1487 | }
1488 | }
1489 | }
1490 | while (view->selection >= newdcount) {
1491 | if (view->selection) {
1492 | view->selection--;
1493 | if (view->pos) {
1494 | view->pos--;
1495 | }
1496 | }
1497 | }
1498 | if (view->pos == 0 && view->selection == 0 && lastname[0]) {
1499 | for (size_t i = 0; i < newdcount; i++) {
1500 | if (0 == strcmp(lastname, list[i].name)) {
1501 | view->selection = i;
1502 | view->pos = (i > (size_t)rows - 2) ? (size_t)rows/2 : i;
1503 | break;
1504 | }
1505 | }
1506 | lastname[0] = 0;
1507 | }
1508 | }
1509 | dcount = newdcount;
1510 | redraw = true;
1511 | }
1512 |
1513 | if (redraw && interactive) {
1514 | redraw = false;
1515 | // only get the current terminal size if we resized
1516 | if (resize) {
1517 | if (termsize()) {
1518 | exit(EXIT_FAILURE);
1519 | }
1520 |
1521 | // set new scroll region
1522 | printf("\033[2;%dr", rows-1);
1523 |
1524 | // if our current item is outside the bounds of the screen, we need to move it up
1525 | if (view->pos >= (size_t)rows - 2) {
1526 | view->pos = rows - 3;
1527 | }
1528 |
1529 | // TODO: if the bottom of the screen is visible (or would become visible),
1530 | // we should pin it to the bottom and now allow blank space to show up at
1531 | // the bottom of the screen
1532 | // this may require us to store the old rows value before calling termsize()
1533 |
1534 | resize = false;
1535 | }
1536 | drawscreen(homesubstwd(view->wd, userhome, homelen), list, dcount, view->selection, view->pos, view->marks, _view);
1537 | if (view->errorshown) {
1538 | drawstatuslineerror(view->eprefix, view->emsg, view->pos);
1539 | }
1540 | printf("\033[%zu;1H", view->pos+2);
1541 | fflush(stdout);
1542 | }
1543 |
1544 | k = getkey();
1545 | switch(k) {
1546 | case 'h':
1547 | {
1548 | char* bn = basename(view->wd);
1549 | strncpy(lastname, bn, NAME_MAX);
1550 | if (parentdir(view->wd)) {
1551 | view->errorshown = false;
1552 | if (view->backstack) {
1553 | view->pos = view->backstack->pos;
1554 | view->selection = view->backstack->sel;
1555 | struct savedpos* s = view->backstack;
1556 | view->backstack = s->prev;
1557 | free(s);
1558 | } else {
1559 | view->pos = 0;
1560 | view->selection = 0;
1561 | }
1562 | update = true;
1563 | }
1564 | }
1565 | break;
1566 | case '\033':
1567 | if (view->marks > 0) {
1568 | view->marks = 0;
1569 | update = true;
1570 | break;
1571 | } // fallthrough
1572 | case 'q':
1573 | exit(EXIT_SUCCESS);
1574 | break;
1575 | case 'Q':
1576 | cdonclose(view->wd);
1577 | exit(EXIT_SUCCESS);
1578 | break;
1579 | case '.':
1580 | showhidden = !showhidden;
1581 | view->selection = 0;
1582 | view->pos = 0;
1583 | update = true;
1584 | break;
1585 | case 'r':
1586 | update = true;
1587 | break;
1588 | case 'S':
1589 | if (shell[0]) {
1590 | execcmd(view->wd, shell, NULL);
1591 | update = true;
1592 | }
1593 | break;
1594 | #if VIEW_COUNT > 1
1595 | case '0':
1596 | k = '9' + 1;
1597 | case '1':
1598 | case '2':
1599 | case '3':
1600 | case '4':
1601 | case '5':
1602 | case '6':
1603 | case '7':
1604 | case '8':
1605 | case '9':
1606 | if (k - '1' < VIEW_COUNT) {
1607 | _view = k - '1';
1608 | view = &(views[_view]);
1609 | update = true;
1610 | }
1611 | break;
1612 | case '`':
1613 | _view--;
1614 | if (_view < 0) {
1615 | _view = VIEW_COUNT - 1;
1616 | }
1617 | view = &(views[_view]);
1618 | update = true;
1619 | break;
1620 | case '\t':
1621 | _view = (_view + 1) % VIEW_COUNT;
1622 | view = &(views[_view]);
1623 | update = true;
1624 | break;
1625 | #endif
1626 | case 'u':
1627 | if (tmpdir[0] && delstack != NULL) {
1628 | int did = 0;
1629 | do {
1630 | did = delstack->massid;
1631 | snprintf(tmpbuf, PATH_MAX, "%s/%d", tmpdir, delstack->id);
1632 | if (0 != cpfile(tmpbuf, delstack->original)) {
1633 | view->eprefix = "Error undoing";
1634 | view->emsg = strerror(errno);
1635 | view->errorshown = true;
1636 | } else {
1637 | // fail silently here
1638 | unlink(tmpbuf);
1639 | delstack = freedeleted(delstack);
1640 | }
1641 | } while (delstack && delstack->mass && delstack->massid == did);
1642 | update = 1;
1643 | }
1644 | break;
1645 | case 'T':
1646 | status = readfname(tmpnam, "new file name");
1647 | switch (status) {
1648 | case -1:
1649 | view->eprefix = "Error";
1650 | view->emsg = "No editor available";
1651 | view->errorshown = true;
1652 | break;
1653 | case -2:
1654 | if (tmpnam[0] == '\0') {
1655 | view->eprefix = "Error";
1656 | view->emsg = strerror(errno);
1657 | view->errorshown = true;
1658 | } else {
1659 | view->eprefix = "Warning";
1660 | view->emsg = strerror(errno);
1661 | view->errorshown = true;
1662 | status = 0;
1663 | }
1664 | break;
1665 | case -3:
1666 | view->eprefix = "Error";
1667 | view->emsg = "Invalid file name";
1668 | view->errorshown = true;
1669 | break;
1670 | }
1671 |
1672 | if (status == 0) {
1673 | snprintf(tmpbuf, PATH_MAX, "%s/%s", view->wd, tmpnam);
1674 | // use fopen instead of testing with 'exists' here because
1675 | // this way the file will be created if the file doesn't
1676 | // exist
1677 | FILE* f = fopen(tmpbuf, "wx");
1678 | if (!f) {
1679 | if (errno == EEXIST) {
1680 | view->eprefix = "Error";
1681 | view->emsg = "File already exists";
1682 | view->errorshown = true;
1683 | } else {
1684 | view->eprefix = "Error";
1685 | view->emsg = strerror(errno);
1686 | view->errorshown = true;
1687 | }
1688 | } else {
1689 | fclose(f);
1690 | strncpy(lastname, tmpnam, NAME_MAX);
1691 | view->pos = 0;
1692 | view->selection = 0;
1693 | }
1694 | }
1695 | update = true;
1696 | break;
1697 | case 'M':
1698 | status = readfname(tmpnam, "new directory name");
1699 | switch (status) {
1700 | case -1:
1701 | view->eprefix = "Error";
1702 | view->emsg = "No editor available";
1703 | view->errorshown = true;
1704 | break;
1705 | case -2:
1706 | if (tmpnam[0] == '\0') {
1707 | view->eprefix = "Error";
1708 | view->emsg = strerror(errno);
1709 | view->errorshown = true;
1710 | } else {
1711 | view->eprefix = "Warning";
1712 | view->emsg = strerror(errno);
1713 | view->errorshown = true;
1714 | status = 0;
1715 | }
1716 | break;
1717 | case -3:
1718 | view->eprefix = "Error";
1719 | view->emsg = "Invalid directory name";
1720 | view->errorshown = true;
1721 | break;
1722 | }
1723 | snprintf(tmpbuf, PATH_MAX, "%s/%s", view->wd, tmpnam);
1724 | if (-1 == mkdir(tmpbuf, 0751)) {
1725 | if (errno == EEXIST) {
1726 | view->eprefix = "Error";
1727 | view->emsg = "Directory already exists";
1728 | view->errorshown = true;
1729 | } else {
1730 | view->eprefix = "Error";
1731 | view->emsg = strerror(errno);
1732 | view->errorshown = true;
1733 | }
1734 | }
1735 | update = true;
1736 | break;
1737 | case 'p':
1738 | if (hasyanked) {
1739 | strncpy(tmpbuf, yankbuf, PATH_MAX);
1740 | snprintf(tmpbuf2, PATH_MAX, "%s/%s", view->wd, basename(yankbuf));
1741 | } else if (hascut) {
1742 | snprintf(tmpbuf, PATH_MAX, "%s/%d", tmpdir, cutid);
1743 | snprintf(tmpbuf2, PATH_MAX, "%s/%s", view->wd, cutbuf);
1744 | }
1745 | bool didpaste = true;
1746 | do {
1747 | if (!didpaste) {
1748 | if (hasyanked) {
1749 | status = readfname(tmpnam, basename(yankbuf));
1750 | } else if (hascut) {
1751 | status = readfname(tmpnam, cutbuf);
1752 | }
1753 | switch (status) {
1754 | case -1:
1755 | view->eprefix = "Error";
1756 | view->emsg = "No editor available";
1757 | view->errorshown = true;
1758 | break;
1759 | case -2:
1760 | if (tmpnam[0] == '\0') {
1761 | view->eprefix = "Error";
1762 | view->emsg = strerror(errno);
1763 | view->errorshown = true;
1764 | } else {
1765 | view->eprefix = "Warning";
1766 | view->emsg = strerror(errno);
1767 | view->errorshown = true;
1768 | status = 0;
1769 | }
1770 | break;
1771 | case -3:
1772 | view->eprefix = "Error";
1773 | view->emsg = "Invalid file name";
1774 | view->errorshown = true;
1775 | break;
1776 | }
1777 |
1778 | if (status == 0) {
1779 | snprintf(tmpbuf2, PATH_MAX, "%s/%s", view->wd, tmpnam);
1780 | } else {
1781 | goto outofloop;
1782 | }
1783 | }
1784 | int s = exists(tmpbuf2);
1785 | if (s == 0) {
1786 | if (0 != cpfile(tmpbuf, tmpbuf2)) {
1787 | view->eprefix = "Error";
1788 | view->emsg = "Could not copy files";
1789 | view->errorshown = true;
1790 | }
1791 | didpaste = true;
1792 | } else if (s == -1) {
1793 | view->eprefix = "Error";
1794 | view->emsg = "Could not stat target file";
1795 | view->errorshown = true;
1796 | goto outofloop;
1797 | } else if (s == 1) {
1798 | didpaste = false;
1799 | }
1800 | } while (!didpaste);
1801 | outofloop:
1802 | update = true;
1803 | break;
1804 | }
1805 |
1806 | if (!dcount) {
1807 | pk = k;
1808 | fflush(stdout);
1809 | continue;
1810 | }
1811 |
1812 | switch (k) {
1813 | case 'j':
1814 | if (view->selection < dcount - 1) {
1815 | view->errorshown = false;
1816 | drawentry(&(list[view->selection]), false);
1817 | view->selection++;
1818 | printf("\n");
1819 | drawentry(&(list[view->selection]), true);
1820 | if (view->pos < (size_t)rows - 3) {
1821 | view->pos++;
1822 | }
1823 | drawstatusline(&(list[view->selection]), dcount, view->selection, view->marks, view->pos);
1824 | }
1825 | break;
1826 | case 'k':
1827 | if (view->selection > 0) {
1828 | view->errorshown = false;
1829 | drawentry(&(list[view->selection]), false);
1830 | view->selection--;
1831 | if (view->pos > 0) {
1832 | view->pos--;
1833 | printf("\r\033[A");
1834 | } else {
1835 | printf("\r\033[L");
1836 | }
1837 | drawentry(&(list[view->selection]), true);
1838 | drawstatusline(&(list[view->selection]), dcount, view->selection, view->marks, view->pos);
1839 | }
1840 | break;
1841 | case KEY_PGDN:
1842 | // don't do anything if we are too low to page down
1843 | if ((size_t)rows - 2 + view->selection - view->pos < dcount) {
1844 | // 1. move the view down so the last item is now the top item
1845 | // 2. select that one
1846 | // 3. if we are within view of the bottom
1847 | view->selection += (size_t)rows - 2 - view->pos - 1;
1848 | view->pos = 0;
1849 | view->errorshown = false;
1850 | redraw = true;
1851 | }
1852 | break;
1853 | case KEY_PGUP:
1854 | // do nothing if we are in the top "page"
1855 | if (view->pos != view->selection) {
1856 | // 1. move view up so that the top item is now the last item
1857 | // 2. select that one
1858 | // 3. if we are within view of the top, don't go up
1859 | if ((size_t)rows - 2 > view->selection - view->pos) {
1860 | view->selection = rows - 3;
1861 | } else {
1862 | view->selection -= view->pos;
1863 | }
1864 | view->pos = rows - 3;
1865 | view->errorshown = false;
1866 | redraw = true;
1867 | }
1868 | break;
1869 | case 'g':
1870 | if (pk != 'g') {
1871 | break;
1872 | }
1873 | view->errorshown = false;
1874 | if (view->pos != view->selection) {
1875 | view->pos = 0;
1876 | view->selection = 0;
1877 | redraw = true;
1878 | } else {
1879 | drawentry(&(list[view->selection]), false);
1880 | view->pos = 0;
1881 | view->selection = 0;
1882 | printf("\033[%zu;1H", view->pos+2);
1883 | drawentry(&(list[view->selection]), true);
1884 | drawstatusline(&(list[view->selection]), dcount, view->selection, view->marks, view->pos);
1885 | }
1886 | break;
1887 | case 'G':
1888 | view->selection = dcount - 1;
1889 | if (dcount > (size_t)rows - 2) {
1890 | view->pos = rows - 3;
1891 | } else {
1892 | view->pos = view->selection;
1893 | }
1894 | view->errorshown = false;
1895 | redraw = true;
1896 | break;
1897 | #ifndef ENTER_OPEN
1898 | case '\n':
1899 | #endif
1900 | case 'l':
1901 | if (E_DIR(list[view->selection].type)) {
1902 | struct savedpos* sp = malloc(sizeof(struct savedpos));
1903 | sp->pos = view->pos;
1904 | sp->sel = view->selection;
1905 | sp->prev = view->backstack;
1906 | view->backstack = sp;
1907 | if (view->wd[1] != '\0') {
1908 | strcat(view->wd, "/");
1909 | }
1910 | strncat(view->wd, list[view->selection].name, PATH_MAX - strlen(view->wd) - 2);
1911 | view->selection = 0;
1912 | view->pos = 0;
1913 | update = true;
1914 | } else {
1915 | if (editor[0]) {
1916 | execcmd(view->wd, editor, list[view->selection].name);
1917 | update = true;
1918 | }
1919 | }
1920 | view->errorshown = false;
1921 | break;
1922 | #ifdef ENTER_OPEN
1923 | case '\n':
1924 | #endif
1925 | case 'o':
1926 | if (E_DIR(list[view->selection].type)) {
1927 | struct savedpos* sp = malloc(sizeof(struct savedpos));
1928 | sp->pos = view->pos;
1929 | sp->sel = view->selection;
1930 | sp->prev = view->backstack;
1931 | view->backstack = sp;
1932 | if (view->wd[1] != '\0') {
1933 | strcat(view->wd, "/");
1934 | }
1935 | strncat(view->wd, list[view->selection].name, PATH_MAX - strlen(view->wd) - 2);
1936 | view->selection = 0;
1937 | view->pos = 0;
1938 | update = true;
1939 | break;
1940 | } else if (opener[0]) {
1941 | if (opener[0]) {
1942 | execcmd(view->wd, opener, list[view->selection].name);
1943 | update = true;
1944 | }
1945 | }
1946 | view->errorshown = false;
1947 | break;
1948 | case K_ALT('d'):
1949 | case 'd':
1950 | if (pk != k) {
1951 | break;
1952 | }
1953 | if (interactive && k == 'd' && tmpdir[0]) {
1954 | if (NULL == delstack) {
1955 | delstack = newdeleted(false);
1956 | } else {
1957 | struct deletedfile* d = newdeleted(false);
1958 | d->prev = delstack;
1959 | delstack = d;
1960 | }
1961 |
1962 | snprintf(delstack->original, PATH_MAX, "%s/%s", view->wd, list[view->selection].name);
1963 |
1964 | snprintf(tmpbuf, PATH_MAX, "%s/%d", tmpdir, delstack->id);
1965 | if (0 != cpfile(delstack->original, tmpbuf)) {
1966 | view->eprefix = "Error deleting";
1967 | view->emsg = strerror(errno);
1968 | view->errorshown = true;
1969 | delstack = freedeleted(delstack);
1970 | } else {
1971 | if (0 != del(delstack->original)) {
1972 | view->eprefix = "Error deleting";
1973 | view->emsg = strerror(errno);
1974 | view->errorshown = true;
1975 | delstack = freedeleted(delstack);
1976 | } else {
1977 | if (list[view->selection].marked) {
1978 | view->marks--;
1979 | }
1980 | }
1981 | }
1982 | } else {
1983 | snprintf(tmpbuf, PATH_MAX, "%s/%s", view->wd, list[view->selection].name);
1984 | if (0 != del(tmpbuf)) {
1985 | view->eprefix = "Error deleting";
1986 | view->emsg = strerror(errno);
1987 | view->errorshown = true;
1988 | } else {
1989 | if (list[view->selection].marked) {
1990 | view->marks--;
1991 | }
1992 | }
1993 | }
1994 | update = true;
1995 | break;
1996 | case 'D':
1997 | if (!view->marks) {
1998 | break;
1999 | }
2000 | for (size_t i = 0; i < dcount; i++) {
2001 | if (list[i].marked) {
2002 | if (tmpdir[0]) {
2003 | if (NULL == delstack) {
2004 | delstack = newdeleted(true);
2005 | } else {
2006 | struct deletedfile* d = newdeleted(true);
2007 | d->prev = delstack;
2008 | delstack = d;
2009 | }
2010 |
2011 | snprintf(delstack->original, PATH_MAX, "%s/%s", view->wd, list[i].name);
2012 |
2013 | snprintf(tmpbuf, PATH_MAX, "%s/%d", tmpdir, delstack->id);
2014 | if (0 != cpfile(delstack->original, tmpbuf)) {
2015 | view->eprefix = "Error copying for deletion";
2016 | view->emsg = strerror(errno);
2017 | view->errorshown = true;
2018 | delstack = freedeleted(delstack);
2019 | } else {
2020 | if (0 != del(delstack->original)) {
2021 | view->eprefix = "Error deleting";
2022 | view->emsg = strerror(errno);
2023 | view->errorshown = true;
2024 | delstack = freedeleted(delstack);
2025 | } else {
2026 | view->marks--;
2027 | }
2028 | }
2029 | } else {
2030 | snprintf(tmpbuf, PATH_MAX, "%s/%s", view->wd, list[i].name);
2031 | if (0 != del(tmpbuf)) {
2032 | view->eprefix = "Error deleting";
2033 | view->emsg = strerror(errno);
2034 | view->errorshown = true;
2035 | } else {
2036 | view->marks--;
2037 | }
2038 | }
2039 | }
2040 | }
2041 | if (tmpdir[0]) {
2042 | mdel_id++;
2043 | }
2044 | update = true;
2045 | break;
2046 | case 'e':
2047 | if (editor[0]) {
2048 | execcmd(view->wd, editor, list[view->selection].name);
2049 | update = true;
2050 | }
2051 | break;
2052 | case ' ':
2053 | case 'm':
2054 | list[view->selection].marked = !(list[view->selection].marked);
2055 | if (list[view->selection].marked) {
2056 | view->marks++;
2057 | } else {
2058 | view->marks--;
2059 | }
2060 | drawstatusline(&(list[view->selection]), dcount, view->selection, view->marks, view->pos);
2061 | drawentry(&(list[view->selection]), true);
2062 | break;
2063 | case 'R':
2064 | status = readfname(tmpnam, list[view->selection].name);
2065 | switch (status) {
2066 | case -1:
2067 | view->eprefix = "Error";
2068 | view->emsg = "No editor available";
2069 | view->errorshown = true;
2070 | break;
2071 | case -2:
2072 | if (tmpnam[0] == '\0') {
2073 | view->eprefix = "Error";
2074 | view->emsg = strerror(errno);
2075 | view->errorshown = true;
2076 | } else {
2077 | view->eprefix = "Warning";
2078 | view->emsg = strerror(errno);
2079 | view->errorshown = true;
2080 | status = 0;
2081 | }
2082 | break;
2083 | case -3:
2084 | view->eprefix = "Error";
2085 | view->emsg = "Invalid file name";
2086 | view->errorshown = true;
2087 | break;
2088 | }
2089 |
2090 | if (status == 0) {
2091 | snprintf(tmpbuf, PATH_MAX, "%s/%s", view->wd, tmpnam);
2092 | int s = exists(tmpbuf);
2093 | if (s == 1) {
2094 | view->eprefix = "Error";
2095 | view->emsg = "Target file already exists";
2096 | view->errorshown = true;
2097 | } else if (s == 0) {
2098 | // the target file does not exist
2099 | snprintf(tmpbuf2, PATH_MAX, "%s/%s", view->wd, list[view->selection].name);
2100 | if (-1 == rename(tmpbuf2, tmpbuf)) {
2101 | view->eprefix = "Error";
2102 | view->emsg = strerror(errno);
2103 | view->errorshown = true;
2104 | } else {
2105 | // go find the new file and select it
2106 | strncpy(lastname, tmpnam, NAME_MAX);
2107 | view->pos = 0;
2108 | view->selection = 0;
2109 | }
2110 | } else if (s == -1) {
2111 | view->eprefix = "Error";
2112 | view->emsg = strerror(errno);
2113 | view->errorshown = true;
2114 | }
2115 | }
2116 | update = true;
2117 | break;
2118 | case 'y':
2119 | if (pk != 'y') {
2120 | break;
2121 | }
2122 | snprintf(yankbuf, PATH_MAX, "%s/%s", view->wd, list[view->selection].name);
2123 | hasyanked = true;
2124 | break;
2125 | case 'X':
2126 | if (tmpbuf[0]) {
2127 | snprintf(tmpbuf, PATH_MAX, "%s/%s", view->wd, list[view->selection].name);
2128 | snprintf(cutbuf, NAME_MAX, "%s", list[view->selection].name);
2129 | snprintf(tmpbuf2, PATH_MAX, "%s/%d", tmpdir, (cutid = del_id++));
2130 | if (0 != cpfile(tmpbuf, tmpbuf2)) {
2131 | view->eprefix = "Error";
2132 | view->emsg = strerror(errno);
2133 | view->errorshown = true;
2134 | } else {
2135 | if (0 != del(tmpbuf)) {
2136 | view->eprefix = "Error";
2137 | view->emsg = "Couldn't delete original file";
2138 | view->errorshown = true;
2139 | } else {
2140 | if (list[view->selection].marked) {
2141 | view->marks--;
2142 | }
2143 | }
2144 | hasyanked = false;
2145 | hascut = true;
2146 | }
2147 | } else {
2148 | view->eprefix = "Error";
2149 | view->emsg = "No tmp dir, cannot cut!";
2150 | view->errorshown = true;
2151 | }
2152 | update = true;
2153 | break;
2154 | case '~':
2155 | if (userhome) {
2156 | strncpy(view->wd, userhome, PATH_MAX);
2157 | view->pos = 0;
2158 | view->selection = 0;
2159 | update = true;
2160 | }
2161 | break;
2162 | case '/':
2163 | view->wd[0] = '/';
2164 | view->wd[1] = '\0';
2165 | view->pos = 0;
2166 | view->selection = 0;
2167 | update = true;
2168 | break;
2169 | }
2170 |
2171 | if (pk != k) {
2172 | pk = k;
2173 | } else {
2174 | pk = 0;
2175 | }
2176 | fflush(stdout);
2177 | }
2178 |
2179 | exit(EXIT_SUCCESS);
2180 | }
2181 |
--------------------------------------------------------------------------------
/config.def.h:
--------------------------------------------------------------------------------
1 | #ifndef CFM_CONFIG_H
2 | #define CFM_CONFIG_H
3 |
4 | /*
5 | * Uncomment an option to enable it.
6 | * Comment to disable.
7 | * Each is accompanied with an explanation.
8 | */
9 |
10 | /* EDITOR:
11 | * If not set, cfm will attempt to use the
12 | * $EDITOR environment variable when opening
13 | * files.
14 | *
15 | * Default: $CFM_EDITOR then $EDITOR
16 | * Value: string
17 | */
18 | //#define EDITOR "/bin/vi"
19 |
20 | /* SHELL:
21 | * If not set, cfm will attempt to use the $SHELL
22 | * environment variable to open shells in a given directory.
23 | * Else, the specified shell will be invoked.
24 | *
25 | * Default: $CFM_SHELL then $SHELL
26 | * Value: string
27 | */
28 | //#define SHELL "/bin/bash"
29 |
30 | /* OPENER:
31 | * If not set, cfm will attempt to use the $OPENER
32 | * environment variable to open files with an opener.
33 | * Otherwise, it will use the specified opener when
34 | * you use the 'o' key.
35 | *
36 | * Default: $CFM_OPENER then $OPENER
37 | * Value: string
38 | */
39 | //#define OPENER "xdg-open"
40 |
41 | /* TMP_DIR:
42 | * If not set, cfm will attempt to use $CFM_TMP as its
43 | * temporary file directory. If this does not exist,
44 | * /tmp/cfmtmp will be used instead. TMP_DIR must be
45 | * set to an absolute path. If you wish to disable temp
46 | * files (which disables cut and paste as well as undo),
47 | * you can set this to an empty string.
48 | *
49 | * Note that cfm will not allow you to mark or delete its
50 | * temp directory.
51 | *
52 | * Default: "/tmp/cfmtmp"
53 | * Value: string
54 | */
55 | //#define TMP_DIR "/tmp/cfmtmp"
56 |
57 | /* ENTER_OPENS:
58 | * If not set, using enter will be the same as using
59 | * l or right arrow on normal files, aka they will be
60 | * opened with EDITOR. Otherwise, pressing enter will open
61 | * a file with OPENER. No-op if OPENER not defined or NULL.
62 | *
63 | * Default: 0
64 | * Value: boolean (1 or 0)
65 | */
66 | //#define ENTER_OPENS 0
67 |
68 | /* POINTER:
69 | * If not set, cfm will use "->" in front of the selected
70 | * item. You can set a different string here, such as ">".
71 | *
72 | * Default: "->"
73 | * Value: string
74 | */
75 | //#define POINTER "->"
76 |
77 | /* BOLD_POINTER:
78 | * If set, cfm will make the pointer bold for files
79 | * who appear bold, such as directories and executables.
80 |
81 | * Default: 1
82 | * Value: boolean (1 or 0)
83 | */
84 | //#define BOLD_POINTER 1
85 |
86 | /* INVERT_SELECTION:
87 | * If set, cfm will reverse the foreground/background colors
88 | * of the currently selected line.
89 | *
90 | * Default: 1
91 | * Value: boolean (1 or 0)
92 | */
93 | //#define INVERT_SELECTION 1
94 |
95 | /* INVERT_FULL_SELECTION:
96 | * If set, cfm will invert the colors on the entire selected line. Otherwise,
97 | * the effect will only appear on the name of the file.
98 | * This has no effect if INVERT_SELECTION is disabled.
99 | *
100 | * Tip: This looks a little strange with the pointer. It's recommended that
101 | * you either set POINTER to "" or enable INDENT_SELECTION.
102 | *
103 | * Default: 1
104 | * Value: boolean (1 or 0)
105 | */
106 | //#define INVERT_FULL_SELECTION 1
107 |
108 | /* INDENT_SELECTION:
109 | * If not set, all lines will be indented enough to make room
110 | * for the pointer, regardless of being selected or not.
111 | * If set, only the selected line will be indented.
112 | *
113 | * Default: 1
114 | * Value: boolean (1 or 0)
115 | */
116 | //#define INDENT_SELECTION 0
117 |
118 | /* MARK_SYMBOL:
119 | * If not set, marked items will be prefixed with a '^' symbol.
120 | * Else, can be a single character which will be placed at the
121 | * start of the line for any non-selected, marked item.
122 | *
123 | * Default: '^'
124 | * Value: char ('c')
125 | */
126 | //#define MARK_SYMBOL '^'
127 |
128 | /* VIEW_COUNT:
129 | * If not set, cfm will allow for two views by default.
130 | * Else, it will use this number of views. The value
131 | * of VIEW_COUNT must be within the range 1-10.
132 | *
133 | * Default: 2
134 | * Value: integer (1-10)
135 | */
136 | //#define VIEW_COUNT 2
137 |
138 | /* ALLOW_SPACES:
139 | * If not set, cfm will allow spaces when creating new
140 | * files/directories. If set to 0, only POSIX
141 | * "fully portable filenames" will be allowed,
142 | * which includes the characters 0-9A-Za-z._-
143 | * When enabled (or unset), spaces will be added to
144 | * the allowed characters.
145 | *
146 | * Default: 1
147 | * Value: boolean (1 or 0)
148 | */
149 | //#define ALLOW_SPACES 1
150 |
151 | /* CD_ON_CLOSE:
152 | * If set to a file path, cfm can write its current working
153 | * directory to a file on closing with Q (as opposed to q).
154 | * If set to NULL, this feature will be disabled.
155 | *
156 | * If not set, cfm will attempt to use the file specified in
157 | * the $CFM_CD_ON_CLOSE environment variable.
158 | *
159 | * Default: $CFM_CD_ON_CLOSE
160 | * Value: String
161 | */
162 | //#define CD_ON_CLOSE "/tmp/cfmdir"
163 |
164 | /* ABREVIATE_HOME:
165 | * If set, replace a leading $HOME in the current working directory with ~. For
166 | * example, if the current directory is /home/cactus/git/cfm, ~/git/cfm will be
167 | * shown.
168 | *
169 | * Default: 1
170 | * Value: boolean (1 or 0)
171 | */
172 | //#define ABBREVIATE_HOME 1
173 |
174 | #endif
175 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willeccles/cfm/2da5f164e989087486b68f44b6cf340f98219cfb/screenshot.png
--------------------------------------------------------------------------------