├── .clang-format ├── .github └── workflows │ └── action.yaml ├── LICENSE ├── Makefile ├── README.md ├── config.def.h ├── config.mk ├── sfm.1 ├── sfm.c ├── sfm.h └── sfm.png /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: WebKit 2 | AccessModifierOffset: 8 3 | AlignConsecutiveAssignments: false 4 | AlignConsecutiveDeclarations: false 5 | AlignConsecutiveMacros: true 6 | AlignEscapedNewlines: Left 7 | AlignOperands: false 8 | AlignTrailingComments: true 9 | AllowAllParametersOfDeclarationOnNextLine: false 10 | AllowShortBlocksOnASingleLine: false 11 | AllowShortCaseLabelsOnASingleLine: false 12 | AllowShortFunctionsOnASingleLine: InlineOnly 13 | AllowShortIfStatementsOnASingleLine: false 14 | AllowShortLoopsOnASingleLine: false 15 | AlwaysBreakAfterReturnType: TopLevelDefinitions 16 | AlwaysBreakBeforeMultilineStrings: false 17 | AlwaysBreakTemplateDeclarations: MultiLine 18 | BinPackArguments: true 19 | BinPackParameters: true 20 | BreakBeforeBinaryOperators: None 21 | BreakBeforeBraces: WebKit 22 | BreakBeforeTernaryOperators: false 23 | BreakStringLiterals: false 24 | ColumnLimit: 80 25 | CompactNamespaces: true 26 | UseTab: ForContinuationAndIndentation 27 | ConstructorInitializerIndentWidth: 8 28 | ContinuationIndentWidth: 8 29 | DerivePointerAlignment: false 30 | DisableFormat: false 31 | IncludeBlocks: Regroup 32 | IndentCaseLabels: false 33 | IndentPPDirectives: BeforeHash 34 | IndentWidth: 8 35 | KeepEmptyLinesAtTheStartOfBlocks: true 36 | Language: Cpp 37 | MaxEmptyLinesToKeep: 1 38 | NamespaceIndentation: None 39 | PenaltyBreakBeforeFirstCallParameter: 1000 40 | PenaltyBreakComment: 100 41 | PointerAlignment: Right 42 | ReflowComments: false 43 | SortIncludes: true 44 | SpaceAfterCStyleCast: false 45 | TabWidth: 8 46 | 47 | IncludeCategories: 48 | - Regex: '^' 49 | Priority: -2 50 | SortPriority: -4 51 | - Regex: '^' 52 | Priority: -2 53 | SortPriority: -3 54 | - Regex: '^' 55 | Priority: -2 56 | SortPriority: -2 57 | - Regex: '^<.*>' 58 | Priority: -1 59 | SortPriority: -1 60 | - Regex: '^"sfm\.h"' 61 | Priority: 0 62 | SortPriority: 1 63 | - Regex: '^".*\.h"' 64 | Priority: 0 65 | SortPriority: 2 66 | 67 | -------------------------------------------------------------------------------- /.github/workflows/action.yaml: -------------------------------------------------------------------------------- 1 | name: Cross platform build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ubuntu_x86: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | - name: Print OS and architecture 18 | run: uname -a 19 | - name: Compile 20 | run: make 21 | 22 | macos_arm: 23 | runs-on: macos-latest 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | - name: Print OS and architecture 28 | run: uname -a 29 | - name: Compile 30 | run: make 31 | 32 | freebsd_x86: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | - name: FreeBSD setup and compile 38 | uses: vmactions/freebsd-vm@v1 39 | with: 40 | usesh: true 41 | prepare: | 42 | echo "FreeBSD environment prepared" 43 | run: | 44 | uname -a 45 | make 46 | 47 | openbsd_x86: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout code 51 | uses: actions/checkout@v4 52 | - name: OpenBSD setup and compile 53 | uses: vmactions/openbsd-vm@v1 54 | with: 55 | prepare: | 56 | echo "OpenBSD environment prepared" 57 | run: | 58 | uname -a 59 | make 60 | 61 | dragonflybsd_x86: 62 | runs-on: ubuntu-latest 63 | steps: 64 | - name: Checkout code 65 | uses: actions/checkout@v4 66 | - name: DragonFlyBSD setup and compile 67 | uses: vmactions/dragonflybsd-vm@v1 68 | with: 69 | usesh: true 70 | prepare: | 71 | echo "DragonFlyBSD environment prepared" 72 | run: | 73 | uname -a 74 | make 75 | 76 | netbsd_x86: 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Checkout code 80 | uses: actions/checkout@v4 81 | - name: NetBSD setup and compile 82 | uses: vmactions/netbsd-vm@v1 83 | with: 84 | prepare: | 85 | echo "NetBSD environment prepared" 86 | run: | 87 | uname -a 88 | make 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | © 2020-2022 Hassan Afify 4 | © 2020 Mohamed Afify 5 | © 2021 Nikolay Korotkiy 6 | © 2021 David Kalliecharan 7 | © 2021 Tdukv 8 | © 2022 Christoph Polcin 9 | 10 | Permission to use, copy, modify, and distribute this software for any 11 | purpose with or without fee is hereby granted, provided that the above 12 | copyright notice and this permission notice appear in all copies. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 15 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 16 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 17 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 18 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 19 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 20 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # sfm - simple file manager 2 | # See LICENSE file for copyright and license details. 3 | 4 | include config.mk 5 | 6 | BIN = sfm 7 | SRC = ${BIN}.c 8 | OBJ = ${SRC:.c=.o} 9 | 10 | all: options ${BIN} 11 | 12 | options: 13 | @echo ${BIN} build options: 14 | @echo "CFLAGS = ${CFLAGS}" 15 | @echo "LDFLAGS = ${LDFLAGS}" 16 | @echo "CC = ${CC}" 17 | 18 | .c.o: 19 | ${CC} -c ${CFLAGS} $< 20 | 21 | ${OBJ}: config.h config.mk 22 | 23 | config.h: 24 | cp config.def.h $@ 25 | 26 | ${BIN}: ${OBJ} 27 | ${CC} ${LDFLAGS} -o $@ ${OBJ} 28 | 29 | clean: 30 | rm -f ${BIN} ${OBJ} ${BIN}-${VERSION}.tar.gz 31 | 32 | dist: clean 33 | mkdir -p ${BIN}-${VERSION} 34 | cp -R LICENSE Makefile README.md config.def.h config.mk\ 35 | ${BIN}.1 ${BIN}.png ${SRC} ${BIN}-${VERSION} 36 | tar -cf ${BIN}-${VERSION}.tar ${BIN}-${VERSION} 37 | gzip ${BIN}-${VERSION}.tar 38 | rm -rf ${BIN}-${VERSION} 39 | 40 | install: ${BIN} 41 | mkdir -p ${DESTDIR}${PREFIX}/bin 42 | cp -f ${BIN} ${DESTDIR}${PREFIX}/bin 43 | chmod 755 ${DESTDIR}${PREFIX}/bin/${BIN} 44 | mkdir -p ${DESTDIR}${MANPREFIX}/man1 45 | sed "s/VERSION/${VERSION}/g" < ${BIN}.1 > ${DESTDIR}${MANPREFIX}/man1/${BIN}.1 46 | chmod 644 ${DESTDIR}${MANPREFIX}/man1/${BIN}.1 47 | 48 | uninstall: 49 | rm -f ${DESTDIR}${PREFIX}/bin/${BIN}\ 50 | ${DESTDIR}${MANPREFIX}/man1/${BIN}.1 51 | 52 | valgrind: $(BIN) 53 | @echo "Running valgrind..." 54 | valgrind \ 55 | --track-origins=yes \ 56 | --leak-check=full \ 57 | --show-leak-kinds=all \ 58 | --trace-children=yes \ 59 | --expensive-definedness-checks=yes \ 60 | --undef-value-errors=yes \ 61 | -s ./$(BIN) 62 | 63 | splint: $(SRC) 64 | @echo "Running splint..." 65 | splint $(SRC) 66 | 67 | cppcheck: 68 | @echo "Running cppcheck..." 69 | cppcheck \ 70 | --enable=all \ 71 | --inconclusive \ 72 | --std=c99 \ 73 | --language=c \ 74 | --suppress=missingIncludeSystem ./${SRC} 75 | 76 | clang-tidy: 77 | @echo "Running clang-tidy..." 78 | clang-tidy $(SRC) -- $(CFLAGS) 79 | 80 | security: valgrind cppcheck clang-tidy 81 | 82 | .PHONY: all options clean dist install uninstall 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sfm logo 2 | 3 | **simple file manager** 4 | 5 | [![Build status](https://ci.appveyor.com/api/projects/status/goq88ahjyvtjrui2?svg=true)](https://ci.appveyor.com/project/afify/sfm) 6 | [![CodeQL](https://github.com/afify/sfm/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/afify/sfm/actions/workflows/github-code-scanning/codeql) 7 | [![Cross platform build](https://github.com/afify/sfm/actions/workflows/action.yaml/badge.svg)](https://github.com/afify/sfm/actions/workflows/action.yaml) 8 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fafify%2Fsfm.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fafify%2Fsfm?ref=badge_shield) 9 | 10 | Description 11 | ------------ 12 | sfm is a simple file manager for unix-like systems. 13 | * BSD kqueue(2) - kernel event notification mechanism. 14 | * Linux inotify(7) - monitoring filesystem events. 15 | * pthreads(7) to read events, no timers. 16 | * dual pane. 17 | * bookmarks. 18 | * open files by extension. 19 | * bottom statusbar. 20 | * vim-like key bindings. 21 | * no dependencies. 22 | * search. 23 | * Inspired by [vifm](https://vifm.info/) and [noice](https://git.2f30.org/noice/). 24 | * Follows the suckless [philosophy](https://suckless.org/philosophy/). 25 | 26 | Patches 27 | ------- 28 | [sfm-patches](https://github.com/afify/sfm-patches) 29 | 30 | Performance 31 | ------------ 32 | ```sh 33 | $ perf stat -r 10 sfm 34 | ``` 35 | 36 | Options 37 | ------- 38 | ```sh 39 | $ sfm [-v] 40 | $ man sfm 41 | ``` 42 | sfm screenshot 43 | 44 | Installation 45 | ------------ 46 | 47 | Packaging status 48 | 49 | 50 | **current** 51 | ```sh 52 | git clone https://github.com/afify/sfm 53 | cd sfm/ 54 | make 55 | make install 56 | ``` 57 | **latest release** 58 | ```sh 59 | latest=$(curl -s https://api.github.com/repos/afify/sfm/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4) 60 | tgz="https://github.com/afify/sfm/archive/refs/tags/${latest}.tar.gz" 61 | curl -L -o "sfm-${latest}.tar.gz" "${tgz}" 62 | tar -xzf "sfm-${latest}.tar.gz" 63 | cd "sfm-${latest#v}" && \ 64 | make && make install || echo "Build failed!" 65 | ``` 66 | 67 | Run 68 | --- 69 | ```sh 70 | $ sfm 71 | ``` 72 | 73 | Configuration 74 | ------------- 75 | The configuration of sfm is done by creating a custom config.h 76 | and (re)compiling the source code. This keeps it fast, secure and simple. 77 | 78 | 79 | ## License 80 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fafify%2Fsfm.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fafify%2Fsfm?ref=badge_large) -------------------------------------------------------------------------------- /config.def.h: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #ifndef CONFIG_H 4 | #define CONFIG_H 5 | 6 | #include 7 | 8 | #include "sfm.h" 9 | 10 | /* color pairs fg bg attr*/ 11 | static const ColorPair color_blk = { 95, 0, NORM }; 12 | static const ColorPair color_brlnk = { 0, 9, NORM }; 13 | static const ColorPair color_chr = { 94, 0, NORM }; 14 | static const ColorPair color_dir = { 31, 0, NORM }; 15 | static const ColorPair color_exec = { 91, 0, NORM }; 16 | static const ColorPair color_sock = { 92, 0, NORM }; 17 | static const ColorPair color_ifo = { 93, 0, NORM }; 18 | static const ColorPair color_lnk = { 96, 0, NORM }; 19 | static const ColorPair color_other = { 90, 0, NORM }; 20 | static const ColorPair color_file = { 243, 0, NORM }; 21 | 22 | static const ColorPair color_frame = { 237, 0, NORM }; 23 | static const ColorPair color_panell = { 166, 233, BOLD }; 24 | static const ColorPair color_panelr = { 5, 233, BOLD }; 25 | static const ColorPair color_status = { 243, 0, NORM }; 26 | 27 | static const ColorPair color_search = { 15, 104, NORM }; 28 | static const ColorPair color_selected = { 21, 118, NORM }; 29 | 30 | static const ColorPair color_normal = { 33, 0, NORM }; 31 | static const ColorPair color_warn = { 220, 0, NORM }; 32 | static const ColorPair color_err = { 124, 0, BOLD }; 33 | 34 | 35 | /* commands */ 36 | #if defined(__linux__) 37 | #define CHFLAG "chattr" 38 | #else 39 | #define CHFLAG "chflags" 40 | #endif 41 | static const char *rm_cmd[] = { "rm", "-rf" }; /* delete */ 42 | static const char *cp_cmd[] = { "cp", "-r" }; /* copy */ 43 | static const char *chown_cmd[] = { "chown", "-R" }; /* change file owner and group */ 44 | static const char *chmod_cmd[] = { "chmod" }; /* change file mode bits */ 45 | static const char *chflags_cmd[] = { CHFLAG }; /* change file flags */ 46 | static const char *mv_cmd[] = { "mv" }; /* move */ 47 | static const char delconf[] = "yes"; 48 | 49 | static const size_t rm_cmd_len = LEN(rm_cmd); 50 | static const size_t cp_cmd_len = LEN(cp_cmd); 51 | static const size_t chown_cmd_len = LEN(chown_cmd); 52 | static const size_t chmod_cmd_len = LEN(chmod_cmd); 53 | static const size_t chflags_cmd_len = LEN(chflags_cmd); 54 | static const size_t mv_cmd_len = LEN(mv_cmd); 55 | static const size_t delconf_len = LEN(delconf); 56 | 57 | /* bookmarks */ 58 | static const char root[] = "/"; 59 | 60 | /* software */ 61 | static const char *mpv[] = { "mpv", "--fullscreen" }; 62 | static const char *sxiv[] = { "sxiv" }; 63 | static const char *mupdf[] = { "mupdf", "-I" }; 64 | static const char *libreoffice[] = { "libreoffice" }; 65 | static const char *gimp[] = { "gimp" }; 66 | static const char *r2[] = { "r2", "-c", "vv" }; 67 | 68 | /* extensions*/ 69 | static const char *images[] = { "bmp", "jpg", "jpeg", "png", "gif", "webp", "xpm" }; 70 | static const char *pdf[] = { "epub", "pdf" }; 71 | static const char *arts[] = { "xcf" }; 72 | static const char *obj[] = { "o", "a", "so" }; 73 | static const char *videos[] = { "avi", "flv", "wav", "webm", "wma", "wmv", 74 | "m2v", "m4a", "m4v", "mkv", "mov", "mp3", 75 | "mp4", "mpeg", "mpg" }; 76 | static const char *docs[] = { "odt", "doc", "docx", "xls", "xlsx", "odp", 77 | "ods", "pptx", "odg" }; 78 | 79 | static Rule rules[] = { 80 | RULE(videos, mpv, DontWait), 81 | RULE(images, sxiv, DontWait), 82 | RULE(pdf, mupdf, DontWait), 83 | RULE(docs, libreoffice, DontWait), 84 | RULE(arts, gimp, DontWait), 85 | RULE(obj, r2, Wait) 86 | }; 87 | 88 | /* normal keys */ 89 | static Key nkeys[] = { 90 | /* key function arg */ 91 | { 'j', move_cursor, { .i = +1 } }, 92 | { XK_DOWN, move_cursor, { .i = +1 } }, 93 | { 'k', move_cursor, { .i = -1 } }, 94 | { XK_UP, move_cursor, { .i = -1 } }, 95 | { XK_CTRL('u'), move_cursor, { .i = -5 } }, 96 | { XK_CTRL('d'), move_cursor, { .i = +5 } }, 97 | { '{', move_cursor, { .i = -10 } }, 98 | { '}', move_cursor, { .i = +10 } }, 99 | { 'l', open_entry, { 0 } }, 100 | { 'h', cd_to_parent, { 0 } }, 101 | { 'q', quit, { 0 } }, 102 | { 'G', move_bottom, { 0 } }, 103 | { 'g', move_top, { 0 } }, 104 | { XK_SPACE, switch_pane, { 0 } }, 105 | { '.', toggle_dotfiles, { 0 } }, 106 | { XK_CTRL('r'), refresh, { 0 } }, 107 | { XK_CTRL('f'), create_new_file, { 0 } }, 108 | { XK_CTRL('m'), create_new_dir, { 0 } }, 109 | { 'd', delete_entry, { 0 } }, 110 | { 'y', copy_entries, { 0 } }, 111 | { 'p', paste_entries, { 0 } }, 112 | { 'P', move_entries, { 0 } }, 113 | { 'v', visual_mode, { 0 } }, 114 | { XK_ESC, normal_mode, { 0 } }, 115 | { 's', select_cur_entry, { .i = InvertSelection } }, 116 | { 'x', select_all, { .i = DontSelect } }, 117 | { 'a', select_all, { .i = Select } }, 118 | { 'i', select_all, { .i = InvertSelection } }, 119 | { '/', start_search, { 0 } }, 120 | { 'n', move_to_match, { .i = NextMatch } }, 121 | { 'N', move_to_match, { .i = PrevMatch } }, 122 | }; 123 | 124 | static const size_t nkeyslen = LEN(nkeys); 125 | //static const size_t ckeyslen = LEN(ckeys); 126 | 127 | /* permissions */ 128 | static const mode_t new_dir_perm = S_IRWXU; 129 | static const mode_t new_file_perm = S_IRUSR | S_IWUSR; 130 | 131 | /* dotfiles */ 132 | static int show_dotfiles = 1; 133 | 134 | /* statusbar */ 135 | static const char dtfmt[] = "%F %R"; /* date time format */ 136 | 137 | #endif // CONFIG_H 138 | -------------------------------------------------------------------------------- /config.mk: -------------------------------------------------------------------------------- 1 | # sfm version 2 | VERSION = 0.5 3 | 4 | # paths 5 | PREFIX = /usr/local 6 | MANPREFIX = ${PREFIX}/share/man 7 | 8 | # flags 9 | CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -DVERSION=\"${VERSION}\" 10 | CFLAGS = -g3 -std=c99 -pedantic -Wextra -Wall -Wno-unused-parameter -Os ${CPPFLAGS} 11 | LDFLAGS = -pthread 12 | 13 | # compiler and linker 14 | CC = cc 15 | -------------------------------------------------------------------------------- /sfm.1: -------------------------------------------------------------------------------- 1 | .TH sfm 1 sfm\-VERSION 2 | .SH NAME 3 | sfm \- simple file manager 4 | .SH SYNOPSIS 5 | .B sfm 6 | .RB [ \-v ] 7 | .SH DESCRIPTION 8 | sfm is a simple file manager for unix-like systems. 9 | dual panes, bottom statusbar, bookmarks, open files by extention, vim-like key bindings as default configuration. cwd is left pane dir. 10 | .P 11 | .SH OPTIONS 12 | .TP 13 | .B \-v 14 | print version. 15 | .SH USAGE 16 | .SS Normal Mode 17 | .TP 18 | .B q 19 | quit 20 | .TP 21 | .B h 22 | back 23 | .TP 24 | .B j 25 | down 26 | .TP 27 | .B k 28 | up 29 | .TP 30 | .B l 31 | open dir | file 32 | .TP 33 | .B g 34 | top 35 | .TP 36 | .B G 37 | bottom 38 | .TP 39 | .B ctrl+u 40 | scroll up 41 | .TP 42 | .B ctrl+d 43 | scroll down 44 | .TP 45 | .B ctrl+f 46 | create new file 47 | .TP 48 | .B ctrl+m 49 | create new directory 50 | .TP 51 | .B d 52 | delete file | directory recursively 53 | .TP 54 | .B y 55 | yank 56 | .TP 57 | .B p 58 | paste 59 | .TP 60 | .B P 61 | move 62 | .TP 63 | .B . 64 | toggle dotfiles 65 | .TP 66 | .B v 67 | start visual mode 68 | .TP 69 | .B / 70 | start search 71 | .TP 72 | .B n 73 | next match 74 | .TP 75 | .B N 76 | previous match 77 | .TP 78 | .B SPACE 79 | switch pane 80 | .TP 81 | .B ctrl+r 82 | refresh panes 83 | .SS Visual Mode 84 | .TP 85 | .B j 86 | select down 87 | .TP 88 | .B k 89 | select up 90 | .TP 91 | .B d 92 | delete selection 93 | .TP 94 | .B v 95 | exit visual mode 96 | .TP 97 | .B q 98 | exit visual mode 99 | .TP 100 | .B ESC 101 | exit visual mode 102 | .SH CUSTOMIZATION 103 | sfm is customized by creating a custom 104 | .IR config.h 105 | and (re)compiling the source 106 | code. This keeps it fast, secure and simple. 107 | .SH ENVIRONMENT 108 | .TP 109 | .B EDITOR 110 | open unconfigured file extention. vi(1) if not set. 111 | .TP 112 | .B HOME 113 | right pane default directory. / if not set. 114 | .TP 115 | .B SHELL 116 | shell spawned with the 'b' key. /bin/sh if not set. 117 | .SH AUTHORS 118 | See the LICENSE file for the authors. 119 | .SH LICENSE 120 | See the LICENSE file for the terms of redistribution. 121 | -------------------------------------------------------------------------------- /sfm.c: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #if defined(__linux__) 4 | #define _GNU_SOURCE 5 | #include 6 | #include 7 | #define EV_BUF_LEN (1024 * (sizeof(struct inotify_event) + 16)) 8 | #define OFF_T "%ld" 9 | #define M_TIME st_mtim 10 | 11 | #elif defined(__APPLE__) 12 | #define _DARWIN_C_SOURCE 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #define OFF_T "%lld" 20 | #define M_TIME st_mtimespec 21 | 22 | #elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__) 23 | #define __BSD_VISIBLE 1 24 | #include 25 | #include 26 | #include 27 | 28 | #include 29 | #include 30 | #define OFF_T "%ld" 31 | #define M_TIME st_mtim 32 | 33 | #elif defined(__OpenBSD__) 34 | #include 35 | #include 36 | #include 37 | 38 | #include 39 | #define OFF_T "%lld" 40 | #define M_TIME st_mtim 41 | 42 | #endif 43 | 44 | #include 45 | #include 46 | #include 47 | 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | #include 62 | #include 63 | 64 | #include "sfm.h" 65 | #include "config.h" 66 | 67 | /* global variables */ 68 | static Terminal term; 69 | static Pane *current_pane; 70 | static Pane panes[2]; 71 | static int pane_idx; 72 | char *editor[2] = { "vi", NULL }; 73 | char *shell[2] = { "/bin/sh", NULL }; 74 | char *home = "/"; 75 | static pid_t fork_pid, main_pid; 76 | static char **selected_entries = NULL; 77 | static int selected_count = 0; 78 | static int mode; 79 | 80 | static void 81 | log_to_file(const char *func, int line, const char *format, ...) 82 | { 83 | pid_t pid = getpid(); 84 | FILE *logfile = fopen("/tmp/sfm.log", "a"); 85 | if (logfile) { 86 | va_list args; 87 | va_start(args, format); 88 | fprintf(logfile, "%d-- [%s:%d] ", pid, func, line); 89 | vfprintf(logfile, format, args); 90 | va_end(args); 91 | fprintf(logfile, "\n"); 92 | if (fclose(logfile) != 0) { 93 | fprintf(stderr, "Error closing log file\n"); 94 | } 95 | } else { 96 | fprintf(stderr, "Error opening log file\n"); 97 | } 98 | } 99 | 100 | static void 101 | init_term(void) 102 | { 103 | get_term_size(); 104 | term.buffer_size = (unsigned long)term.rows * term.cols * 4; 105 | term.buffer = ecalloc(term.buffer_size, sizeof(char)); 106 | term.buffer_left = term.buffer_size; 107 | term.buffer_index = 0; 108 | } 109 | 110 | static void 111 | enable_raw_mode(void) 112 | { 113 | tcgetattr(STDIN_FILENO, &term.orig); 114 | term.newterm = term.orig; 115 | term.newterm.c_lflag &= ~(ECHO | ICANON | ISIG); 116 | term.newterm.c_iflag &= ~(IXON | ICRNL); 117 | term.newterm.c_oflag &= ~(OPOST); 118 | term.newterm.c_cflag |= (CS8); 119 | tcsetattr(STDIN_FILENO, TCSAFLUSH, &term.newterm); 120 | if (write(STDOUT_FILENO, "\x1b[?1049h", 8) < 0) 121 | die("write:"); 122 | } 123 | 124 | static void 125 | get_term_size(void) 126 | { 127 | struct winsize ws; 128 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); 129 | term.rows = ws.ws_row; 130 | term.cols = ws.ws_col; 131 | } 132 | 133 | static void 134 | get_env(void) 135 | { 136 | char *env_editor = NULL; 137 | char *env_shell = NULL; 138 | char *env_home = NULL; 139 | 140 | env_editor = getenv("EDITOR"); 141 | if (env_editor != NULL) 142 | editor[0] = env_editor; 143 | 144 | env_shell = getenv("SHELL"); 145 | if (env_shell != NULL) 146 | shell[0] = env_shell; 147 | 148 | env_home = getenv("HOME"); 149 | if (env_home != NULL) 150 | home = env_home; 151 | } 152 | 153 | static int 154 | start_signal(void) 155 | { 156 | struct sigaction sa; 157 | main_pid = getpid(); 158 | sa.sa_handler = sighandler; 159 | sigemptyset(&sa.sa_mask); 160 | sa.sa_flags = SA_RESTART; 161 | sigaction(SIGUSR1, &sa, 0); 162 | sigaction(SIGUSR2, &sa, 0); 163 | sigaction(SIGWINCH, &sa, 0); 164 | return 0; 165 | } 166 | 167 | static void 168 | sighandler(int signo) 169 | { 170 | if (fork_pid > 0) /* while forking ignore signals */ 171 | return; 172 | 173 | switch (signo) { 174 | case SIGWINCH: 175 | log_to_file(__func__, __LINE__, "SIGWINCH"); 176 | termb_resize(); 177 | break; 178 | case SIGUSR1: 179 | log_to_file(__func__, __LINE__, "SIGUSR1"); 180 | set_pane_entries(&panes[Left]); 181 | update_screen(); 182 | break; 183 | case SIGUSR2: 184 | log_to_file(__func__, __LINE__, "SIGUSR2"); 185 | set_pane_entries(&panes[Right]); 186 | update_screen(); 187 | break; 188 | default: 189 | break; 190 | } 191 | } 192 | 193 | static void 194 | set_panes(void) 195 | { 196 | char cwd[PATH_MAX]; 197 | 198 | if ((getcwd(cwd, sizeof(cwd)) == NULL)) 199 | strncpy(cwd, home, PATH_MAX - 1); 200 | 201 | strncpy(panes[Left].path, cwd, PATH_MAX - 1); 202 | panes[Left].entries = NULL; 203 | panes[Left].entry_count = 0; 204 | panes[Left].start_index = 0; 205 | panes[Left].current_index = 0; 206 | panes[Left].watcher.fd = -1; 207 | panes[Left].watcher.signal = SIGUSR1; 208 | panes[Left].offset = 0; 209 | 210 | strncpy(panes[Right].path, home, PATH_MAX - 1); 211 | panes[Right].entries = NULL; 212 | panes[Right].entry_count = 0; 213 | panes[Right].start_index = 0; 214 | panes[Right].current_index = 0; 215 | panes[Right].watcher.fd = -1; 216 | panes[Right].watcher.signal = SIGUSR2; 217 | panes[Right].offset = term.cols / 2; 218 | 219 | pane_idx = Left; /* cursor pane */ 220 | current_pane = &panes[pane_idx]; 221 | 222 | set_pane_entries(&panes[Left]); 223 | set_pane_entries(&panes[Right]); 224 | } 225 | 226 | static void 227 | set_pane_entries(Pane *pane) 228 | { 229 | int fd, i; 230 | DIR *dir; 231 | char tmpfull[PATH_MAX]; 232 | const struct dirent *entry; 233 | struct stat status; 234 | 235 | if (pane->entries != NULL) { 236 | free(pane->entries); 237 | pane->entries = NULL; 238 | } 239 | 240 | fd = open(pane->path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); 241 | if (fd < 0) { 242 | print_status(color_err, strerror(errno)); 243 | return; 244 | } 245 | 246 | dir = fdopendir(fd); 247 | if (dir == NULL) { 248 | close(fd); 249 | print_status(color_err, strerror(errno)); 250 | return; 251 | } 252 | 253 | pane->entry_count = 0; 254 | while ((entry = readdir(dir)) != NULL) { 255 | if (should_skip_entry(entry) == 0) { 256 | pane->entry_count++; 257 | } 258 | } 259 | 260 | pane->entries = ecalloc(pane->entry_count, sizeof(Entry)); 261 | 262 | rewinddir(dir); 263 | 264 | i = 0; 265 | while ((entry = readdir(dir)) != NULL) { 266 | if (should_skip_entry(entry) == 1) { 267 | continue; 268 | } 269 | get_fullpath(tmpfull, pane->path, entry->d_name); 270 | 271 | if (i >= pane->entry_count) { 272 | log_to_file(__func__, __LINE__, 273 | "Entry count exceeded allocated memory"); 274 | break; 275 | } 276 | 277 | // file deleted while getting its details 278 | if (lstat(tmpfull, &status) != 0) { 279 | log_to_file(__func__, __LINE__, 280 | "lstat error for %s: %s", tmpfull, 281 | strerror(errno)); 282 | memset(&pane->entries[i], 0, sizeof(Entry)); 283 | strncpy(pane->entries[i].fullpath, tmpfull, 284 | PATH_MAX - 1); 285 | strncpy(pane->entries[i].name, entry->d_name, 286 | NAME_MAX - 1); 287 | i++; 288 | continue; 289 | } 290 | 291 | size_t fullpath_len = strlen(tmpfull); 292 | size_t name_len = strlen(entry->d_name); 293 | 294 | memcpy(pane->entries[i].fullpath, tmpfull, fullpath_len); 295 | pane->entries[i].fullpath[fullpath_len] = '\0'; 296 | 297 | memcpy(pane->entries[i].name, entry->d_name, name_len); 298 | pane->entries[i].name[name_len] = '\0'; 299 | 300 | pane->entries[i].st = status; 301 | 302 | set_entry_color(&pane->entries[pane->start_index + i]); 303 | 304 | i++; 305 | } 306 | 307 | pane->entry_count = i; 308 | 309 | if (closedir(dir) < 0) 310 | die("closedir:"); 311 | qsort(pane->entries, pane->entry_count, sizeof(Entry), entry_compare); 312 | } 313 | 314 | static int 315 | should_skip_entry(const struct dirent *entry) 316 | { 317 | if (entry->d_name[0] == '.') { 318 | if (entry->d_name[1] == '\0' || 319 | (entry->d_name[1] == '.' && entry->d_name[2] == '\0')) 320 | return 1; 321 | if (show_dotfiles != 1) 322 | return 1; 323 | } 324 | return 0; 325 | } 326 | 327 | static void 328 | get_fullpath(char *full_path, const char *first, const char *second) 329 | { 330 | int ret; 331 | 332 | if (first[0] == '/' && first[1] == '\0') 333 | (void)snprintf(full_path, PATH_MAX, "/%s", second); 334 | 335 | ret = snprintf(full_path, PATH_MAX, "%s/%s", first, second); 336 | if (ret < 0) 337 | die(strerror(errno)); 338 | if (ret >= PATH_MAX) 339 | die("Path exceeded maximum length"); 340 | } 341 | 342 | static int 343 | get_selected_paths(Pane *pane, char **result) 344 | { 345 | int count = 0; 346 | 347 | for (int i = 0; i < pane->entry_count; i++) { 348 | if (pane->entries[i].selected) { 349 | result[count] = pane->entries[i].fullpath; 350 | count++; 351 | } 352 | } 353 | 354 | return count; 355 | } 356 | 357 | // static int 358 | // entry_compare(const void *const A, const void *const B) 359 | // { 360 | // int result; 361 | // mode_t data1 = (*(Entry *)A).st.st_mode; 362 | // mode_t data2 = (*(Entry *)B).st.st_mode; 363 | // 364 | // if (data1 < data2) { 365 | // return -1; 366 | // } else if (data1 == data2) { 367 | // result = strncmp( 368 | // (*(Entry *)A).name, (*(Entry *)B).name, NAME_MAX); 369 | // return result; 370 | // } else { 371 | // return 1; 372 | // } 373 | // } 374 | 375 | static int 376 | entry_compare(const void *a, const void *b) 377 | { 378 | const Entry *entryA = (const Entry *)a; 379 | const Entry *entryB = (const Entry *)b; 380 | 381 | if (!entryA || !entryB) { 382 | return 0; 383 | } 384 | 385 | mode_t modeA = entryA->st.st_mode; 386 | mode_t modeB = entryB->st.st_mode; 387 | 388 | if (modeA < modeB) { 389 | return -1; 390 | } else if (modeA == modeB) { 391 | return strncmp(entryA->name, entryB->name, NAME_MAX); 392 | } else { 393 | return 1; 394 | } 395 | } 396 | 397 | static void 398 | update_screen(void) 399 | { 400 | // clear all except last line 401 | write(STDOUT_FILENO, "\x1b[F\x1b[A\x1b[999C\x1b[1J", 16); 402 | append_entries(&panes[Left]); 403 | append_entries(&panes[Right]); 404 | termb_write(); 405 | write_entries_name(); 406 | 407 | log_to_file(__func__, __LINE__, "err: (%d)", errno); 408 | if (mode == NormalMode && errno == 0) 409 | display_entry_details(); 410 | else 411 | print_status(color_err, strerror(errno)); 412 | } 413 | 414 | static void 415 | disable_raw_mode(void) 416 | { 417 | tcsetattr(STDIN_FILENO, TCSAFLUSH, &term.orig); 418 | if (write(STDOUT_FILENO, "\x1b[?1049l", 8) < 0) 419 | die("write:"); 420 | } 421 | 422 | static void 423 | append_entries(Pane *pane) 424 | { 425 | int i; 426 | int n = 0; 427 | size_t index = 0; 428 | size_t max_len = term.cols / 2; 429 | Entry entry; 430 | char *buffer; 431 | 432 | if (pane->entries == NULL) { 433 | return; 434 | } 435 | buffer = ecalloc(term.buffer_size, sizeof(char)); 436 | termb_append("\x1b[2;1f", 6); // move to top left 437 | 438 | for (i = 0; 439 | i < term.rows - 2 && pane->start_index + i < pane->entry_count; 440 | i++) { 441 | 442 | if (pane->start_index + i >= pane->entry_count || 443 | pane->entries == NULL) { 444 | continue; 445 | } 446 | 447 | entry = pane->entries[pane->start_index + i]; 448 | 449 | /* selected entry */ 450 | if (entry.selected == 1) 451 | entry.color = color_selected; 452 | 453 | /* current entry */ 454 | if (pane == current_pane && 455 | pane->start_index + i == pane->current_index) { 456 | entry.color.attr |= RVS; 457 | } 458 | 459 | // Format the entry with truncation and padding 460 | n = snprintf(buffer + index, term.buffer_size - index, 461 | "\x1b[%dG" 462 | "\x1b[%d;38;5;%d;48;5;%dm%-*.*s\x1b[0m\r\n", 463 | pane->offset, entry.color.attr, entry.color.fg, 464 | entry.color.bg, (int)max_len, (int)max_len, entry.name); 465 | if (n < 0) 466 | break; 467 | 468 | index += n; 469 | } 470 | 471 | termb_append(buffer, index); 472 | free(buffer); 473 | buffer = NULL; 474 | } 475 | 476 | static void 477 | handle_keypress(char c) 478 | { 479 | log_to_file(__func__, __LINE__, "key: (%c)", c); 480 | 481 | grabkeys(c, nkeys, nkeyslen); 482 | } 483 | 484 | static void 485 | grabkeys(uint32_t k, Key *key, size_t max_keys) 486 | { 487 | size_t i; 488 | for (i = 0; i < max_keys; i++) { 489 | if (k == key[i].k) { 490 | key[i].func(&key[i].arg); 491 | return; 492 | } 493 | } 494 | print_status(color_err, "No key binding found for key 0x%x", k); 495 | } 496 | 497 | static void 498 | print_status(ColorPair color, const char *fmt, ...) 499 | { 500 | char buf[term.cols]; 501 | int buf_len; 502 | size_t max_result_size; 503 | size_t result_len; 504 | va_list vl; 505 | 506 | va_start(vl, fmt); 507 | buf_len = vsnprintf(buf, term.cols, fmt, vl); 508 | va_end(vl); 509 | 510 | max_result_size = 5 + UINT16_LEN + 4 + 15 + UINT8_LEN + UINT8_LEN + 511 | UINT8_LEN + buf_len + 6 + 1; 512 | 513 | char result[max_result_size]; 514 | result_len = snprintf(result, max_result_size, 515 | "\x1b[%d;1f" // moves cursor to last line, column 1 516 | "\x1b[2K" // erase the entire line 517 | "\x1b[%d;38;5;%d;48;5;%dm" // set string colors 518 | "%s" 519 | "\x1b[0;0m", // reset colors 520 | term.rows, color.attr, color.fg, color.bg, buf); 521 | 522 | if (write(STDOUT_FILENO, result, result_len) < 0) 523 | die("write:"); 524 | } 525 | 526 | static void 527 | display_entry_details(void) 528 | { 529 | char sz[FSIZE_MAX]; 530 | char ur[USER_MAX]; 531 | char gr[GROUP_MAX]; 532 | char dt[DATETIME_MAX]; 533 | char prm[PERMISSION_MAX]; 534 | struct stat st; 535 | 536 | if (current_pane == NULL || current_pane->entries == NULL || 537 | current_pane->entry_count < 1) { 538 | print_status(color_warn, "Empty directory."); 539 | return; 540 | } 541 | 542 | if (current_pane->current_index >= current_pane->entry_count) { 543 | return; 544 | } 545 | 546 | st = current_pane->entries[current_pane->current_index].st; 547 | 548 | get_entry_permission(prm, st.st_mode); 549 | get_entry_owner(ur, st.st_uid); 550 | get_entry_group(gr, st.st_gid); 551 | get_entry_datetime(dt, st.M_TIME.tv_sec); 552 | get_file_size(sz, st.st_size); 553 | 554 | print_status(color_status, "%02d/%02d %s %s:%s %s %s", 555 | current_pane->current_index + 1, current_pane->entry_count, prm, 556 | ur, gr, dt, sz); 557 | } 558 | 559 | static void 560 | set_entry_color(Entry *ent) 561 | { 562 | if (ent->selected) { 563 | ent->color = color_selected; 564 | return; 565 | } else if (ent->matched) { 566 | ent->color = color_search; 567 | return; 568 | } 569 | 570 | switch (ent->st.st_mode & S_IFMT) { 571 | case S_IFREG: 572 | ent->color = color_file; 573 | if ((S_IXUSR | S_IXGRP | S_IXOTH) & ent->st.st_mode) 574 | ent->color = color_exec; 575 | break; 576 | case S_IFDIR: 577 | ent->color = color_dir; 578 | break; 579 | case S_IFLNK: 580 | ent->color = color_lnk; 581 | break; 582 | case S_IFBLK: 583 | ent->color = color_blk; 584 | break; 585 | case S_IFCHR: 586 | ent->color = color_chr; 587 | break; 588 | case S_IFIFO: 589 | ent->color = color_ifo; 590 | break; 591 | case S_IFSOCK: 592 | ent->color = color_sock; 593 | break; 594 | default: 595 | ent->color = color_other; 596 | break; 597 | } 598 | } 599 | 600 | static void 601 | get_entry_datetime(char *buf, time_t status) 602 | { 603 | struct tm lt; 604 | localtime_r(&status, <); 605 | strftime(buf, DATETIME_MAX, "%Y-%m-%d %H:%M", <); 606 | buf[DATETIME_MAX - 1] = '\0'; 607 | } 608 | 609 | static void 610 | get_entry_permission(char *buf, mode_t mode) 611 | { 612 | size_t i = 0; 613 | const char chars[] = "rwxrwxrwx"; 614 | 615 | if (S_ISDIR(mode)) 616 | buf[0] = 'd'; 617 | else if (S_ISREG(mode)) 618 | buf[0] = '-'; 619 | else if (S_ISLNK(mode)) 620 | buf[0] = 'l'; 621 | else if (S_ISBLK(mode)) 622 | buf[0] = 'b'; 623 | else if (S_ISCHR(mode)) 624 | buf[0] = 'c'; 625 | else if (S_ISFIFO(mode)) 626 | buf[0] = 'p'; 627 | else if (S_ISSOCK(mode)) 628 | buf[0] = 's'; 629 | else 630 | buf[0] = '?'; 631 | 632 | for (i = 1; i < PERMISSION_MAX; i++) { 633 | buf[i] = (mode & (1 << (9 - i))) ? chars[i - 1] : '-'; 634 | } 635 | buf[PERMISSION_MAX - 1] = '\0'; 636 | } 637 | 638 | static void 639 | get_file_size(char *buf, off_t size) 640 | { 641 | char unit; 642 | int counter = 0; 643 | 644 | while (size >= 1024) { 645 | size >>= 10; 646 | ++counter; 647 | } 648 | 649 | switch (counter) { 650 | case 0: 651 | unit = 'B'; 652 | break; 653 | case 1: 654 | unit = 'K'; 655 | break; 656 | case 2: 657 | unit = 'M'; 658 | break; 659 | case 3: 660 | unit = 'G'; 661 | break; 662 | case 4: 663 | unit = 'T'; 664 | break; 665 | default: 666 | unit = '?'; 667 | } 668 | 669 | if (snprintf(buf, FSIZE_MAX, OFF_T "%c", size, unit) < 0) 670 | print_status(color_err, strerror(errno)); 671 | } 672 | 673 | static void 674 | get_entry_owner(char *buf, const uid_t uid) 675 | { 676 | const struct passwd *pw; 677 | 678 | pw = getpwuid(uid); 679 | if (pw == NULL) { 680 | snprintf(buf, USER_MAX, "%u", uid); 681 | } else { 682 | strncpy(buf, pw->pw_name, USER_MAX - 1); 683 | buf[GROUP_MAX - 1] = '\0'; 684 | } 685 | } 686 | 687 | static void 688 | get_entry_group(char *buf, const gid_t gid) 689 | { 690 | const struct group *gr; 691 | 692 | gr = getgrgid(gid); 693 | if (gr == NULL) { 694 | snprintf(buf, GROUP_MAX, "%u", gid); 695 | } else { 696 | strncpy(buf, gr->gr_name, GROUP_MAX - 1); 697 | buf[GROUP_MAX - 1] = '\0'; 698 | } 699 | } 700 | 701 | static int 702 | get_user_input(char *input, size_t size, const char *prompt, ...) 703 | { 704 | va_list args; 705 | char msg[PROMPT_MAX]; 706 | int c; 707 | size_t index = 0; 708 | 709 | va_start(args, prompt); 710 | vsnprintf(msg, PROMPT_MAX, prompt, args); 711 | print_status(color_normal, msg); 712 | va_end(args); 713 | 714 | while (1) { 715 | c = getchar(); 716 | 717 | switch (c) { 718 | case XK_ESC: 719 | display_entry_details(); 720 | return -1; 721 | case XK_ENTER: 722 | input[index] = '\0'; 723 | //display_entry_details(); 724 | return 0; 725 | case XK_BACKSPACE: 726 | if (index > 0) { 727 | index--; 728 | printf("\b \b"); 729 | } 730 | break; 731 | default: 732 | if (index < size - 1) { 733 | input[index++] = c; 734 | putchar(c); 735 | } 736 | break; 737 | } 738 | } 739 | 740 | return 0; 741 | } 742 | 743 | static int 744 | check_dir(char *path) 745 | { 746 | DIR *dir; 747 | dir = opendir(path); 748 | 749 | if (dir == NULL) { 750 | if (errno == ENOTDIR) { 751 | return 1; 752 | } else { 753 | return -1; 754 | } 755 | } 756 | 757 | if (closedir(dir) < 0) 758 | return -1; 759 | 760 | return 0; 761 | } 762 | 763 | static void 764 | open_file(char *file) 765 | { 766 | char *ext; 767 | int rule_index; 768 | Command cmd; 769 | 770 | ext = get_file_extension(file); 771 | rule_index = -1; 772 | 773 | if (ext != NULL) { 774 | rule_index = check_rule(ext); 775 | free(ext); 776 | ext = NULL; 777 | } 778 | 779 | if (rule_index < 0) { 780 | cmd.cmdv = editor; 781 | cmd.cmdc = 1; 782 | cmd.argv = &file; 783 | cmd.argc = 1; 784 | cmd.wait_exec = Wait; 785 | } else { 786 | cmd.cmdv = (char **)rules[rule_index].v; 787 | cmd.cmdc = rules[rule_index].vlen; 788 | cmd.argv = &file; 789 | cmd.argc = 1; 790 | cmd.wait_exec = rules[rule_index].wait_exec; 791 | } 792 | 793 | spawn(&cmd); 794 | } 795 | 796 | static char * 797 | get_file_extension(const char *str) 798 | { 799 | char *ext; 800 | const char *dot; 801 | 802 | if (!str) 803 | return NULL; 804 | 805 | dot = strrchr(str, '.'); 806 | if (!dot || dot == str) 807 | return NULL; 808 | 809 | ext = ecalloc(EXTENTION_MAX + 1, sizeof(char)); 810 | strncpy(ext, dot + 1, EXTENTION_MAX); 811 | 812 | for (char *p = ext; *p; p++) 813 | *p = tolower((unsigned char)*p); 814 | 815 | return ext; 816 | } 817 | 818 | static int 819 | check_rule(const char *ex) 820 | { 821 | size_t c, d; 822 | 823 | for (c = 0; c < LEN(rules); c++) 824 | for (d = 0; d < rules[c].exlen; d++) 825 | if (strncmp(rules[c].ext[d], ex, EXTENTION_MAX) == 0) 826 | return c; 827 | return -1; 828 | } 829 | 830 | static void 831 | spawn(Command *cmd) 832 | { 833 | int execvp_errno; 834 | 835 | if (cmd->wait_exec == Wait) 836 | disable_raw_mode(); 837 | 838 | execvp_errno = execute_command(cmd); 839 | 840 | if (cmd->wait_exec == Wait) { 841 | enable_raw_mode(); 842 | termb_resize(); 843 | } 844 | 845 | switch (execvp_errno) { 846 | case 0: 847 | break; 848 | case ENOENT: 849 | print_status(color_err, "Command not found: %s", cmd->cmdv[0]); 850 | break; 851 | case EACCES: 852 | print_status(color_err, "Permission denied: %s", cmd->argv[0]); 853 | break; 854 | case E2BIG: 855 | print_status( 856 | color_err, "Argument list too long: %s", cmd->argv[0]); 857 | break; 858 | case EFAULT: 859 | print_status(color_err, "Bad address: %s", cmd->argv[0]); 860 | break; 861 | case EIO: 862 | print_status(color_err, "I/O error: %s", cmd->argv[0]); 863 | break; 864 | case ENOEXEC: 865 | print_status(color_err, "Exec format error: %s", cmd->argv[0]); 866 | break; 867 | case ENOMEM: 868 | print_status(color_err, "Out of memory: %s", cmd->argv[0]); 869 | break; 870 | case ENOTDIR: 871 | print_status(color_err, "Not a directory: %s", cmd->argv[0]); 872 | break; 873 | case ETXTBSY: 874 | print_status(color_err, "Text file busy: %s", cmd->argv[0]); 875 | break; 876 | case EPERM: 877 | print_status( 878 | color_err, "Operation not permitted: %s", cmd->argv[0]); 879 | break; 880 | case ELOOP: 881 | print_status(color_err, 882 | "Too many symbolic links encountered: %s", 883 | cmd->argv[0]); 884 | break; 885 | case ENAMETOOLONG: 886 | print_status(color_err, "File name too long: %s", cmd->argv[0]); 887 | break; 888 | case ENFILE: 889 | print_status( 890 | color_err, "File table overflow: %s", cmd->argv[0]); 891 | break; 892 | case ENODEV: 893 | print_status(color_err, "No such device: %s", cmd->argv[0]); 894 | break; 895 | case ENOLCK: 896 | print_status(color_err, "No locks available: %s", cmd->argv[0]); 897 | break; 898 | case ENOSYS: 899 | print_status(color_err, "Function not implemented: %s", 900 | cmd->argv[0]); 901 | break; 902 | case ENOTBLK: 903 | print_status( 904 | color_err, "Block device required: %s", cmd->argv[0]); 905 | break; 906 | case EISDIR: 907 | print_status(color_err, "Is a directory: %s", cmd->argv[0]); 908 | break; 909 | case EROFS: 910 | print_status( 911 | color_err, "Read-only file system: %s", cmd->argv[0]); 912 | break; 913 | case EMFILE: 914 | print_status( 915 | color_err, "Too many open files: %s", cmd->argv[0]); 916 | break; 917 | default: 918 | print_status(color_err, "execvp failed with errno: %d", 919 | execvp_errno); 920 | break; 921 | } 922 | 923 | errno = 0; 924 | } 925 | 926 | static int 927 | execute_command(Command *cmd) 928 | { 929 | size_t argc; 930 | char **argv; 931 | char log_command[99024]; 932 | size_t pos; 933 | int wait_status; 934 | int exit_status = 0; 935 | int exit_errno = 0; 936 | 937 | argc = cmd->cmdc + cmd->argc + 2; 938 | argv = ecalloc(argc, sizeof(char *)); 939 | 940 | memcpy(argv, cmd->cmdv, cmd->cmdc * sizeof(char *)); 941 | memcpy(&argv[cmd->cmdc], cmd->argv, cmd->argc * sizeof(char *)); 942 | 943 | argv[argc - 1] = NULL; 944 | 945 | // Construct the command string for logging 946 | log_command[0] = '\0'; // Initialize the string with null terminator 947 | pos = 0; 948 | for (size_t i = 0; i < argc - 1; ++i) { 949 | if (argv[i] != NULL) { 950 | int len = snprintf(log_command + pos, 951 | sizeof(log_command) - pos, "%s ", argv[i]); 952 | if (len < 0 || pos + len >= sizeof(log_command)) { 953 | break; // Avoid buffer overflow 954 | } 955 | pos += len; 956 | } 957 | } 958 | log_command[sizeof(log_command) - 1] = '\0'; // Ensure null-termination 959 | log_to_file(__func__, __LINE__, "exec = %s", log_command); 960 | 961 | fork_pid = fork(); 962 | switch (fork_pid) { 963 | case -1: 964 | free(argv); 965 | argv = NULL; 966 | return -1; 967 | case 0: 968 | exit_status = execvp(argv[0], argv); 969 | if (exit_status < 0) { 970 | free(argv); 971 | argv = NULL; 972 | } 973 | exit(errno); 974 | default: 975 | waitpid(fork_pid, &wait_status, cmd->wait_exec); 976 | if (WIFEXITED(wait_status) && WEXITSTATUS(wait_status)) { 977 | exit_errno = WEXITSTATUS(wait_status); 978 | } 979 | } 980 | free(argv); 981 | argv = NULL; 982 | fork_pid = 0; 983 | return exit_errno; 984 | } 985 | 986 | static void 987 | termb_append(const char *str, size_t len) 988 | { 989 | if (len >= term.buffer_left) { 990 | term.buffer = erealloc(term.buffer, term.buffer_size * 2); 991 | term.buffer_size *= 2; 992 | } 993 | 994 | memcpy(&term.buffer[term.buffer_index], str, len); 995 | term.buffer_index += len; 996 | term.buffer_left = term.buffer_size - term.buffer_index; 997 | } 998 | 999 | static void 1000 | termb_write(void) 1001 | { 1002 | if (write(STDOUT_FILENO, term.buffer, term.buffer_index - 1) < 0) 1003 | die("write:"); 1004 | term.buffer_index = 0; 1005 | term.buffer_left = term.buffer_size; 1006 | } 1007 | 1008 | static void 1009 | write_entries_name(void) 1010 | { 1011 | int half_cols = term.cols / 2; 1012 | char result[term.cols + 100]; 1013 | 1014 | int result_len = snprintf(result, sizeof(result), 1015 | "\x1b[1;1H" // Move cursor to top-left corner 1016 | "\x1b[%d;38;5;%d;48;5;%dm" // Set colors for left pane 1017 | "%-*.*s" // Left string with padding 1018 | "\x1b[%d;38;5;%d;48;5;%dm" // Set colors for right pane 1019 | "%-*.*s" // Right string with padding 1020 | "\x1b[0m", // Reset colors 1021 | color_panell.attr, color_panell.fg, color_panell.bg, half_cols, 1022 | half_cols, panes[Left].path, color_panelr.attr, color_panelr.fg, 1023 | color_panelr.bg, half_cols, half_cols, panes[Right].path); 1024 | 1025 | write(STDOUT_FILENO, result, result_len); 1026 | } 1027 | 1028 | static void 1029 | termb_resize(void) 1030 | { 1031 | termb_append("\033[2J", 4); 1032 | get_term_size(); 1033 | update_screen(); 1034 | } 1035 | 1036 | static void 1037 | cd_to_parent(const Arg *arg) 1038 | { 1039 | char parent_path[PATH_MAX]; 1040 | char *last_slash; 1041 | 1042 | if (current_pane->path[0] == '/' && current_pane->path[1] == '\0') 1043 | return; 1044 | 1045 | strncpy(parent_path, current_pane->path, PATH_MAX); 1046 | last_slash = strrchr(parent_path, '/'); 1047 | if (last_slash != NULL) 1048 | *last_slash = '\0'; 1049 | 1050 | if (strnlen(parent_path, PATH_MAX) == 0) { 1051 | strncpy(parent_path, "/", PATH_MAX); 1052 | } 1053 | 1054 | strncpy(current_pane->path, parent_path, PATH_MAX); 1055 | 1056 | remove_watch(current_pane); 1057 | set_pane_entries(current_pane); 1058 | add_watch(current_pane); 1059 | 1060 | current_pane->current_index = 0; 1061 | current_pane->start_index = 0; 1062 | update_screen(); 1063 | } 1064 | 1065 | static void 1066 | create_new_file(const Arg *arg) 1067 | { 1068 | char file_name[NAME_MAX]; 1069 | char full_path[PATH_MAX]; 1070 | int fd; 1071 | 1072 | if (get_user_input(file_name, NAME_MAX, "new file: ") != 0) 1073 | return; 1074 | 1075 | get_fullpath(full_path, current_pane->path, file_name); 1076 | 1077 | fd = open(full_path, O_CREAT | O_EXCL, new_file_perm); 1078 | if (fd < 0) { 1079 | print_status(color_err, strerror(errno)); 1080 | return; 1081 | } 1082 | 1083 | display_entry_details(); 1084 | close(fd); 1085 | } 1086 | 1087 | static void 1088 | create_new_dir(const Arg *arg) 1089 | { 1090 | char dir_name[NAME_MAX]; 1091 | char full_path[PATH_MAX]; 1092 | 1093 | if (get_user_input(dir_name, sizeof(dir_name), "new directory: ") != 0) 1094 | return; 1095 | 1096 | get_fullpath(full_path, current_pane->path, dir_name); 1097 | 1098 | if (mkdir(full_path, new_dir_perm) != 0) 1099 | print_status(color_err, strerror(errno)); 1100 | } 1101 | 1102 | static void 1103 | copy_entries(const Arg *arg) 1104 | { 1105 | selected_entries = ecalloc(current_pane->entry_count, sizeof(char *)); 1106 | selected_count = get_selected_paths(current_pane, selected_entries); 1107 | 1108 | if (selected_count < 1) { 1109 | selected_entries[0] = 1110 | current_pane->entries[current_pane->current_index] 1111 | .fullpath; 1112 | selected_count = 1; 1113 | } 1114 | 1115 | if (selected_count < 1) { 1116 | print_status(color_warn, "No entries selected."); 1117 | } else { 1118 | print_status(color_normal, "Entries copied."); 1119 | } 1120 | 1121 | mode = NormalMode; 1122 | } 1123 | 1124 | static void 1125 | delete_entry(const Arg *arg) 1126 | { 1127 | Command cmd; 1128 | char confirmation[4]; 1129 | 1130 | if (current_pane->entry_count <= 0 || 1131 | current_pane->current_index >= current_pane->entry_count) { 1132 | print_status(color_err, "No entry selected or invalid index."); 1133 | return; 1134 | } 1135 | 1136 | selected_entries = ecalloc(current_pane->entry_count, sizeof(char *)); 1137 | selected_count = get_selected_paths(current_pane, selected_entries); 1138 | 1139 | if (selected_count < 1) { 1140 | selected_entries[0] = 1141 | current_pane->entries[current_pane->current_index] 1142 | .fullpath; 1143 | selected_count = 1; 1144 | } 1145 | 1146 | log_to_file(__func__, __LINE__, "SELECTED COUNT = %d", selected_count); 1147 | log_to_file(__func__, __LINE__, "SELECTED = %s", selected_entries[0]); 1148 | 1149 | /* confirmation */ 1150 | if (get_user_input(confirmation, sizeof(confirmation), "Delete (%s)?", 1151 | delconf) < 0) { 1152 | free(selected_entries); 1153 | selected_entries = NULL; 1154 | selected_count = 0; 1155 | return; 1156 | } 1157 | if (strncmp(confirmation, delconf, delconf_len) != 0) { 1158 | print_status(color_warn, "Deletion aborted."); 1159 | free(selected_entries); 1160 | selected_entries = NULL; 1161 | selected_count = 0; 1162 | return; 1163 | } 1164 | 1165 | cmd.cmdv = (char **)rm_cmd; 1166 | cmd.cmdc = rm_cmd_len; 1167 | cmd.argv = selected_entries; 1168 | cmd.argc = selected_count; 1169 | cmd.wait_exec = DontWait; 1170 | 1171 | spawn(&cmd); 1172 | 1173 | free(selected_entries); 1174 | selected_entries = NULL; 1175 | selected_count = 0; 1176 | mode = NormalMode; 1177 | } 1178 | 1179 | static void 1180 | move_bottom(const Arg *arg) 1181 | { 1182 | current_pane->current_index = current_pane->entry_count - 1; 1183 | current_pane->start_index = current_pane->entry_count - (term.rows - 2); 1184 | if (current_pane->start_index < 0) { 1185 | current_pane->start_index = 0; 1186 | } 1187 | update_screen(); 1188 | } 1189 | 1190 | static void 1191 | update_entry(Pane *pane, int index) 1192 | { 1193 | int err; 1194 | size_t max_len; 1195 | Entry entry; 1196 | char buffer[PATH_MAX]; 1197 | int pos; 1198 | 1199 | if (index < 0 || index >= pane->entry_count) 1200 | return; 1201 | 1202 | max_len = term.cols / 2; 1203 | entry = pane->entries[index]; 1204 | pos = index - pane->start_index; 1205 | 1206 | if (pane->entries[index].selected == 1) 1207 | entry.color = color_selected; 1208 | 1209 | if (pane == current_pane && index == current_pane->current_index) { 1210 | entry.color.attr |= RVS; 1211 | } 1212 | 1213 | err = snprintf(buffer, sizeof(buffer), 1214 | "\x1b[%d;%dH" // Move cursor to the entry position 1215 | "\x1b[%d;38;5;%d;48;5;%dm%-*.*s\x1b[0m", 1216 | pos + 2, pane->offset, entry.color.attr, entry.color.fg, 1217 | entry.color.bg, (int)max_len - 1, (int)max_len, entry.name); 1218 | 1219 | if (err < 0) 1220 | print_status(color_err, strerror(errno)); 1221 | 1222 | write(STDOUT_FILENO, buffer, strlen(buffer)); 1223 | } 1224 | 1225 | static void 1226 | move_cursor(const Arg *arg) 1227 | { 1228 | int new_start_index; 1229 | int old_index; 1230 | 1231 | if (current_pane->entry_count == 0) 1232 | return; 1233 | 1234 | old_index = current_pane->current_index; 1235 | current_pane->current_index += arg->i; 1236 | 1237 | if (current_pane->current_index < 0) { 1238 | current_pane->current_index = 0; 1239 | } else if (current_pane->current_index >= current_pane->entry_count) { 1240 | current_pane->current_index = current_pane->entry_count - 1; 1241 | } 1242 | 1243 | new_start_index = current_pane->start_index; 1244 | if (current_pane->current_index < current_pane->start_index) { 1245 | current_pane->start_index = current_pane->current_index; 1246 | } else if (current_pane->current_index >= 1247 | current_pane->start_index + term.rows - 2) { 1248 | current_pane->start_index = 1249 | current_pane->current_index - (term.rows - 3); 1250 | } 1251 | 1252 | if (new_start_index != current_pane->start_index) { 1253 | update_screen(); 1254 | } else { 1255 | // Update only the necessary entries 1256 | if (old_index != current_pane->current_index) { 1257 | update_entry(current_pane, old_index); 1258 | update_entry(current_pane, current_pane->current_index); 1259 | } 1260 | } 1261 | 1262 | if (mode == VisualMode) 1263 | select_cur_entry(&(Arg) { .i = Select }); 1264 | } 1265 | 1266 | static void 1267 | move_top(const Arg *arg) 1268 | { 1269 | current_pane->current_index = 0; 1270 | current_pane->start_index = 0; 1271 | update_screen(); 1272 | } 1273 | 1274 | static void 1275 | move_entries(const Arg *arg) 1276 | { 1277 | if (selected_count <= 0) { 1278 | print_status(color_warn, "No entries copied."); 1279 | log_to_file(__func__, __LINE__, "No entries copied."); 1280 | return; 1281 | } 1282 | 1283 | char **argv = ecalloc(selected_count + 2, PATH_MAX); 1284 | for (int i = 0; i < selected_count; i++) { 1285 | argv[i] = selected_entries[i]; 1286 | } 1287 | argv[selected_count] = current_pane->path; // Destination path 1288 | argv[selected_count + 1] = NULL; 1289 | 1290 | Command cmd; 1291 | cmd.cmdv = (char **)mv_cmd; 1292 | cmd.cmdc = mv_cmd_len; 1293 | cmd.argv = argv; 1294 | cmd.argc = selected_count + 1; 1295 | cmd.wait_exec = DontWait; 1296 | 1297 | print_status(color_normal, "Moving..."); 1298 | spawn(&cmd); 1299 | print_status(color_normal, "Moved..."); 1300 | 1301 | free(selected_entries); 1302 | selected_entries = NULL; 1303 | selected_count = 0; 1304 | free(argv); 1305 | argv = NULL; 1306 | } 1307 | 1308 | static void 1309 | open_entry(const Arg *arg) 1310 | { 1311 | if (current_pane->entry_count < 1) 1312 | return; 1313 | 1314 | Entry *current_entry = 1315 | ¤t_pane->entries[current_pane->current_index]; 1316 | 1317 | switch (check_dir(current_entry->fullpath)) { 1318 | case 0: /* directory */ 1319 | strncpy(current_pane->path, current_entry->fullpath, PATH_MAX); 1320 | remove_watch(current_pane); 1321 | set_pane_entries(current_pane); 1322 | add_watch(current_pane); 1323 | current_pane->current_index = 0; 1324 | current_pane->start_index = 0; 1325 | update_screen(); 1326 | break; 1327 | case 1: /* not a directory open file */ 1328 | if (S_ISREG(current_entry->st.st_mode)) { 1329 | errno = 0; /* check_dir errno */ 1330 | open_file(current_entry->fullpath); 1331 | } 1332 | break; 1333 | case -1: /* failed to open directory */ 1334 | print_status(color_err, strerror(errno)); 1335 | } 1336 | } 1337 | 1338 | static void 1339 | paste_entries(const Arg *arg) 1340 | { 1341 | if (selected_count <= 0) { 1342 | print_status(color_warn, "No entries copied"); 1343 | log_to_file(__func__, __LINE__, "No entries copied."); 1344 | return; 1345 | } 1346 | 1347 | char **argv = ecalloc(selected_count + 2, sizeof(char *)); 1348 | for (int i = 0; i < selected_count; i++) { 1349 | argv[i] = selected_entries[i]; 1350 | } 1351 | argv[selected_count] = current_pane->path; // Destination path 1352 | argv[selected_count + 1] = NULL; 1353 | 1354 | Command cmd; 1355 | cmd.cmdv = (char **)cp_cmd; 1356 | cmd.cmdc = cp_cmd_len; 1357 | cmd.argv = argv; 1358 | cmd.argc = selected_count + 1; 1359 | cmd.wait_exec = DontWait; 1360 | 1361 | print_status(color_normal, "Pasting..."); 1362 | spawn(&cmd); 1363 | print_status(color_normal, "Pasted..."); 1364 | 1365 | free(selected_entries); 1366 | selected_entries = NULL; 1367 | selected_count = 0; 1368 | free(argv); 1369 | argv = NULL; 1370 | } 1371 | 1372 | static void 1373 | quit(const Arg *arg) 1374 | { 1375 | cancel_search_highlight(); 1376 | cleanup_filesystem_events(); 1377 | if (selected_entries != NULL) 1378 | free(selected_entries); 1379 | if (term.buffer != NULL) 1380 | free(term.buffer); 1381 | if (panes[Left].entries != NULL) 1382 | free(panes[Left].entries); 1383 | if (panes[Right].entries != NULL) 1384 | free(panes[Right].entries); 1385 | disable_raw_mode(); 1386 | exit(EXIT_SUCCESS); 1387 | } 1388 | 1389 | static void 1390 | refresh(const Arg *arg) 1391 | { 1392 | kill(main_pid, SIGWINCH); 1393 | } 1394 | 1395 | static void 1396 | switch_pane(const Arg *arg) 1397 | { 1398 | current_pane = &panes[pane_idx ^= 1]; 1399 | update_screen(); 1400 | } 1401 | 1402 | static void 1403 | select_entry(Entry *entry, int s) 1404 | { 1405 | entry->selected = 1406 | (s == InvertSelection) ? !entry->selected : (s == Select); 1407 | } 1408 | 1409 | static void 1410 | select_cur_entry(const Arg *arg) 1411 | { 1412 | select_entry( 1413 | ¤t_pane->entries[current_pane->current_index], arg->i); 1414 | update_entry(current_pane, current_pane->current_index); 1415 | } 1416 | 1417 | static void 1418 | toggle_dotfiles(const Arg *arg) 1419 | { 1420 | show_dotfiles ^= 1; 1421 | set_pane_entries(&panes[Left]); 1422 | set_pane_entries(&panes[Right]); 1423 | update_screen(); 1424 | } 1425 | 1426 | static void 1427 | die(const char *fmt, ...) 1428 | { 1429 | va_list ap; 1430 | va_start(ap, fmt); 1431 | vfprintf(stderr, fmt, ap); 1432 | va_end(ap); 1433 | if (fmt[0] != '\0' && fmt[strlen(fmt) - 1] == ':') { 1434 | fputc(' ', stderr); 1435 | perror(NULL); 1436 | } else { 1437 | fputc('\n', stderr); 1438 | } 1439 | exit(EXIT_FAILURE); 1440 | } 1441 | 1442 | static void * 1443 | ecalloc(size_t nmemb, size_t size) 1444 | { 1445 | void *p; 1446 | if ((p = calloc(nmemb, size)) == NULL) 1447 | die("calloc:"); 1448 | return p; 1449 | } 1450 | 1451 | static void * 1452 | erealloc(void *p, size_t len) 1453 | { 1454 | if ((p = realloc(p, len)) == NULL) 1455 | die("realloc: %s\n", strerror(errno)); 1456 | return p; 1457 | } 1458 | 1459 | #if defined(__linux__) 1460 | static void * 1461 | event_handler(void *arg) 1462 | { 1463 | Pane *pane = (Pane *)arg; 1464 | char buffer[EV_BUF_LEN]; 1465 | int length, i; 1466 | 1467 | log_to_file(__func__, __LINE__, "Event handler started for path: %s", 1468 | pane->path); 1469 | 1470 | pane->watcher.fd = inotify_init(); 1471 | if (pane->watcher.fd < 0) { 1472 | log_to_file(__func__, __LINE__, 1473 | "Error initializing inotify: %s", strerror(errno)); 1474 | die("inotify_init:"); 1475 | pthread_exit(NULL); 1476 | } 1477 | 1478 | add_watch(pane); 1479 | 1480 | while (1) { 1481 | length = read(pane->watcher.fd, buffer, EV_BUF_LEN); 1482 | if (length <= 0) { 1483 | log_to_file(__func__, __LINE__, 1484 | "Error reading inotify event: %s", 1485 | strerror(errno)); 1486 | die("read:"); 1487 | break; 1488 | } 1489 | 1490 | if (length < (int)sizeof(struct inotify_event)) { 1491 | log_to_file(__func__, __LINE__, 1492 | "Incomplete inotify event read"); 1493 | die("read:"); 1494 | break; 1495 | } 1496 | 1497 | i = 0; 1498 | while (i < length) { 1499 | struct inotify_event *event = 1500 | (struct inotify_event *)&buffer[i]; 1501 | if (event->mask) { 1502 | log_to_file(__func__, __LINE__, 1503 | "Inotify event detected: mask=%u, len=%u, name=%s", 1504 | event->mask, event->len, 1505 | event->len ? event->name : ""); 1506 | usleep(50 * 1000); // 500 milliseconds 1507 | kill(main_pid, pane->watcher.signal); 1508 | } 1509 | i += sizeof(struct inotify_event) + event->len; 1510 | } 1511 | } 1512 | close(pane->watcher.fd); 1513 | return NULL; 1514 | } 1515 | 1516 | void 1517 | add_watch(Pane *pane) 1518 | { 1519 | pane->watcher.descriptor = inotify_add_watch( 1520 | pane->watcher.fd, pane->path, IN_CREATE | IN_DELETE); 1521 | if (pane->watcher.descriptor < 0) { 1522 | log_to_file(__func__, __LINE__, 1523 | "Error adding inotify watch: %s", strerror(errno)); 1524 | die("inotify_add_watch:"); 1525 | } 1526 | log_to_file(__func__, __LINE__, "Added inotify watch for path: %s", 1527 | pane->path); 1528 | } 1529 | 1530 | void 1531 | remove_watch(Pane *pane) 1532 | { 1533 | if (inotify_rm_watch(pane->watcher.fd, pane->watcher.descriptor) < 0) 1534 | die("inotify_rm_watch:"); 1535 | } 1536 | 1537 | void 1538 | cleanup_filesystem_events(void) 1539 | { 1540 | remove_watch(&panes[Left]); 1541 | pthread_cancel(panes[Left].watcher.thread); 1542 | pthread_join(panes[Left].watcher.thread, NULL); 1543 | 1544 | remove_watch(&panes[Right]); 1545 | pthread_cancel(panes[Right].watcher.thread); 1546 | pthread_join(panes[Right].watcher.thread, NULL); 1547 | 1548 | close(panes[Left].watcher.fd); 1549 | close(panes[Right].watcher.fd); 1550 | } 1551 | 1552 | void 1553 | filesystem_event_init(void) 1554 | { 1555 | pthread_create( 1556 | &panes[Left].watcher.thread, NULL, event_handler, &panes[Left]); 1557 | pthread_create(&panes[Right].watcher.thread, NULL, event_handler, 1558 | &panes[Right]); 1559 | } 1560 | 1561 | #elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \ 1562 | defined(__APPLE__) || defined(__DragonFly__) 1563 | 1564 | static void * 1565 | event_handler(void *arg) 1566 | { 1567 | Pane *pane = (Pane *)arg; 1568 | struct kevent event; 1569 | 1570 | log_to_file(__func__, __LINE__, "Event handler started for path: %s", 1571 | pane->path); 1572 | 1573 | pane->watcher.kq = kqueue(); 1574 | if (pane->watcher.kq == -1) { 1575 | log_to_file(__func__, __LINE__, "kqueue creation error: %s", 1576 | strerror(errno)); 1577 | pthread_exit(NULL); 1578 | } 1579 | 1580 | log_to_file(__func__, __LINE__, "Kqueue created with descriptor: %d", 1581 | pane->watcher.kq); 1582 | 1583 | add_watch(pane); 1584 | 1585 | while (1) { 1586 | log_to_file(__func__, __LINE__, "Waiting for events..."); 1587 | int nev = kevent(pane->watcher.kq, NULL, 0, &event, 1, 1588 | NULL); // NULL timeout for indefinite wait 1589 | if (nev < 0) { 1590 | if (errno == EINTR) { 1591 | continue; // Retry if interrupted by signal 1592 | } 1593 | log_to_file(__func__, __LINE__, "kevent wait error: %s", 1594 | strerror(errno)); 1595 | pthread_exit(NULL); 1596 | } else if (nev > 0) { 1597 | log_to_file(__func__, __LINE__, "Event detected."); 1598 | usleep(500 * 1000); // 500 milliseconds 1599 | kill(main_pid, pane->watcher.signal); 1600 | if (event.fflags & NOTE_DELETE) { 1601 | log_to_file(__func__, __LINE__, 1602 | "File deleted: %s", pane->path); 1603 | break; // Exit the loop if the file is deleted 1604 | } 1605 | log_to_file(__func__, __LINE__, 1606 | "Re-adding watch after event."); 1607 | add_watch( 1608 | pane); // Re-add the watch after handling the event 1609 | } 1610 | } 1611 | 1612 | log_to_file(__func__, __LINE__, "Closing file descriptor: %d", 1613 | pane->watcher.fd); 1614 | close(pane->watcher.fd); 1615 | close(pane->watcher.kq); 1616 | return NULL; 1617 | } 1618 | 1619 | static void 1620 | add_watch(Pane *pane) 1621 | { 1622 | if (pane->watcher.fd >= 0) { 1623 | log_to_file(__func__, __LINE__, 1624 | "Closing previous file descriptor: %d", 1625 | pane->watcher.fd); 1626 | close(pane->watcher.fd); 1627 | } 1628 | 1629 | pane->watcher.fd = open(pane->path, O_RDONLY); 1630 | if (pane->watcher.fd < 0) { 1631 | log_to_file( 1632 | __func__, __LINE__, "open error: %s", strerror(errno)); 1633 | return; 1634 | } 1635 | 1636 | log_to_file(__func__, __LINE__, 1637 | "Opened file descriptor: %d for path: %s", pane->watcher.fd, 1638 | pane->path); 1639 | 1640 | EV_SET(&pane->watcher.change, pane->watcher.fd, EVFILT_VNODE, 1641 | EV_ADD | EV_ENABLE | EV_ONESHOT, 1642 | NOTE_DELETE | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME | 1643 | NOTE_REVOKE, 1644 | 0, (void *)pane->path); 1645 | 1646 | if (kevent(pane->watcher.kq, &pane->watcher.change, 1, NULL, 0, NULL) == 1647 | -1) { 1648 | log_to_file(__func__, __LINE__, "kevent register error: %s", 1649 | strerror(errno)); 1650 | close(pane->watcher.fd); 1651 | pane->watcher.fd = -1; 1652 | return; 1653 | } 1654 | 1655 | log_to_file(__func__, __LINE__, 1656 | "Event registered successfully for path: %s", pane->path); 1657 | } 1658 | 1659 | static void 1660 | remove_watch(Pane *pane) 1661 | { 1662 | if (pane->watcher.fd >= 0) { 1663 | log_to_file(__func__, __LINE__, 1664 | "Removing watch for file descriptor: %d", 1665 | pane->watcher.fd); 1666 | if (close(pane->watcher.fd) < 0) 1667 | log_to_file(__func__, __LINE__, 1668 | "kevent remove error: %s", strerror(errno)); 1669 | pane->watcher.fd = -1; 1670 | } 1671 | } 1672 | 1673 | void 1674 | cleanup_filesystem_events(void) 1675 | { 1676 | remove_watch(&panes[Left]); 1677 | pthread_cancel(panes[Left].watcher.thread); 1678 | //pthread_join(panes[Left].watcher.thread, NULL); 1679 | 1680 | remove_watch(&panes[Right]); 1681 | pthread_cancel(panes[Right].watcher.thread); 1682 | //pthread_join(panes[Right].watcher.thread, NULL); 1683 | 1684 | close(panes[Left].watcher.kq); 1685 | close(panes[Right].watcher.kq); 1686 | } 1687 | 1688 | void 1689 | filesystem_event_init(void) 1690 | { 1691 | pthread_create( 1692 | &panes[Left].watcher.thread, NULL, event_handler, &panes[Left]); 1693 | pthread_create(&panes[Right].watcher.thread, NULL, event_handler, 1694 | &panes[Right]); 1695 | } 1696 | 1697 | #endif 1698 | 1699 | static void 1700 | visual_mode(const Arg *arg) 1701 | { 1702 | if (current_pane->entry_count <= 0) { 1703 | print_status(color_warn, "No entries to select."); 1704 | return; 1705 | } 1706 | 1707 | if (mode == VisualMode) { 1708 | normal_mode(&(Arg) { 0 }); 1709 | } else { 1710 | mode = VisualMode; 1711 | select_cur_entry(&(Arg) { .i = Select }); 1712 | print_status(color_normal, " --VISUAL-- "); 1713 | } 1714 | 1715 | update_screen(); 1716 | } 1717 | 1718 | static void 1719 | normal_mode(const Arg *arg) 1720 | { 1721 | if (mode == SearchMode) 1722 | cancel_search_highlight(); 1723 | mode = NormalMode; 1724 | display_entry_details(); 1725 | } 1726 | 1727 | void 1728 | select_all(const Arg *arg) 1729 | { 1730 | if (current_pane->entry_count <= 0) { 1731 | print_status(color_warn, "No entries to select."); 1732 | return; 1733 | } 1734 | 1735 | for (int i = 0; i < current_pane->entry_count; i++) { 1736 | select_entry(¤t_pane->entries[i], arg->i); 1737 | } 1738 | 1739 | update_screen(); 1740 | } 1741 | 1742 | static void 1743 | start_search(const Arg *arg) 1744 | { 1745 | if (mode == SearchMode) { 1746 | cancel_search_highlight(); 1747 | mode = NormalMode; 1748 | update_screen(); 1749 | return; 1750 | } 1751 | 1752 | mode = SearchMode; 1753 | memset(current_pane->search_term, 0, NAME_MAX); 1754 | 1755 | if (get_user_input(current_pane->search_term, NAME_MAX, "Search: ") != 1756 | 0) { 1757 | cancel_search_highlight(); 1758 | mode = NormalMode; 1759 | update_screen(); 1760 | return; 1761 | } 1762 | 1763 | update_search_highlight(current_pane->search_term); 1764 | mode = NormalMode; 1765 | current_pane->current_match = -1; 1766 | update_screen(); 1767 | } 1768 | 1769 | static void 1770 | update_search_highlight(const char *search_term) 1771 | { 1772 | if (current_pane->matched_indices != NULL) { 1773 | free(current_pane->matched_indices); 1774 | current_pane->matched_indices = NULL; 1775 | current_pane->matched_count = 0; 1776 | } 1777 | 1778 | current_pane->matched_indices = 1779 | (int *)ecalloc(current_pane->entry_count, sizeof(int)); 1780 | 1781 | for (int i = 0; i < current_pane->entry_count; i++) { 1782 | if (strcasestr(current_pane->entries[i].name, search_term) != 1783 | NULL) { 1784 | current_pane->entries[i].matched = 1; 1785 | current_pane->matched_indices 1786 | [current_pane->matched_count++] = i; 1787 | } else { 1788 | current_pane->entries[i].matched = 0; 1789 | } 1790 | set_entry_color(¤t_pane->entries[i]); 1791 | } 1792 | update_screen(); 1793 | } 1794 | 1795 | static void 1796 | cancel_search_highlight(void) 1797 | { 1798 | if (current_pane->matched_indices == NULL) 1799 | return; 1800 | 1801 | for (int i = 0; i < current_pane->entry_count; i++) { 1802 | set_entry_color(¤t_pane->entries[i]); 1803 | } 1804 | if (current_pane->matched_indices != NULL) { 1805 | free(current_pane->matched_indices); 1806 | current_pane->matched_indices = NULL; 1807 | } 1808 | current_pane->matched_count = 0; 1809 | current_pane->current_match = -1; 1810 | } 1811 | 1812 | static void 1813 | move_to_match(const Arg *arg) 1814 | { 1815 | if (current_pane->matched_count == 0) { 1816 | print_status(color_warn, "No matches found."); 1817 | return; 1818 | } 1819 | 1820 | if (arg->i == NextMatch) { 1821 | current_pane->current_match = 1822 | (current_pane->current_match + 1) % 1823 | current_pane->matched_count; 1824 | } else if (arg->i == PrevMatch) { 1825 | current_pane->current_match = 1826 | (current_pane->current_match - 1 + 1827 | current_pane->matched_count) % 1828 | current_pane->matched_count; 1829 | } 1830 | 1831 | current_pane->current_index = 1832 | current_pane->matched_indices[current_pane->current_match]; 1833 | 1834 | if (current_pane->current_index < current_pane->start_index || 1835 | current_pane->current_index >= 1836 | current_pane->start_index + term.rows - 2) { 1837 | current_pane->start_index = 1838 | current_pane->current_index - (term.rows - 2) / 2; 1839 | if (current_pane->start_index < 0) { 1840 | current_pane->start_index = 0; 1841 | } 1842 | } 1843 | 1844 | update_screen(); 1845 | } 1846 | 1847 | int 1848 | main(int argc, const char *argv[]) 1849 | { 1850 | char c; 1851 | 1852 | if (remove("/tmp/sfm.log") != 0) { 1853 | fprintf(stderr, "Error removing log file: %s\n", 1854 | strerror(errno)); 1855 | } 1856 | 1857 | if (argc == 1) { 1858 | #if defined(__OpenBSD__) 1859 | if (pledge("cpath exec getpw proc rpath stdio tmppath tty wpath", 1860 | NULL) == -1) 1861 | die("pledge"); 1862 | #endif /* __OpenBSD__ */ 1863 | mode = NormalMode; 1864 | init_term(); 1865 | enable_raw_mode(); 1866 | get_env(); 1867 | set_panes(); 1868 | start_signal(); 1869 | log_to_file(__func__, __LINE__, "start"); 1870 | 1871 | termb_append("\033[2J", 4); 1872 | update_screen(); 1873 | 1874 | filesystem_event_init(); 1875 | while (1) { 1876 | c = getchar(); 1877 | handle_keypress(c); 1878 | } 1879 | } else if (argc == 2 && strncmp("-v", argv[1], 2) == 0) { 1880 | die("sfm-" VERSION); 1881 | } else { 1882 | die("usage: sfm [-v]"); 1883 | } 1884 | 1885 | return 0; 1886 | } 1887 | -------------------------------------------------------------------------------- /sfm.h: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | 3 | #ifndef SFM_H 4 | #define SFM_H 5 | 6 | /* macros */ 7 | #define NORM 0 8 | #define BOLD 1 9 | #define DIM 2 10 | #define ITALIC 3 11 | #define UNDERL 4 12 | #define BLINK 5 13 | #define RVS 7 14 | #define HIDDEN 8 15 | #define STRIKE 9 16 | 17 | #define XK_CTRL(k) ((k) & 0x1f) 18 | #define XK_ALT(k) (k)1b 19 | #define XK_UP 0x415b1b 20 | #define XK_DOWN 0x425b1b 21 | #define XK_RIGHT 0x435b1b 22 | #define XK_LEFT 0x445b1b 23 | #define XK_HOME 0x485b1b 24 | #define XK_END 0x7e345b1b 25 | #define XK_PGUP 0x7e355b1b 26 | #define XK_PGDOWN 0x7e365b1b 27 | #define XK_BACKSPACE 0x7f 28 | #define XK_TAB 0x09 29 | #define XK_ENTER 0x0D 30 | #define XK_ESC 0x1B 31 | #define XK_SPACE 0x20 32 | 33 | #define UINT8_LEN 3 34 | #define UINT16_LEN 5 35 | 36 | #define GROUP_MAX 32 37 | #define USER_MAX 32 38 | #define DATETIME_MAX 20 39 | #define EXTENTION_MAX 4 40 | #define PROMPT_MAX 64 41 | #define PERMISSION_MAX 10 42 | #define FSIZE_MAX 32 43 | 44 | #define MAX(A, B) ((A) > (B) ? (A) : (B)) 45 | #define MIN(A, B) ((A) < (B) ? (A) : (B)) 46 | #define LEN(A) (sizeof(A) / sizeof(A[0])) 47 | #define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B)) 48 | 49 | #define RULE(category, command, wait) \ 50 | { \ 51 | category, LEN(category), command, LEN(command), (wait) \ 52 | } 53 | 54 | typedef struct { 55 | struct termios orig; 56 | struct termios newterm; 57 | int rows; 58 | int cols; 59 | char *buffer; 60 | unsigned long buffer_size; 61 | unsigned long buffer_left; 62 | ssize_t buffer_index; 63 | } Terminal; 64 | 65 | typedef struct { 66 | uint8_t fg; 67 | uint8_t bg; 68 | uint8_t attr; 69 | } ColorPair; 70 | 71 | typedef struct { 72 | char fullpath[PATH_MAX]; 73 | char name[NAME_MAX]; 74 | struct stat st; 75 | int selected; 76 | int matched; 77 | ColorPair color; 78 | } Entry; 79 | 80 | #if defined(__linux__) 81 | typedef struct { 82 | char directory[PATH_MAX]; 83 | pthread_t thread; 84 | int fd; 85 | int signal; 86 | int descriptor; 87 | } Watcher; 88 | #elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \ 89 | defined(__APPLE__) || defined(__DragonFly__) 90 | typedef struct { 91 | char directory[PATH_MAX]; 92 | pthread_t thread; 93 | int fd; 94 | int signal; 95 | struct kevent change; 96 | int kq; 97 | } Watcher; 98 | #endif 99 | 100 | typedef struct { 101 | char path[PATH_MAX]; 102 | Entry *entries; 103 | int entry_count; 104 | int start_index; 105 | int current_index; 106 | Watcher watcher; 107 | char search_term[NAME_MAX]; 108 | int *matched_indices; 109 | int matched_count; 110 | int current_match; 111 | int offset; 112 | } Pane; 113 | 114 | typedef union { 115 | int i; 116 | const void *v; 117 | } Arg; 118 | 119 | typedef struct { 120 | const uint32_t k; 121 | void (*func)(const Arg *); 122 | const Arg arg; 123 | } Key; 124 | 125 | typedef struct { 126 | const char **ext; 127 | size_t exlen; 128 | const void *v; 129 | size_t vlen; 130 | int wait_exec; 131 | } Rule; 132 | 133 | typedef struct { 134 | char **cmdv; 135 | size_t cmdc; 136 | char **argv; 137 | size_t argc; 138 | int wait_exec; 139 | } Command; 140 | 141 | enum { Left, Right }; /* panes */ 142 | enum { Wait = 0, DontWait = WNOHANG }; /* spawn forks */ 143 | enum { NormalMode, VisualMode, SearchMode }; 144 | enum { DontSelect, Select, InvertSelection }; 145 | enum { NextMatch, PrevMatch }; /* search */ 146 | 147 | /* function declarations */ 148 | static void log_to_file(const char *, int, const char *, ...); /* DELETE */ 149 | 150 | static void init_term(void); 151 | static void enable_raw_mode(void); 152 | static void get_term_size(void); 153 | static void get_env(void); 154 | static int start_signal(void); 155 | static void sighandler(int); 156 | static void set_panes(void); 157 | static void set_pane_entries(Pane *); 158 | static int should_skip_entry(const struct dirent *); 159 | static void get_fullpath(char *, const char *, const char *); 160 | static int get_selected_paths(Pane *, char **); 161 | static int entry_compare(const void *const, const void *const); 162 | static void update_screen(void); 163 | static void disable_raw_mode(void); 164 | static void append_entries(Pane *); 165 | static void handle_keypress(char); 166 | static void grabkeys(uint32_t, Key *, size_t); 167 | static void print_status(ColorPair, const char *, ...); 168 | static void display_entry_details(void); 169 | static void set_entry_color(Entry *); 170 | static void get_entry_datetime(char *, time_t); 171 | static void get_entry_permission(char *, mode_t); 172 | static void get_file_size(char *, off_t); 173 | static void get_entry_owner(char *, uid_t); 174 | static void get_entry_group(char *, gid_t); 175 | static int get_user_input(char *, size_t, const char *, ...); 176 | static int check_dir(char *); 177 | static void open_file(char *); 178 | static char *get_file_extension(const char *); 179 | static int check_rule(const char *); 180 | static void spawn(Command *); 181 | static int execute_command(Command *); 182 | static void termb_append(const char *, size_t); 183 | static void termb_write(void); 184 | static void write_entries_name(void); 185 | 186 | static void filesystem_event_init(void); 187 | static void *event_handler(void *); 188 | static void add_watch(Pane *); 189 | static void remove_watch(Pane *); 190 | static void cleanup_filesystem_events(void); 191 | static void update_search_highlight(const char *); 192 | static void cancel_search_highlight(void); 193 | 194 | static void termb_resize(void); 195 | 196 | static void cd_to_parent(const Arg *); 197 | static void create_new_file(const Arg *); 198 | static void create_new_dir(const Arg *); 199 | static void copy_entries(const Arg *); 200 | static void delete_entry(const Arg *); 201 | static void move_bottom(const Arg *); 202 | static void move_cursor(const Arg *); 203 | static void move_top(const Arg *); 204 | static void move_entries(const Arg *); 205 | static void open_entry(const Arg *); 206 | static void paste_entries(const Arg *); 207 | static void switch_pane(const Arg *); 208 | static void select_cur_entry(const Arg *); 209 | 210 | static void select_entry(Entry *, int); 211 | 212 | static void refresh(const Arg *); 213 | static void toggle_dotfiles(const Arg *); 214 | static void die(const char *, ...); 215 | static void *ecalloc(size_t, size_t); 216 | static void *erealloc(void *, size_t); 217 | static void quit(const Arg *); 218 | 219 | static void visual_mode(const Arg *); 220 | static void select_all(const Arg *); 221 | static void normal_mode(const Arg *); 222 | static void start_search(const Arg *); 223 | static void move_to_match(const Arg *); 224 | 225 | #endif // SFM_H 226 | -------------------------------------------------------------------------------- /sfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afify/sfm/f1f1197142421d3f727dc109a5910129d0bcb0b0/sfm.png --------------------------------------------------------------------------------