├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── ctpvclear ├── deptable ├── insert.awk ├── list.awk ├── markdown.sed └── roff.sed ├── doc ├── ctpv.1 └── showcase.gif ├── embed ├── Makefile └── embed.c ├── genhelp.sh ├── help.txt ├── previews.h ├── quit ├── Makefile └── ctpvquit.c ├── sh ├── clear.sh ├── end.sh ├── helpers.sh └── prev │ ├── any.sh │ ├── atool.sh │ ├── audio.sh │ ├── bat.sh │ ├── cat.sh │ ├── colordiff.sh │ ├── delta.sh │ ├── diff_so_fancy.sh │ ├── elinks.sh │ ├── font.sh │ ├── glow.sh │ ├── gpg.sh │ ├── highlight.sh │ ├── image.sh │ ├── jq.sh │ ├── libreoffice.sh │ ├── ls.sh │ ├── lynx.sh │ ├── mdcat.sh │ ├── pdf.sh │ ├── source_highlight.sh │ ├── svg.sh │ ├── symlink.sh │ ├── torrent.sh │ ├── video.sh │ └── w3m.sh ├── src ├── attrs.h ├── config.c ├── config.h ├── ctpv.c ├── ctpv.h ├── error.c ├── error.h ├── lexer.c ├── lexer.h ├── preview.c ├── preview.h ├── result.h ├── server.c ├── server.h ├── shell.c ├── shell.h ├── ulist.c ├── ulist.h ├── utils.c ├── utils.h ├── vector.c └── vector.h └── version.h /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.d 3 | /gen/ 4 | /ctpv 5 | /embed/embed 6 | /quit/ctpvquit 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nikita Ivanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX := /usr/local 2 | BINPREFIX := $(DESTDIR)$(PREFIX)/bin 3 | MANPREFIX := $(DESTDIR)$(PREFIX)/share/man/man1 4 | 5 | SRC := $(wildcard src/*.c) 6 | OBJ := $(SRC:.c=.o) 7 | DEP := $(OBJ:.o=.d) 8 | PRE := $(wildcard sh/prev/*) 9 | GEN := gen/help.h gen/previews.h gen/server.h gen/helpers.h 10 | 11 | LIBS := magic crypto 12 | 13 | ALL_CFLAGS := -O2 -MMD -Wall -Wextra -Wno-unused-parameter $(CFLAGS) $(CPPFLAGS) 14 | ALL_LDFLAGS := $(addprefix -l,$(LIBS)) $(CFLAGS) $(LDFLAGS) 15 | 16 | MKDIR := mkdir -p 17 | INSTALL := install 18 | INSTALL_EXE := $(INSTALL) 19 | INSTALL_DATA := $(INSTALL) -m 0644 20 | 21 | all: ctpv 22 | 23 | options: 24 | @echo "CC = $(CC)" 25 | @echo "CFLAGS = $(ALL_CFLAGS)" 26 | @echo "LDFLAGS = $(ALL_LDFLAGS)" 27 | 28 | install: install.bin install.man 29 | 30 | install.bin: ctpv quit/ctpvquit ctpvclear 31 | $(MKDIR) $(BINPREFIX) 32 | $(INSTALL_EXE) $^ $(BINPREFIX) 33 | 34 | install.man: doc/ctpv.1 35 | $(MKDIR) $(MANPREFIX) 36 | $(INSTALL_DATA) $^ $(MANPREFIX) 37 | 38 | uninstall: 39 | $(RM) $(BINPREFIX)/ctpv $(BINPREFIX)/ctpvquit $(BINPREFIX)/ctpvclear \ 40 | $(MANPREFIX)/ctpv.1 41 | 42 | clean: 43 | $(RM) ctpv $(OBJ) $(DEP) $(GEN) 44 | $(MAKE) -C embed clean 45 | $(MAKE) -C quit clean 46 | 47 | docs: README.md doc/ctpv.1 48 | deptable/list.awk $(PRE) | deptable/markdown.sed | deptable/insert.awk README.md 49 | deptable/list.awk $(PRE) | deptable/roff.sed | deptable/insert.awk doc/ctpv.1 50 | ./genhelp.sh doc/ctpv.1 > help.txt 51 | 52 | ctpv: $(OBJ) 53 | $(CC) -o $@ $+ $(ALL_LDFLAGS) 54 | 55 | .c.o: 56 | $(CC) -o $@ $< -c $(ALL_CFLAGS) 57 | 58 | # Exclicit rules for generated header files 59 | src/ctpv.o: gen/previews.h gen/help.h 60 | src/shell.o: gen/helpers.h 61 | src/server.o: gen/server.h 62 | 63 | gen/help.h: help.txt embed/embed 64 | embed/embed help.txt > $@ 65 | 66 | gen/previews.h: $(PRE) embed/embed 67 | embed/embed -p prev_scr_ $(PRE) > $@ 68 | 69 | gen/server.h: sh/clear.sh sh/end.sh embed/embed 70 | embed/embed -p scr_ sh/clear.sh sh/end.sh > $@ 71 | 72 | gen/helpers.h: sh/helpers.sh embed/embed 73 | embed/embed -p scr_ sh/helpers.sh > $@ 74 | 75 | $(GEN): | gen 76 | 77 | gen: 78 | $(MKDIR) $@ 79 | 80 | embed/embed: .force 81 | $(MAKE) -C embed 82 | 83 | quit/ctpvquit: .force 84 | $(MAKE) -C quit 85 | 86 | -include $(DEP) 87 | 88 | .PHONY: all options install install.bin install.man uninstall \ 89 | clean docs .force 90 | 91 | .DELETE_ON_ERROR: 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ctpv 2 | 3 | File previewer for a terminal 4 | 5 | ![showcase](doc/showcase.gif) 6 | 7 | ---- 8 | 9 | ctpv is a file previewer utility for a terminal. 10 | 11 | It was made with integration into [lf file manager][lf] in mind, 12 | but I believe that it can be easily integrated into other programs 13 | as well. 14 | 15 | It supports previews for source code, archives, PDF files, images, 16 | videos, etc. 17 | See [Previews](#previews) for more info. 18 | 19 | Image previews are powered by one of these programs: 20 | 21 | * [Überzug][ueberzug] (X11 only) 22 | * [Chafa][chafa] (X11 and Wayland) 23 | * [Kitty terminal][kitty] 24 | 25 | ctpv is a remake of an awesome program named 26 | [stpv](https://github.com/Naheel-Azawy/stpv). 27 | stpv did everything I wanted, except it was a bit sluggish because 28 | it was written in POSIX shell. 29 | ctpv is written in C and is an attempt to make a faster version of 30 | stpv with a few new features. 31 | 32 | ## Previews 33 | 34 | Previewing each file type requires specific programs installed on 35 | a system. 36 | If a program is not found on the system, ctpv 37 | will try to use another one. 38 | Only one program is required for each file type. 39 | For example, you only need either `elinks`, `lynx` or 40 | `w3m` installed on your system to view HTML files. 41 | 42 | 43 | 44 | | File types | Programs | 45 | | ---- | ---- | 46 | | any | [exiftool][exiftool] cat | 47 | | archive | [atool][atool] | 48 | | audio | [ffmpegthumbnailer][ffmpegthumbnailer] [ffmpeg][ffmpeg] | 49 | | diff | [colordiff][colordiff] [delta][delta] [diff-so-fancy][diff-so-fancy] | 50 | | directory | ls | 51 | | font | fontimage | 52 | | gpg-encrypted | [gpg][gpg] | 53 | | html | [elinks][elinks] [lynx][lynx] [w3m][w3m] | 54 | | image | [ueberzug][ueberzug] [chafa][chafa] | 55 | | json | [jq][jq] | 56 | | markdown | [glow][glow] [mdcat][mdcat] | 57 | | office | [libreoffice][libreoffice] | 58 | | pdf | pdftoppm | 59 | | svg | convert | 60 | | text | bat cat [highlight][highlight] [source-highlight][source-highlight] | 61 | | torrent | transmission-show | 62 | | video | [ffmpegthumbnailer][ffmpegthumbnailer] | 63 | 64 | [ffmpegthumbnailer]: https://github.com/dirkvdb/ffmpegthumbnailer 65 | [w3m]: https://w3m.sourceforge.net/ 66 | [elinks]: http://elinks.cz/ 67 | [fontforge]: https://fontforge.org 68 | [exiftool]: https://github.com/exiftool/exiftool 69 | [highlight]: https://gitlab.com/saalen/highlight 70 | [chafa]: https://github.com/hpjansson/chafa 71 | [gpg]: https://www.gnupg.org/ 72 | [transmission]: https://transmissionbt.com/ 73 | [delta]: https://github.com/dandavison/delta 74 | [colordiff]: https://www.colordiff.org/ 75 | [source-highlight]: https://www.gnu.org/software/src-highlite/ 76 | [ueberzug]: https://github.com/seebye/ueberzug 77 | [mdcat]: https://github.com/swsnr/mdcat 78 | [glow]: https://github.com/charmbracelet/glow 79 | [atool]: https://www.nongnu.org/atool/ 80 | [lynx]: https://github.com/jpanther/lynx 81 | [libreoffice]: https://www.libreoffice.org/ 82 | [diff-so-fancy]: https://github.com/so-fancy/diff-so-fancy 83 | [imagemagick]: https://imagemagick.org/ 84 | [poppler]: https://poppler.freedesktop.org/ 85 | [jq]: https://github.com/jqlang/jq 86 | [ffmpeg]: https://ffmpeg.org/ 87 | 88 | 89 | 90 | ## Installation 91 | 92 | ### Manual 93 | 94 | If you are building from source, make sure to install these libraries! 95 | Depending on your system, you probably will also need "devel" versions 96 | of the same libraries. 97 | 98 | * `libcrypto` 99 | * `libmagic` 100 | 101 | Install: 102 | 103 | ```console 104 | git clone https://github.com/NikitaIvanovV/ctpv 105 | cd ctpv 106 | make 107 | sudo make install 108 | ``` 109 | 110 | Uninstall: 111 | 112 | ```console 113 | sudo make uninstall 114 | ``` 115 | 116 | ### AUR 117 | 118 | If you are an Arch Linux user, you can install 119 | [`ctpv-git`](https://aur.archlinux.org/packages/ctpv-git) 120 | AUR package. 121 | 122 | ```console 123 | yay -S ctpv-git 124 | ``` 125 | 126 | ### MacPorts 127 | 128 | With MacPorts, you can install the 129 | [`ctpv`](https://ports.macports.org/port/ctpv) 130 | package. 131 | 132 | ```console 133 | sudo port install ctpv 134 | ``` 135 | 136 | ### Homebrew 137 | 138 | With Homebrew, you can install the 139 | [`ctpv`](https://formulae.brew.sh/formula/ctpv) 140 | package. 141 | 142 | ```console 143 | brew install ctpv 144 | ``` 145 | 146 | ### Nix 147 | 148 | #### Nix package 149 | 150 | ```console 151 | nix-env -ivf cptv 152 | nix profile install nixpkgs#cptv # with flakes enabled 153 | ``` 154 | 155 | #### NixOS and HomeManager 156 | 157 | If you don't need to call it directly and 158 | just want to use it through lf: 159 | 160 | ```nix 161 | programs.lf = { 162 | previewer = { 163 | keybinding = "i"; 164 | source = "${pkgs.ctpv}/bin/ctpv"; 165 | }; 166 | extraConfig = '' 167 | &${pkgs.ctpv}/bin/ctpv -s $id 168 | cmd on-quit %${pkgs.ctpv}/bin/ctpv -e $id 169 | set cleaner ${pkgs.ctpv}/bin/ctpvclear 170 | ''; 171 | } 172 | ``` 173 | 174 | ### Gentoo 175 | Add this 176 | [ctpv-9999.ebuild](https://github.com/Sneethe/sneethe-overlay/blob/main/app-misc/ctpv/ctpv-9999.ebuild) 177 | to your own 178 | [repository](https://wiki.gentoo.org/wiki/Creating_an_ebuild_repository). 179 | 180 | Or alternatively: 181 | 182 | ```console 183 | eselect repository add sneethe-overlay git https://github.com/Sneethe/sneethe-overlay.git 184 | emaint sync --repo sneethe-overlay 185 | emerge --ask --verbose app-misc/ctpv 186 | ``` 187 | 188 | ## Integration 189 | 190 | ### lf file manager 191 | 192 | Add these lines to your lf config 193 | (usually located at `~/.config/lf/lfrc`). 194 | 195 | ``` 196 | set previewer ctpv 197 | set cleaner ctpvclear 198 | &ctpv -s $id 199 | &ctpvquit $id 200 | ``` 201 | 202 | #### Wayland 203 | 204 | If you use Wayland, follow these steps: 205 | 206 | * Make sure you use one of the [terminals that support sixel][sixel] 207 | * Install [this fork of lf][lf-sixel] 208 | * Install [Chafa][chafa] 209 | * Add `set chafasixel` to `~/.config/ctpv/config` 210 | 211 | As of 2023-03-19, original lf does not support sixel protocol, 212 | which is why you need use the fork. 213 | 214 | ## Documentation 215 | 216 | Full documentation on command line options, 217 | configuration and how to define custom previews can be found here: 218 | 219 | 220 | [ueberzug]: https://github.com/seebye/ueberzug 221 | [kitty]: https://github.com/kovidgoyal/kitty 222 | [chafa]: https://github.com/hpjansson/chafa 223 | [lf]: https://github.com/gokcehan/lf 224 | [lf-sixel]: https://github.com/horriblename/lf 225 | [sixel]: https://www.arewesixelyet.com 226 | -------------------------------------------------------------------------------- /ctpvclear: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -z "$id" ] && id="$1" 4 | 5 | exec ctpv -c "$id" 6 | -------------------------------------------------------------------------------- /deptable/insert.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/gawk -f 2 | 3 | @include "inplace" 4 | 5 | BEGIN { 6 | while ((getline line < "-") > 0) 7 | input = input line "\n" 8 | close("-") 9 | } 10 | 11 | /TABLEEND/ { 12 | table = 0 13 | printf "%s", input 14 | } 15 | 16 | !table 17 | 18 | /TABLESTART/ { 19 | table = 1 20 | } 21 | -------------------------------------------------------------------------------- /deptable/list.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/gawk -f 2 | 3 | function perr(str) { 4 | printf "%s: %s\n", ARGV[0], str > "/dev/stderr" 5 | } 6 | 7 | function join(arr, sep, res, s) { 8 | for (i in arr) { 9 | res = res s arr[i] 10 | if (s == "") 11 | s = sep 12 | } 13 | 14 | return res 15 | } 16 | 17 | function process_file(file, i, line, arr, t, p, p_len) { 18 | i = getline line < file 19 | 20 | if (i == -1) { 21 | perr(ERRNO ": " file) 22 | exit 23 | } else if (i == 0) { 24 | return 25 | } 26 | 27 | if (match(line, /^#\s*([a-zA-Z0-9_-]+):\s*(.*)/, arr) == 0) 28 | return 29 | 30 | t = arr[1] 31 | p_len = split(arr[2], p, /\s+/) 32 | for (i = 1; i <= p_len; i++) { 33 | types[t][types_len[t]++] = LINKS[p[i]] ? sprintf("[%s][%s]", p[i], p[i]) : p[i] 34 | } 35 | } 36 | 37 | BEGIN { 38 | LINKS["exiftool"] = "https://github.com/exiftool/exiftool" 39 | LINKS["atool"] = "https://www.nongnu.org/atool/" 40 | LINKS["ffmpegthumbnailer"] = "https://github.com/dirkvdb/ffmpegthumbnailer" 41 | LINKS["ffmpeg"] = "https://ffmpeg.org/" 42 | LINKS["colordiff"] = "https://www.colordiff.org/" 43 | LINKS["delta"] = "https://github.com/dandavison/delta" 44 | LINKS["diff-so-fancy"] = "https://github.com/so-fancy/diff-so-fancy" 45 | LINKS["fontforge"] = "https://fontforge.org" 46 | LINKS["gpg"] = "https://www.gnupg.org/" 47 | LINKS["libreoffice"] = "https://www.libreoffice.org/" 48 | LINKS["elinks"] = "http://elinks.cz/" 49 | LINKS["lynx"] = "https://github.com/jpanther/lynx" 50 | LINKS["w3m"] = "https://w3m.sourceforge.net/" 51 | LINKS["ueberzug"] = "https://github.com/seebye/ueberzug" 52 | LINKS["chafa"] = "https://github.com/hpjansson/chafa" 53 | LINKS["jq"] = "https://github.com/jqlang/jq" 54 | LINKS["glow"] = "https://github.com/charmbracelet/glow" 55 | LINKS["mdcat"] = "https://github.com/swsnr/mdcat" 56 | LINKS["poppler"] = "https://poppler.freedesktop.org/" 57 | LINKS["imagemagick"] = "https://imagemagick.org/" 58 | LINKS["highlight"] = "https://gitlab.com/saalen/highlight" 59 | LINKS["source-highlight"] = "https://www.gnu.org/software/src-highlite/" 60 | LINKS["transmission"] = "https://transmissionbt.com/" 61 | 62 | for (i = 1; i < ARGC; i++) 63 | process_file(ARGV[i]) 64 | 65 | for (t in types) 66 | s = s sprintf("%s\t%s\n", t, join(types[t], " ")) 67 | 68 | printf "%s", s | "sort" 69 | close("sort") 70 | 71 | print "" 72 | for (k in LINKS) { 73 | if (!LINKS[k]) 74 | continue 75 | printf ">\t%s\t%s\n", k, LINKS[k] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /deptable/markdown.sed: -------------------------------------------------------------------------------- 1 | #!/bin/sed -f 2 | 3 | # Add header 4 | 1i\ 5 | | File types | Programs |\ 6 | | ---- | ---- | 7 | 8 | # Format links 9 | /^>/ { 10 | s/>\t/[/ 11 | s/\t/]: / 12 | be 13 | } 14 | 15 | # Format rows 16 | /^$/! { 17 | s/ / /g 18 | s/\t/ | / 19 | s/^/| / 20 | s/$/ |/ 21 | } 22 | 23 | :e 24 | 25 | # Add a newline at the end 26 | $a\ 27 | 28 | -------------------------------------------------------------------------------- /deptable/roff.sed: -------------------------------------------------------------------------------- 1 | #!/bin/sed -f 2 | 3 | # Add header 4 | 1i\ 5 | .TS\ 6 | allbox;\ 7 | lb lb\ 8 | l li .\ 9 | File type\tPrograms 10 | 11 | # Format rows 12 | /^$/! { 13 | # Remove links 14 | :a 15 | s/\[\(\S*\)\]\[\S*\]/\1/ 16 | ta 17 | 18 | # Substitute '-' with '\-' 19 | :b 20 | s/\(\t.*[^\\]\)-/\1\\-/ 21 | tb 22 | 23 | # Add data block to enable line wrapping 24 | s/\t/&T{\n/; s/$/\nT}/ 25 | } 26 | 27 | # Add footer 28 | $a\ 29 | .TE 30 | 31 | # Delete links 32 | /^>/d 33 | 34 | # Delete empty lines 35 | /^$/d 36 | -------------------------------------------------------------------------------- /doc/ctpv.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .ds ub \(:Uberzug 3 | . 4 | .TH CTPV 1 2022 Linux "User's Reference Manuals" 5 | . 6 | .SH NAME 7 | ctpv \- terminal previewer 8 | . 9 | .SH SYNOPSIS 10 | . 11 | .SY ctpv 12 | .RI [ \-d ] 13 | .I file 14 | .RI [ w ] 15 | .RI [ h ] 16 | .RI [ x ] 17 | .RI [ y ] 18 | .RI [ id ] 19 | .YS 20 | . 21 | .SY ctpv 22 | .B \-h 23 | .YS 24 | . 25 | .SY ctpv 26 | .B \-l 27 | .YS 28 | . 29 | .SY ctpv 30 | .B \-m 31 | .I file 32 | \&.\|.\|.\& 33 | .YS 34 | . 35 | .SY ctpv 36 | .RB { \-s | \-c | \-e } 37 | .I id 38 | .YS 39 | . 40 | .SH DESCRIPTION 41 | . 42 | .B ctpv 43 | is a previewer utility made for integration into other programs like 44 | .IR lf . 45 | . 46 | .PP 47 | When 48 | .B ctpv 49 | is given a 50 | .IR file , 51 | it determines an appropriate preview for it and runs it. 52 | Depending on the preview and installed programs, it can print 53 | some text to standard output or display an image. 54 | Symbolic links are dereferenced. 55 | . 56 | .SS Built-in previews and dependencies 57 | . 58 | Previewing each file type requires specific programs. 59 | If a program is not found on the system, 60 | .B ctpv 61 | will try to use another one. 62 | Only one program is required for each file type. 63 | For example, you only need either 64 | .IR elinks , 65 | .I lynx 66 | or 67 | .I w3m 68 | installed on your system to view HTML files. 69 | . 70 | .PP 71 | . 72 | .\" This table is auto generated! 73 | . 74 | .\" TABLESTART 75 | .TS 76 | allbox; 77 | lb lb 78 | l li . 79 | File type Programs 80 | any T{ 81 | exiftool cat 82 | T} 83 | archive T{ 84 | atool 85 | T} 86 | audio T{ 87 | ffmpegthumbnailer ffmpeg 88 | T} 89 | diff T{ 90 | colordiff delta diff\-so\-fancy 91 | T} 92 | directory T{ 93 | ls 94 | T} 95 | font T{ 96 | fontimage 97 | T} 98 | gpg-encrypted T{ 99 | gpg 100 | T} 101 | html T{ 102 | elinks lynx w3m 103 | T} 104 | image T{ 105 | ueberzug chafa 106 | T} 107 | json T{ 108 | jq 109 | T} 110 | markdown T{ 111 | glow mdcat 112 | T} 113 | office T{ 114 | libreoffice 115 | T} 116 | pdf T{ 117 | pdftoppm 118 | T} 119 | svg T{ 120 | convert 121 | T} 122 | text T{ 123 | bat cat highlight source\-highlight 124 | T} 125 | torrent T{ 126 | transmission\-show 127 | T} 128 | video T{ 129 | ffmpegthumbnailer 130 | T} 131 | .TE 132 | .\" TABLEEND 133 | . 134 | .SS Integration 135 | . 136 | .TP 137 | .IR lf \~\c 138 | file manager 139 | Add this snippet to 140 | .I lf 141 | configuration file (usually located at 142 | .IR \(ti/.config/lf/lfrc ). 143 | . 144 | .RS 145 | .IP 146 | .EX 147 | set previewer ctpv 148 | set cleaner ctpvclear 149 | &ctpv \-s $id 150 | &ctpvquit $id 151 | .EE 152 | .RE 153 | . 154 | .SS Image previews 155 | . 156 | Image previews are enabled by either installing 157 | .I \*(ub 158 | or 159 | .I Chafa 160 | (as seen in 161 | .IR "Built-in previews and dependencies" ) 162 | or using built-in image preview functionality of 163 | .I Kitty 164 | terminal. 165 | . 166 | .PP 167 | When possible, 168 | .B ctpv 169 | will prefer 170 | .I \*(ub 171 | over others image preview methods. You can override this 172 | behavior by using 173 | .B forcekitty 174 | or 175 | .B forcechafa 176 | options (see 177 | .IR "Preview options" ). 178 | . 179 | .PP 180 | .I \*(ub 181 | supports 182 | .I X11 183 | only, so 184 | .I Wayland 185 | is not supported. 186 | To enable high-resolution image previews on 187 | .IR Wayland , 188 | you need to follow these steps: 189 | . 190 | .IP \(bu 4 191 | Make sure you use one of the 192 | .UR https://\:www\:.arewesixelyet\:.com 193 | terminals that support sixel 194 | .UE 195 | .IP \(bu 4 196 | Install 197 | .UR https://\:github\:.com/\:horriblename/\:lf 198 | .I lf-sixel 199 | .UE . 200 | . 201 | .IP \(bu 202 | Install 203 | .I Chafa 204 | . 205 | .IP \(bu 206 | Add 207 | .B chafasixel 208 | configuration option (see 209 | .IR "Preview options" ) 210 | . 211 | .PP 212 | The fork is required because, as of 2023-03-19, 213 | the original lf does not support sixel protocol. 214 | . 215 | .SS How previews are selected 216 | . 217 | Initially, 218 | .B ctpv 219 | retrieves MIME type and extension from 220 | .I file 221 | passed as the first argument (MIME type is extracted using 222 | .BR libmagic (3)). 223 | . 224 | .PP 225 | Then it creates a list of all previews respecting user 226 | configuration in a special order, where previews that are 227 | more specific appear at the top and more generic ones at the bottom. 228 | The list can be viewed by using 229 | .B \-l 230 | option. The order can be changed (see 231 | .IR "Setting priority" ). 232 | . 233 | .PP 234 | Finally, 235 | .B ctpv 236 | goes through the list starting with the first element 237 | and checks if a preview matches 238 | .IR file 's 239 | extension and MIME type. 240 | If it does, it runs a preview script. 241 | If the script exits with status 127 242 | (which usually means that a program that is necessary for generating 243 | a preview content is not installed on the system), 244 | .B ctpv 245 | attempts to run another appropriate preview and so on. 246 | If the script exists with 0 or a any other status, both standard output 247 | and standard error are printed. 248 | . 249 | .SH OPTIONS 250 | . 251 | .TP 252 | .B \-h 253 | Print help message. 254 | . 255 | .TP 256 | .B \-l 257 | List all previews. 258 | . 259 | .TP 260 | .BR \-m \~\c 261 | .IR file \~.\|.\|.\& 262 | Print extension and MIME type of 263 | .IR file . 264 | . 265 | .TP 266 | .BR \-s \~\c 267 | .I id 268 | Start server with ID 269 | .IR id . 270 | . 271 | .TP 272 | .BR \-c \~\c 273 | .I id 274 | Send 275 | .B clear 276 | command to server with ID 277 | .I id 278 | (usually, it removes image from terminal). 279 | . 280 | .TP 281 | .BR \-e \~\c 282 | .I id 283 | Kill server with ID 284 | .IR id . 285 | . 286 | .TP 287 | .B \-d 288 | Enable debug messages. 289 | . 290 | .SH CONFIGURATION 291 | . 292 | .B ctpv 293 | uses a configuration file usually located at 294 | .I \(ti/.config/ctpv/config 295 | (see 296 | .IR FILES ). 297 | Its format somewhat resembles one used by 298 | .IR lf . 299 | There are several commands that can be used to add 300 | previews or set different settings. 301 | Commands are separated by newlines. 302 | Comments start with number sign 303 | .RB \(oq # \(cq. 304 | . 305 | .PP 306 | Example: 307 | . 308 | .IP 309 | .EX 310 | # Set some options 311 | set forcekitty 312 | set shell "/usr/bin/bash" 313 | .sp 314 | # Add a new preview 315 | preview cow .moo {{ 316 | \& cowsay < "$f" 317 | }} 318 | .sp 319 | # Remove some previews 320 | remove w3m 321 | remove lynx 322 | remove elinks 323 | .EE 324 | . 325 | .SS Preview options 326 | . 327 | An option can be set using 328 | .B set 329 | command. 330 | . 331 | .TP 332 | .BR shell \~\c 333 | .RI \(dq path \(dq 334 | Use 335 | .I path 336 | as a path to a shell to run previews with. 337 | Use it if you have a non-POSIX compliant shell installed as a default shell. 338 | The setting defaults to 339 | .BR /bin/sh . 340 | . 341 | .TP 342 | .B forcekitty 343 | Always use 344 | .I Kitty 345 | terminal's built-in method of previewing images. 346 | . 347 | .TP 348 | .B forcekittyanim 349 | Always use 350 | .I Kitty 351 | terminal's built-in method of previewing images for animated 352 | images. 353 | . 354 | .TP 355 | .B forcechafa 356 | Always use 357 | .I Chafa 358 | for image previews. 359 | . 360 | .TP 361 | .B noimages 362 | Print only text and do not use any image previewing method. 363 | . 364 | .TP 365 | .B nosymlinkinfo 366 | Do not print resolved path of symbolic links. 367 | . 368 | .TP 369 | .B chafasixel 370 | Set output format of 371 | .I Chafa 372 | to \(lqsixels\(rq instead of \(lqsymbols\(rq. 373 | Use it if your file manager and terminal are capable of properly displaying 374 | sixel data. 375 | . 376 | .TP 377 | .B showgpg 378 | Preview 379 | .BR gpg (1) 380 | encrypted files. 381 | Filename must have \(lq.gpg\(rq extension. 382 | . 383 | .SS Defining custom previews 384 | . 385 | User-defined previews are added with 386 | .B preview 387 | command. 388 | . 389 | .PP 390 | An example below defines a new preview with name \(lqmanpage\(rq 391 | that applies to files with extension \(lq.1\(rq. 392 | A preview itself is a shell script enclosed within double curly 393 | braces. 394 | . 395 | .IP 396 | .EX 397 | preview manpage .1 {{ 398 | \& groff \-man \-tep \-Tutf8 \-rLL="${w}n" "${f}" | col \-x 399 | }} 400 | .EE 401 | . 402 | .PP 403 | Running 404 | .I "ctpv\~file.1" 405 | where 406 | .I file.1 407 | is a source code for a manpage will run 408 | .BR groff (1) 409 | to produce a formatted manpage like the one you are reading. 410 | . 411 | .PP 412 | Manpages filenames may also end with other extensions: 413 | \(lq.2\(rq, \(lq.3\(rq, \(lq.4\(rq and so on. 414 | It's possible to make user-defines previews apply to several 415 | file types at once: 416 | . 417 | .IP 418 | .EX 419 | preview manpage .1 .2 .3 .4 .5 .6 .7 .8 {{ 420 | \& # groff command 421 | }} 422 | .EE 423 | . 424 | .PP 425 | Variable 426 | .B $f 427 | stores 428 | .I file 429 | that was passed as a first argument to 430 | .BR ctpv . 431 | It's strongly suggested to enclose 432 | .B $f 433 | with double quotes 434 | .RB ( \(dq$f\(dq ) 435 | because otherwise the script will not work as 436 | expected if 437 | .B $f 438 | stores a filename with whitespace. 439 | . 440 | .PP 441 | There are other variables that are exported into preview 442 | script environment: 443 | .BR $w , 444 | .BR $h , 445 | .BR $x , 446 | .B $y 447 | and 448 | .BR $id . 449 | There are also 450 | .B $m 451 | and 452 | .B $e 453 | which store MIME type and extension of 454 | .IR file . 455 | . 456 | .PP 457 | You can specify MIME type instead of filename extension 458 | in preview definition: 459 | . 460 | .IP 461 | .EX 462 | preview json_example application/json {{ 463 | \& # preview json files 464 | }} 465 | .EE 466 | . 467 | .PP 468 | You can omit subtype part of the MIME type 469 | by replacing it with 470 | .RB \(oq * \(cq. 471 | . 472 | .IP 473 | .EX 474 | preview any_text_example text/* {{ 475 | \& # this one applies to all text files 476 | }} 477 | .EE 478 | . 479 | .PP 480 | Setting subtype to 481 | .RB \(oq * \(cq 482 | will make the preview above apply to any file which MIME type starts with 483 | .BR text/ . 484 | . 485 | .SS Setting priority 486 | . 487 | If there are several previews that apply to the same file type, 488 | only the top one in the list is chosen (see 489 | .IR "How previews are selected" ). 490 | To alter this behavior, you can use 491 | .B priority 492 | command to change preview priority: 493 | . 494 | .IP 495 | .EX 496 | priority cat 497 | .EE 498 | . 499 | .PP 500 | The snippet above sets priority of a built-in preview named \(lqcat\(rq 501 | to 1, thus now it's used for all text files. 502 | It's possible to specify an integer as the second argument 503 | to set priority other than 1 (may also be negative). 504 | . 505 | .SS Removing previews 506 | . 507 | .B remove 508 | command simply removes a preview (also works for built-in ones): 509 | . 510 | .IP 511 | .EX 512 | remove cat 513 | .EE 514 | . 515 | .PP 516 | It's useful if you have a program installed on your system but you 517 | don't want 518 | .B ctpv 519 | to use it for generating previews. 520 | . 521 | .SH ENVIRONMENT 522 | . 523 | .TP 524 | .I id 525 | .I id 526 | of a server to connect to 527 | (see 528 | .B \-s 529 | option). 530 | . 531 | .SH FILES 532 | . 533 | .TP 534 | .I $XDG_CONFIG_HOME/ctpv/config 535 | Configuration file. 536 | If 537 | .I $XDG_CONFIG_HOME 538 | is not set, defaults to 539 | .IR \(ti/.config . 540 | . 541 | .TP 542 | .I $XDG_CACHE_HOME/ctpv 543 | Directory to store cached image previews. 544 | It takes some time to generate an image preview for some file types, 545 | such as videos or PDF files, this is why the generated images are 546 | stored in the directory to be shown if the same file is previewed 547 | again. 548 | If 549 | .I $XDG_CACHE_HOME 550 | is not set, defaults to 551 | .IR \(ti/.cache . 552 | . 553 | .SH SEE ALSO 554 | . 555 | .BR lf (1) 556 | . 557 | .SH AUTHOR 558 | . 559 | Written by Nikita Ivanov. 560 | -------------------------------------------------------------------------------- /doc/showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikitaIvanovV/ctpv/4efa0f976eaf8cb814e0aba4f4f1a1d12ee9262e/doc/showcase.gif -------------------------------------------------------------------------------- /embed/Makefile: -------------------------------------------------------------------------------- 1 | embed: embed.c 2 | 3 | clean: 4 | $(RM) embed 5 | 6 | .PHONY: clean 7 | -------------------------------------------------------------------------------- /embed/embed.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | void getvarname(char *res, char *prefix, char *filename) 9 | { 10 | char *s = strrchr(filename, '/'); 11 | if (s) 12 | s++; 13 | else 14 | s = filename; 15 | 16 | if (prefix) { 17 | size_t prefix_len = strlen(prefix); 18 | memcpy(res, prefix, prefix_len); 19 | res += prefix_len; 20 | } 21 | 22 | int c, i = 0; 23 | for (; s[i] != 0; i++) { 24 | c = s[i]; 25 | if (!isalnum(c)) 26 | c = '_'; 27 | 28 | res[i] = c; 29 | } 30 | 31 | res[i] = '\0'; 32 | } 33 | 34 | void print_char(char c) 35 | { 36 | printf("0x%x, ", c); 37 | } 38 | 39 | void print_file(char *f) 40 | { 41 | int c; 42 | FILE *file = fopen(f, "r"); 43 | 44 | if (!file) { 45 | fprintf(stderr, "failed to open %s: %s\n", f, strerror(errno)); 46 | exit(EXIT_FAILURE); 47 | } 48 | 49 | while ((c = fgetc(file)) != EOF) 50 | print_char(c); 51 | 52 | fclose(file); 53 | } 54 | 55 | void embed_file(char *prefix, char *filename, char *helpers) 56 | { 57 | static char varname[FILENAME_MAX]; 58 | 59 | getvarname(varname, prefix, filename); 60 | 61 | printf("char %s[] = { ", varname); 62 | 63 | if (helpers) { 64 | print_file(helpers); 65 | print_char('\n'); 66 | } 67 | 68 | print_file(filename); 69 | 70 | puts("0 };"); 71 | } 72 | 73 | int main(int argc, char *argv[]) 74 | { 75 | char *prefix = NULL, *helpers = NULL; 76 | 77 | int c; 78 | while ((c = getopt(argc, argv, "p:h:")) != -1) { 79 | switch (c) { 80 | case 'p': 81 | prefix = optarg; 82 | break; 83 | case 'h': 84 | helpers = optarg; 85 | break; 86 | default: 87 | return EXIT_FAILURE; 88 | } 89 | } 90 | 91 | for (int i = optind; i < argc; i++) 92 | embed_file(prefix, argv[i], helpers); 93 | 94 | return EXIT_SUCCESS; 95 | } 96 | -------------------------------------------------------------------------------- /genhelp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export GROFF_NO_SGR=1 4 | 5 | groff -t -man -mtty-char "$1" -Tascii | 6 | col -bx | 7 | sed '/OPTIONS/,/^[A-Z]/!d; /^[A-Z]/d' 8 | -------------------------------------------------------------------------------- /help.txt: -------------------------------------------------------------------------------- 1 | -h Print help message. 2 | 3 | -l List all previews. 4 | 5 | -m file ... 6 | Print extension and MIME type of file. 7 | 8 | -s id Start server with ID id. 9 | 10 | -c id Send clear command to server with ID id (usually, it removes im- 11 | age from terminal). 12 | 13 | -e id Kill server with ID id. 14 | 15 | -d Enable debug messages. 16 | 17 | -------------------------------------------------------------------------------- /previews.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "src/utils.h" 5 | #include "src/preview.h" 6 | #include "gen/previews.h" 7 | 8 | /* 9 | * This file is supposed to be included in src/ctpv.c 10 | */ 11 | 12 | #define PNAME(n) prev_scr_##n##_sh 13 | #define PR(e, t, s, n, a) { #n, e, t, s, PNAME(n), 0, 0, a, LEN(PNAME(n)) } 14 | 15 | Preview b_previews[] = { 16 | PR(NULL, NULL, NULL, any, PREV_ATTR_NONE), 17 | 18 | PR("md", NULL, NULL, glow, PREV_ATTR_NONE), 19 | PR("md", NULL, NULL, mdcat, PREV_ATTR_NONE), 20 | 21 | 22 | PR("ods", NULL, NULL, libreoffice, PREV_ATTR_NONE), 23 | PR("fods", NULL, NULL, libreoffice, PREV_ATTR_NONE), 24 | PR("xls", NULL, NULL, libreoffice, PREV_ATTR_NONE), 25 | PR("xlsx", NULL, NULL, libreoffice, PREV_ATTR_NONE), 26 | PR("csv", NULL, NULL, libreoffice, PREV_ATTR_NONE), 27 | 28 | PR("odt", NULL, NULL, libreoffice, PREV_ATTR_NONE), 29 | PR("fodt", NULL, NULL, libreoffice, PREV_ATTR_NONE), 30 | PR("doc", NULL, NULL, libreoffice, PREV_ATTR_NONE), 31 | PR("docx", NULL, NULL, libreoffice, PREV_ATTR_NONE), 32 | 33 | PR("odp", NULL, NULL, libreoffice, PREV_ATTR_NONE), 34 | PR("fodp", NULL, NULL, libreoffice, PREV_ATTR_NONE), 35 | PR("ppt", NULL, NULL, libreoffice, PREV_ATTR_NONE), 36 | PR("pptx", NULL, NULL, libreoffice, PREV_ATTR_NONE), 37 | 38 | PR(NULL, "text", NULL, bat, PREV_ATTR_NONE), 39 | PR(NULL, "text", NULL, highlight, PREV_ATTR_NONE), 40 | PR(NULL, "text", NULL, source_highlight, PREV_ATTR_NONE), 41 | PR(NULL, "text", NULL, cat, PREV_ATTR_NONE), 42 | 43 | PR(NULL, "image", NULL, image, PREV_ATTR_NONE), 44 | PR(NULL, "image", "svg+xml", svg, PREV_ATTR_NONE), 45 | PR(NULL, "video", NULL, video, PREV_ATTR_NONE), 46 | PR(NULL, "audio", NULL, audio, PREV_ATTR_NONE), 47 | 48 | PR(NULL, "application", "pdf", pdf, PREV_ATTR_NONE), 49 | PR(NULL, "application", "json", jq, PREV_ATTR_NONE), 50 | 51 | PR(NULL, "inode", "directory", ls, PREV_ATTR_NONE), 52 | PR(NULL, "inode", "symlink", symlink, PREV_ATTR_NONE), 53 | 54 | PR(NULL, "text", "html", elinks, PREV_ATTR_NONE), 55 | PR(NULL, "text", "html", lynx, PREV_ATTR_NONE), 56 | PR(NULL, "text", "html", w3m, PREV_ATTR_NONE), 57 | 58 | PR(NULL, "text", "x-diff", delta, PREV_ATTR_NONE), 59 | PR(NULL, "text", "x-patch", delta, PREV_ATTR_NONE), 60 | PR(NULL, "text", "x-diff", diff_so_fancy, PREV_ATTR_NONE), 61 | PR(NULL, "text", "x-patch", diff_so_fancy, PREV_ATTR_NONE), 62 | PR(NULL, "text", "x-diff", colordiff, PREV_ATTR_NONE), 63 | PR(NULL, "text", "x-patch", colordiff, PREV_ATTR_NONE), 64 | 65 | PR(NULL, "font", NULL, font, PREV_ATTR_NONE), 66 | 67 | PR("tar.gz", NULL, NULL, atool, PREV_ATTR_NONE), 68 | PR("tgz", NULL, NULL, atool, PREV_ATTR_NONE), 69 | PR("tgz", NULL, NULL, atool, PREV_ATTR_NONE), 70 | PR("tar.gz", NULL, NULL, atool, PREV_ATTR_NONE), 71 | PR("tgz", NULL, NULL, atool, PREV_ATTR_NONE), 72 | PR("tar.bz", NULL, NULL, atool, PREV_ATTR_NONE), 73 | PR("tbz", NULL, NULL, atool, PREV_ATTR_NONE), 74 | PR("tar.bz2", NULL, NULL, atool, PREV_ATTR_NONE), 75 | PR("tbz2", NULL, NULL, atool, PREV_ATTR_NONE), 76 | PR("tar.Z", NULL, NULL, atool, PREV_ATTR_NONE), 77 | PR("tZ", NULL, NULL, atool, PREV_ATTR_NONE), 78 | PR("tar.lzo", NULL, NULL, atool, PREV_ATTR_NONE), 79 | PR("tzo", NULL, NULL, atool, PREV_ATTR_NONE), 80 | PR("tar.lz", NULL, NULL, atool, PREV_ATTR_NONE), 81 | PR("tlz", NULL, NULL, atool, PREV_ATTR_NONE), 82 | PR("tar.xz", NULL, NULL, atool, PREV_ATTR_NONE), 83 | PR("txz", NULL, NULL, atool, PREV_ATTR_NONE), 84 | PR("tar.7z", NULL, NULL, atool, PREV_ATTR_NONE), 85 | PR("t7z", NULL, NULL, atool, PREV_ATTR_NONE), 86 | PR("tar", NULL, NULL, atool, PREV_ATTR_NONE), 87 | PR("zip", NULL, NULL, atool, PREV_ATTR_NONE), 88 | PR("jar", NULL, NULL, atool, PREV_ATTR_NONE), 89 | PR("war", NULL, NULL, atool, PREV_ATTR_NONE), 90 | PR("rar", NULL, NULL, atool, PREV_ATTR_NONE), 91 | PR("lha", NULL, NULL, atool, PREV_ATTR_NONE), 92 | PR("lzh", NULL, NULL, atool, PREV_ATTR_NONE), 93 | PR("7z", NULL, NULL, atool, PREV_ATTR_NONE), 94 | PR("alz", NULL, NULL, atool, PREV_ATTR_NONE), 95 | PR("ace", NULL, NULL, atool, PREV_ATTR_NONE), 96 | PR("a", NULL, NULL, atool, PREV_ATTR_NONE), 97 | PR("arj", NULL, NULL, atool, PREV_ATTR_NONE), 98 | PR("arc", NULL, NULL, atool, PREV_ATTR_NONE), 99 | PR("rpm", NULL, NULL, atool, PREV_ATTR_NONE), 100 | PR("deb", NULL, NULL, atool, PREV_ATTR_NONE), 101 | PR("cab", NULL, NULL, atool, PREV_ATTR_NONE), 102 | PR("gz", NULL, NULL, atool, PREV_ATTR_NONE), 103 | PR("bz", NULL, NULL, atool, PREV_ATTR_NONE), 104 | PR("bz2", NULL, NULL, atool, PREV_ATTR_NONE), 105 | PR("Z", NULL, NULL, atool, PREV_ATTR_NONE), 106 | PR("lzma", NULL, NULL, atool, PREV_ATTR_NONE), 107 | PR("lzo", NULL, NULL, atool, PREV_ATTR_NONE), 108 | PR("lz", NULL, NULL, atool, PREV_ATTR_NONE), 109 | PR("xz", NULL, NULL, atool, PREV_ATTR_NONE), 110 | PR("rz", NULL, NULL, atool, PREV_ATTR_NONE), 111 | PR("lrz", NULL, NULL, atool, PREV_ATTR_NONE), 112 | PR("7z", NULL, NULL, atool, PREV_ATTR_NONE), 113 | PR("cpio", NULL, NULL, atool, PREV_ATTR_NONE), 114 | 115 | PR("torrent", NULL, NULL, torrent, PREV_ATTR_EXT_SHORT), 116 | PR("gpg", NULL, NULL, gpg, PREV_ATTR_EXT_SHORT), 117 | }; 118 | 119 | /* vim: set nowrap: */ 120 | -------------------------------------------------------------------------------- /quit/Makefile: -------------------------------------------------------------------------------- 1 | ctpvquit: ctpvquit.c 2 | 3 | clean: 4 | $(RM) ctpvquit 5 | 6 | .PHONY: clean 7 | -------------------------------------------------------------------------------- /quit/ctpvquit.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | if (argc <= 1) { 11 | fprintf(stderr, "id not given\n"); 12 | return EXIT_FAILURE; 13 | } 14 | 15 | char *endptr, *pid_s = argv[1]; 16 | 17 | errno = 0; 18 | long pid = strtol(pid_s, &endptr, 10); 19 | 20 | if (errno != 0) { 21 | perror("strtol"); 22 | return EXIT_FAILURE; 23 | } 24 | 25 | if (endptr == pid_s) { 26 | fprintf(stderr, "%s: invalid number\n", pid_s); 27 | return EXIT_FAILURE; 28 | } 29 | 30 | while (1) { 31 | sleep(1); 32 | 33 | if (kill(pid, 0) == -1) { 34 | if (errno != ESRCH) { 35 | perror("kill"); 36 | return EXIT_FAILURE; 37 | } 38 | 39 | execlp("ctpv", "ctpv", "-e", pid_s, NULL); 40 | perror("execlp"); 41 | break; 42 | } 43 | } 44 | 45 | return EXIT_FAILURE; 46 | } 47 | -------------------------------------------------------------------------------- /sh/clear.sh: -------------------------------------------------------------------------------- 1 | setup_image 1 2 | 3 | [ "$image_method" = "$image_method_ueberzug" ] && 4 | printf '{"action": "remove", "identifier": "preview"}\n' > "$fifo" 5 | 6 | is_kitty && kitty_clear 7 | -------------------------------------------------------------------------------- /sh/end.sh: -------------------------------------------------------------------------------- 1 | setup_image 1 2 | 3 | # tell ctpv server to exit 4 | printf '\0' > "$fifo" 5 | 6 | # Kill running icat 7 | icat_pid="$(kitty_icat_pid)" 8 | [ -e "$icat_pid" ] && pid="$(cat "$icat_pid")" && [ -e "/proc/$pid" ] && kill "$pid" 9 | 10 | # A dirty hack to fix lf issue where ctpv runs before quit 11 | if is_kitty; then 12 | kitty_clear & 13 | { sleep 1; kitty_clear; } & 14 | wait 15 | fi 16 | -------------------------------------------------------------------------------- /sh/helpers.sh: -------------------------------------------------------------------------------- 1 | image_method_ueberzug='U' 2 | image_method_kitty='K' 3 | image_method_chafa='C' 4 | 5 | echo_err() { 6 | echo "$@" >&2 7 | } 8 | 9 | exists() { 10 | command -v "$1" >/dev/null 11 | } 12 | 13 | check_exists() { 14 | exists "$@" || exit 127 15 | } 16 | 17 | noimages() { 18 | [ -n "$noimages" ] 19 | } 20 | 21 | autochafa() { 22 | [ -n "$autochafa" ] 23 | } 24 | 25 | chafasixel() { 26 | [ -n "$chafasixel" ] 27 | } 28 | 29 | is_kitty() { 30 | [ -n "$KITTY_PID" ] && return 0 31 | 32 | case "$TERM" in 33 | *-kitty) return 0 ;; 34 | *) return 1 ;; 35 | esac 36 | } 37 | 38 | kitty_clear() { 39 | kitty +kitten icat --clear --stdin no --silent --transfer-mode file < /dev/null > /dev/tty 40 | } 41 | 42 | fifo_open() { 43 | # https://unix.stackexchange.com/a/522940/183147 44 | dd oflag=nonblock conv=notrunc,nocreat count=0 of="$1" \ 45 | >/dev/null 2>/dev/null 46 | } 47 | 48 | set_image_method() { 49 | image_method= 50 | 51 | [ -n "$forcekitty" ] && is_kitty && { image_method="$image_method_kitty"; return 0; } 52 | [ -n "$forcekittyanim" ] && is_kitty && is_anim_image && { image_method="$image_method_kitty"; return 0; } 53 | [ -n "$forcechafa" ] && exists chafa && { image_method="$image_method_chafa"; return 0; } 54 | 55 | [ -n "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ] && exists ueberzug && 56 | [ -n "$fifo" ] && [ -e "$fifo" ] && 57 | { image_method="$image_method_ueberzug"; return 0; } 58 | 59 | is_kitty && { image_method="$image_method_kitty"; return 0; } 60 | 61 | exists chafa && { image_method="$image_method_chafa"; return 0; } 62 | } 63 | 64 | is_anim_image() { 65 | case "$m" in 66 | image/apng|image/gif|image/avif|image/webp) 67 | return 0 ;; 68 | *) 69 | return 1 ;; 70 | esac 71 | } 72 | 73 | chafa_run() { 74 | format='-f symbols' 75 | autochafa && format= 76 | chafasixel && format='-f sixels' 77 | chafa -s "${w}x${h}" $format "$1" | sed 's/#/\n#/g' 78 | } 79 | 80 | setup_fifo() { 81 | fifo_open "$fifo" || exit "${1:-127}" 82 | } 83 | 84 | setup_image() { 85 | set_image_method 86 | 87 | [ "$image_method" = "$image_method_ueberzug" ] && setup_fifo "$@" 88 | } 89 | 90 | kitty_icat_pid() { 91 | printf '/tmp/ctpvicat.%d' "$id" 92 | } 93 | 94 | send_image() { 95 | noimages && return 127 96 | 97 | case "$image_method" in 98 | "$image_method_ueberzug") 99 | path="$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')" 100 | printf '{ "action": "add", "identifier": "preview", "x": %d, "y": %d, "width": %d, "height": %d, "scaler": "contain", "scaling_position_x": 0.5, "scaling_position_y": 0.5, "path": "%s" }\n' "$x" "$y" "$w" "$h" "$path" > "$fifo" 101 | return 1 102 | ;; 103 | "$image_method_kitty") 104 | kitty +kitten icat --silent --stdin no --transfer-mode file \ 105 | --place "${w}x${h}@${x}x${y}" "$1" < /dev/null > /dev/tty 106 | printf '%d\n' "$!" > "$(kitty_icat_pid)" 107 | wait 108 | return 1 109 | ;; 110 | "$image_method_chafa") 111 | chafa_run "$1" 112 | ;; 113 | *) 114 | return 127 115 | ;; 116 | esac 117 | } 118 | 119 | convert_and_show_image() { 120 | noimages && return 127 121 | setup_image 122 | [ -n "$cache_valid" ] || "$@" || exit "$?" 123 | send_image "$cache_f" 124 | } 125 | -------------------------------------------------------------------------------- /sh/prev/any.sh: -------------------------------------------------------------------------------- 1 | # any: exiftool cat 2 | 3 | if exists exiftool; then 4 | exiftool -- "$f" || true 5 | else 6 | cat < "$f" 7 | fi 8 | -------------------------------------------------------------------------------- /sh/prev/atool.sh: -------------------------------------------------------------------------------- 1 | # archive: atool 2 | 3 | atool -l -- "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/audio.sh: -------------------------------------------------------------------------------- 1 | # audio: ffmpegthumbnailer ffmpeg 2 | 3 | audio() { 4 | ffmpegthumbnailer -i "$f" -s 0 -q 50% -t 10 -o "$cache_f" 2>/dev/null 5 | } 6 | 7 | if exists ffmpeg; then 8 | x="$(ffmpeg -hide_banner -i "$f" 2>&1)" 9 | printf '%s\n' "$x" 10 | y=$((y + $(printf '%s\n' "$x" | wc -l))) 11 | fi 12 | 13 | convert_and_show_image audio 14 | -------------------------------------------------------------------------------- /sh/prev/bat.sh: -------------------------------------------------------------------------------- 1 | # text: bat 2 | 3 | if exists bat; then 4 | batcmd=bat 5 | elif exists batcat; then 6 | batcmd=batcat 7 | else 8 | exit 127 9 | fi 10 | 11 | "$batcmd" \ 12 | --color always \ 13 | --style plain \ 14 | --paging never \ 15 | --terminal-width "$w" \ 16 | --wrap character \ 17 | -- "$f" 18 | -------------------------------------------------------------------------------- /sh/prev/cat.sh: -------------------------------------------------------------------------------- 1 | # text: cat 2 | 3 | cat < "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/colordiff.sh: -------------------------------------------------------------------------------- 1 | # diff: colordiff 2 | 3 | colordiff < "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/delta.sh: -------------------------------------------------------------------------------- 1 | # diff: delta 2 | 3 | delta < "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/diff_so_fancy.sh: -------------------------------------------------------------------------------- 1 | # diff: diff-so-fancy 2 | 3 | diff-so-fancy < "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/elinks.sh: -------------------------------------------------------------------------------- 1 | # html: elinks 2 | 3 | elinks \ 4 | -dump 1 -dump-width "$w" \ 5 | -no-references -no-numbering < "$f" 6 | -------------------------------------------------------------------------------- /sh/prev/font.sh: -------------------------------------------------------------------------------- 1 | # font: fontimage 2 | 3 | font() { 4 | fontimage -o "$cache_f.png" "$f" 2> /dev/null && 5 | mv -- "$cache_f.png" "$cache_f" 6 | } 7 | 8 | convert_and_show_image font 9 | -------------------------------------------------------------------------------- /sh/prev/glow.sh: -------------------------------------------------------------------------------- 1 | # markdown: glow 2 | 3 | glow -s auto -w "$w" "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/gpg.sh: -------------------------------------------------------------------------------- 1 | # gpg-encrypted: gpg 2 | 3 | # "showgpg" option must be enabled for this preview to work 4 | [ -z "$showgpg" ] && exit 127 5 | 6 | gpg -d -- "$f" 2>&1 7 | -------------------------------------------------------------------------------- /sh/prev/highlight.sh: -------------------------------------------------------------------------------- 1 | # text: highlight 2 | 3 | highlight \ 4 | --replace-tabs=4 --out-format=ansi \ 5 | --style='pablo' --force -- "$f" 6 | -------------------------------------------------------------------------------- /sh/prev/image.sh: -------------------------------------------------------------------------------- 1 | # image: ueberzug chafa 2 | 3 | setup_image 4 | 5 | send_image "$f" 6 | -------------------------------------------------------------------------------- /sh/prev/jq.sh: -------------------------------------------------------------------------------- 1 | # json: jq 2 | 3 | jq -C . < "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/libreoffice.sh: -------------------------------------------------------------------------------- 1 | # office: libreoffice 2 | 3 | doc() { 4 | # File produced by libreoffice 5 | jpg="$(printf '%s\n' "$f" | sed 's|^.*/||; s|\..*$||')" 6 | 7 | libreoffice \ 8 | --headless \ 9 | --convert-to jpg \ 10 | --outdir "$cache_d" \ 11 | "$f" >/dev/null && 12 | mv -- "$cache_d/$jpg.jpg" "$cache_f" 13 | } 14 | 15 | convert_and_show_image doc 16 | -------------------------------------------------------------------------------- /sh/prev/ls.sh: -------------------------------------------------------------------------------- 1 | # directory: ls 2 | 3 | ls --color --group-directories-first -- "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/lynx.sh: -------------------------------------------------------------------------------- 1 | # html: lynx 2 | 3 | lynx -dump -nonumbers -nolist -width="$w" -- "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/mdcat.sh: -------------------------------------------------------------------------------- 1 | # markdown: mdcat 2 | 3 | MDCAT_PAGER= PAGER=cat mdcat -p -l --columns "$w" -- "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/pdf.sh: -------------------------------------------------------------------------------- 1 | # pdf: pdftoppm 2 | 3 | pdf() { 4 | pdftoppm -f 1 -l 1 \ 5 | -scale-to-x 1920 \ 6 | -scale-to-y -1 \ 7 | -singlefile \ 8 | -jpeg \ 9 | -- "$f" "$cache_f" && mv -- "$cache_f.jpg" "$cache_f" 10 | } 11 | 12 | convert_and_show_image pdf 13 | -------------------------------------------------------------------------------- /sh/prev/source_highlight.sh: -------------------------------------------------------------------------------- 1 | # text: source-highlight 2 | 3 | source-highlight \ 4 | --tab=4 --out-format=esc \ 5 | --style=esc256.style --failsafe \ 6 | -i "$f" 7 | -------------------------------------------------------------------------------- /sh/prev/svg.sh: -------------------------------------------------------------------------------- 1 | # svg: convert 2 | 3 | svg() { 4 | convert "$f" "jpg:$cache_f" 5 | } 6 | 7 | convert_and_show_image svg 8 | -------------------------------------------------------------------------------- /sh/prev/symlink.sh: -------------------------------------------------------------------------------- 1 | # 2 | # do nothing because in src/ctpv.c some kind of a "preview" 3 | # is already printed 4 | # 5 | -------------------------------------------------------------------------------- /sh/prev/torrent.sh: -------------------------------------------------------------------------------- 1 | # torrent: transmission-show 2 | 3 | transmission-show -- "$f" 4 | -------------------------------------------------------------------------------- /sh/prev/video.sh: -------------------------------------------------------------------------------- 1 | # video: ffmpegthumbnailer 2 | 3 | video() { 4 | ffmpegthumbnailer -i "$f" -o "$cache_f" -s 0 -t 50% 2>/dev/null 5 | } 6 | 7 | convert_and_show_image video 8 | -------------------------------------------------------------------------------- /sh/prev/w3m.sh: -------------------------------------------------------------------------------- 1 | # html: w3m 2 | 3 | w3m -dump "$f" 4 | -------------------------------------------------------------------------------- /src/attrs.h: -------------------------------------------------------------------------------- 1 | #ifndef ATTRS_H 2 | #define ATTRS_H 3 | 4 | #define UNUSED __attribute__((unused)) 5 | #define WARN_UNUSED_RESULT __attribute__((warn_unused_result)) 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /src/config.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "ctpv.h" 4 | #include "lexer.h" 5 | #include "error.h" 6 | #include "config.h" 7 | #include "preview.h" 8 | 9 | #define CHECK(f, cond) \ 10 | do { \ 11 | enum Status x = (f); \ 12 | if (!(cond)) \ 13 | return x; \ 14 | } while (0) 15 | 16 | #define CHECK_OK(f) CHECK(f, x == STAT_OK) 17 | #define CHECK_NULL(f) CHECK(f, x == STAT_NULL) 18 | 19 | #define EXPECT(c, x) CHECK_OK(expect(c, x)) 20 | #define ACCEPT(c, x) CHECK_OK(accept(c, x)) 21 | #define NOT_ACCEPT(c, x) CHECK_NULL(accept(c, x)) 22 | 23 | #define DEF_OPTION(name, type, val) { (#name), (type), { .val = &ctpv.opts.name } } 24 | #define DEF_OPTION_BOOL(name) DEF_OPTION(name, OPTION_BOOL, i) 25 | #define DEF_OPTION_INT(name) DEF_OPTION(name, OPTION_INT, i) 26 | #define DEF_OPTION_STR(name) DEF_OPTION(name, OPTION_STR, s) 27 | 28 | #define TYPE_SET_EMPTY (struct TypeSet){ NULL, NULL, NULL } 29 | 30 | struct Parser { 31 | Lexer *lexer; 32 | Token token; 33 | VectorPreview *previews; 34 | }; 35 | 36 | struct Option { 37 | char *name; 38 | enum { 39 | OPTION_BOOL, 40 | OPTION_INT, 41 | OPTION_STR, 42 | } arg_type; 43 | union { 44 | int *i; 45 | char **s; 46 | } arg_val; 47 | }; 48 | 49 | struct TypeSet { 50 | char *type, *subtype, *ext; 51 | }; 52 | 53 | enum Status { 54 | STAT_OK, 55 | STAT_ERR, 56 | STAT_NULL, 57 | }; 58 | 59 | static struct Option options[] = { 60 | DEF_OPTION_BOOL(forcekitty), 61 | DEF_OPTION_BOOL(forcekittyanim), 62 | DEF_OPTION_BOOL(forcechafa), 63 | DEF_OPTION_BOOL(noimages), 64 | DEF_OPTION_BOOL(nosymlinkinfo), 65 | DEF_OPTION_BOOL(autochafa), 66 | DEF_OPTION_BOOL(chafasixel), 67 | DEF_OPTION_BOOL(showgpg), 68 | DEF_OPTION_STR(shell), 69 | }; 70 | 71 | static void any_type_null(char **s) 72 | { 73 | if (*s && strcmp(*s, any_type) == 0) 74 | *s = NULL; 75 | } 76 | 77 | static void add_preview(Parser *ctx, char *name, char *script, struct TypeSet *set, 78 | unsigned int set_len) 79 | { 80 | if (!ctx->previews) 81 | return; 82 | 83 | size_t script_len = strlen(script) + 1; 84 | 85 | for (unsigned int i = 0; i < set_len; i++) { 86 | any_type_null(&set[i].type); 87 | any_type_null(&set[i].subtype); 88 | 89 | Preview p = (Preview){ 90 | .name = name, 91 | .script = script, 92 | .script_len = script_len, 93 | .ext = set[i].ext, 94 | .type = set[i].type, 95 | .subtype = set[i].subtype, 96 | .attrs = PREV_ATTR_NONE, 97 | .order = 1, /* custom previews are always prioritized */ 98 | .priority = 0 99 | }; 100 | 101 | vectorPreview_append(ctx->previews, p); 102 | } 103 | } 104 | 105 | static RESULT add_priority(Parser *ctx, char *name, int priority) 106 | { 107 | if (!ctx->previews) 108 | return OK; 109 | 110 | int found = 0; 111 | 112 | for (size_t i = 0; i < ctx->previews->len; i++) { 113 | if (strcmp(ctx->previews->buf[i].name, name) != 0) 114 | continue; 115 | 116 | ctx->previews->buf[i].priority = priority; 117 | found = 1; 118 | } 119 | 120 | return found ? OK : ERR; 121 | } 122 | 123 | static RESULT remove_preview(Parser *ctx, char *name) 124 | { 125 | if (!ctx->previews) 126 | return OK; 127 | 128 | int found = 0; 129 | 130 | for (ssize_t i = ctx->previews->len - 1; i >= 0; i--) { 131 | if (strcmp(ctx->previews->buf[i].name, name) != 0) 132 | continue; 133 | 134 | vectorPreview_remove(ctx->previews, i); 135 | found = 1; 136 | } 137 | 138 | return found ? OK : ERR; 139 | } 140 | 141 | static inline void next_token(Parser *ctx) 142 | { 143 | ctx->token = lexer_get_token(ctx->lexer); 144 | } 145 | 146 | static enum Status accept(Parser *ctx, enum TokenType type) 147 | { 148 | if (ctx->token.type == type) { 149 | next_token(ctx); 150 | return STAT_OK; 151 | } 152 | 153 | return STAT_NULL; 154 | } 155 | 156 | static enum Status expect(Parser *ctx, enum TokenType type) 157 | { 158 | if (accept(ctx, type) == STAT_OK) 159 | return STAT_OK; 160 | 161 | if (ctx->token.type == TOK_ERR) 162 | return STAT_ERR; 163 | 164 | PARSEERROR(ctx->token, "unexpected token: %s, expected: %s", 165 | lexer_token_type_str(ctx->token.type), 166 | lexer_token_type_str(type)); 167 | return STAT_ERR; 168 | } 169 | 170 | static enum Status preview_type_ext(Parser *ctx, char **ext) 171 | { 172 | ACCEPT(ctx, TOK_DOT); 173 | 174 | Token tok = ctx->token; 175 | EXPECT(ctx, TOK_STR); 176 | *ext = tok.val.s; 177 | 178 | return STAT_OK; 179 | } 180 | 181 | static enum Status preview_type_mime_part(Parser *ctx, char **s) 182 | { 183 | NOT_ACCEPT(ctx, TOK_STAR); 184 | 185 | Token tok = ctx->token; 186 | EXPECT(ctx, TOK_STR); 187 | *s = tok.val.s; 188 | 189 | return STAT_OK; 190 | } 191 | 192 | static enum Status preview_type_mime(Parser *ctx, char **type, char **subtype) 193 | { 194 | CHECK_OK(preview_type_mime_part(ctx, type)); 195 | EXPECT(ctx, TOK_SLASH); 196 | CHECK_OK(preview_type_mime_part(ctx, subtype)); 197 | 198 | return STAT_OK; 199 | } 200 | 201 | static inline void num_is_text(Parser *ctx) 202 | { 203 | lexer_set_opts(ctx->lexer, LEX_OPT_NUMISTEXT); 204 | } 205 | 206 | static inline void reset_lexer_opts(Parser *ctx) 207 | { 208 | lexer_set_opts(ctx->lexer, LEX_OPT_NONE); 209 | } 210 | 211 | static enum Status preview_type(Parser *ctx, struct TypeSet *set) 212 | { 213 | enum Status ret; 214 | 215 | *set = TYPE_SET_EMPTY; 216 | 217 | num_is_text(ctx); 218 | 219 | if ((ret = preview_type_ext(ctx, &set->ext)) != STAT_NULL) 220 | goto exit; 221 | 222 | ret = preview_type_mime(ctx, &set->type, &set->subtype); 223 | 224 | exit: 225 | reset_lexer_opts(ctx); 226 | return ret; 227 | } 228 | 229 | static struct Option *get_option(char *name) 230 | { 231 | for (size_t i = 0; i < LEN(options); i++) { 232 | if (strcmp(name, options[i].name) == 0) 233 | return options + i; 234 | } 235 | 236 | return NULL; 237 | } 238 | 239 | static enum Status cmd_set(Parser *ctx) 240 | { 241 | Token name = ctx->token; 242 | EXPECT(ctx, TOK_STR); 243 | 244 | struct Option *opt = get_option(name.val.s); 245 | if (!opt) { 246 | PARSEERROR(name, "option '%s' does not exist", name.val.s); 247 | return STAT_ERR; 248 | } 249 | 250 | Token value = ctx->token; 251 | 252 | switch (opt->arg_type) { 253 | case OPTION_BOOL: 254 | *opt->arg_val.i = accept(ctx, TOK_INT) == STAT_OK ? value.val.i : 1; 255 | break; 256 | case OPTION_INT: 257 | EXPECT(ctx, TOK_INT); 258 | *opt->arg_val.i = value.val.i; 259 | break; 260 | case OPTION_STR: 261 | EXPECT(ctx, TOK_STR); 262 | *opt->arg_val.s = value.val.s; 263 | break; 264 | default: 265 | PRINTINTERR("unknown type: %d", opt->arg_type); 266 | abort(); 267 | } 268 | 269 | return STAT_OK; 270 | } 271 | 272 | static enum Status cmd_preview(Parser *ctx) 273 | { 274 | Token name = ctx->token; 275 | EXPECT(ctx, TOK_STR); 276 | 277 | struct TypeSet types[16]; 278 | unsigned int types_len = 0; 279 | 280 | while (accept(ctx, TOK_BLK_OPEN) == STAT_NULL) { 281 | if (types_len >= LEN(types)) { 282 | PARSEERROR(name, "a preview can only have up through %lu types", 283 | LEN(types)); 284 | return STAT_ERR; 285 | } 286 | 287 | CHECK_OK(preview_type(ctx, types + types_len++)); 288 | } 289 | 290 | Token script = ctx->token; 291 | EXPECT(ctx, TOK_STR); 292 | 293 | EXPECT(ctx, TOK_BLK_CLS); 294 | 295 | add_preview(ctx, name.val.s, script.val.s, types, types_len); 296 | return STAT_OK; 297 | } 298 | 299 | static enum Status cmd_priority(Parser *ctx) 300 | { 301 | Token name = ctx->token; 302 | EXPECT(ctx, TOK_STR); 303 | 304 | Token number = ctx->token; 305 | enum Status i = accept(ctx, TOK_INT) == STAT_OK ? number.val.i : 1; 306 | 307 | if (add_priority(ctx, name.val.s, i) != OK) { 308 | PARSEERROR(name, "preview '%s' not found", name.val.s); 309 | return STAT_ERR; 310 | } 311 | 312 | return STAT_OK; 313 | } 314 | 315 | static enum Status cmd_remove(Parser *ctx) 316 | { 317 | Token name = ctx->token; 318 | EXPECT(ctx, TOK_STR); 319 | 320 | if (remove_preview(ctx, name.val.s) != OK) { 321 | PARSEERROR(name, "preview '%s' not found", name.val.s); 322 | return STAT_ERR; 323 | } 324 | 325 | return STAT_OK; 326 | } 327 | 328 | static enum Status command(Parser *ctx) 329 | { 330 | Token cmd = ctx->token; 331 | EXPECT(ctx, TOK_STR); 332 | 333 | if (strcmp(cmd.val.s, "set") == 0) 334 | return cmd_set(ctx); 335 | else if (strcmp(cmd.val.s, "preview") == 0) 336 | return cmd_preview(ctx); 337 | else if (strcmp(cmd.val.s, "priority") == 0) 338 | return cmd_priority(ctx); 339 | else if (strcmp(cmd.val.s, "remove") == 0) 340 | return cmd_remove(ctx); 341 | 342 | PARSEERROR(cmd, "unknown command: %s", cmd.val.s); 343 | return STAT_ERR; 344 | } 345 | 346 | static void newlines(Parser *ctx) 347 | { 348 | while (1) { 349 | if (accept(ctx, TOK_NEW_LN) != STAT_OK) 350 | break; 351 | } 352 | } 353 | 354 | static enum Status end(Parser *ctx) 355 | { 356 | NOT_ACCEPT(ctx, TOK_EOF); 357 | EXPECT(ctx, TOK_NEW_LN); 358 | 359 | newlines(ctx); 360 | 361 | return STAT_OK; 362 | } 363 | 364 | static enum Status commands(Parser *ctx) 365 | { 366 | newlines(ctx); 367 | 368 | while (1) { 369 | NOT_ACCEPT(ctx, TOK_EOF); 370 | CHECK_OK(command(ctx)); 371 | CHECK_OK(end(ctx)); 372 | } 373 | } 374 | 375 | static RESULT parse(Parser *ctx) 376 | { 377 | #ifdef DEBUG_LEXER 378 | while (1) { 379 | next_token(ctx); 380 | if (token.type == TOK_EOF) 381 | break; 382 | printf("%s", lexer_token_type_str(token.type)); 383 | switch (token.type) { 384 | case TOK_INT: 385 | printf(": %d\n", token.val.i); 386 | break; 387 | case TOK_STR: 388 | printf(": %s\n", token.val.s); 389 | break; 390 | default: 391 | puts(""); 392 | break; 393 | } 394 | } 395 | #endif 396 | #ifndef DEBUG_LEXER 397 | next_token(ctx); 398 | if (commands(ctx) == STAT_ERR) 399 | return ERR; 400 | #endif 401 | 402 | return OK; 403 | } 404 | 405 | RESULT config_load(Parser **ctx, VectorPreview *prevs, char *filename) 406 | { 407 | enum Result ret = OK; 408 | 409 | FILE *f; 410 | ERRCHK_GOTO_ERN(!(f = fopen(filename, "r")), ret, exit); 411 | 412 | if (!(*ctx = malloc(sizeof(**ctx)))) { 413 | FUNCFAILED("malloc", strerror(errno)); 414 | abort(); 415 | } 416 | 417 | (*ctx)->lexer = lexer_init(f); 418 | (*ctx)->previews = prevs; 419 | 420 | ERRCHK_GOTO_OK(parse(*ctx), ret, file); 421 | 422 | ERRCHK_GOTO((access(ctpv.opts.shell, F_OK) != 0), ret, file, "shell '%s' was not found", ctpv.opts.shell); 423 | 424 | file: 425 | fclose(f); 426 | 427 | exit: 428 | return ret; 429 | } 430 | 431 | void config_cleanup(Parser *ctx) 432 | { 433 | if (!ctx->lexer) 434 | return; 435 | 436 | lexer_free(ctx->lexer); 437 | ctx->lexer = NULL; 438 | 439 | free(ctx); 440 | } 441 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | #include "preview.h" 5 | 6 | typedef struct Parser Parser; 7 | 8 | RESULT config_load(Parser **ctx, VectorPreview *prevs, char *filename); 9 | void config_cleanup(Parser *ctx); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /src/ctpv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "ctpv.h" 12 | #include "error.h" 13 | #include "utils.h" 14 | #include "config.h" 15 | #include "server.h" 16 | #include "preview.h" 17 | #include "../version.h" 18 | #include "../previews.h" 19 | #include "../gen/help.h" 20 | 21 | struct InputFile { 22 | char link[PATH_MAX], path[PATH_MAX]; 23 | }; 24 | 25 | struct CTPV ctpv; 26 | 27 | const char any_type[] = ANY_TYPE; 28 | 29 | static magic_t magic; 30 | 31 | static Parser *parser; 32 | 33 | static VectorPreview *previews; 34 | 35 | static void cleanup(void) 36 | { 37 | previews_cleanup(); 38 | if (parser) 39 | config_cleanup(parser); 40 | if (magic != NULL) 41 | magic_close(magic); 42 | if (previews) 43 | vectorPreview_free(previews); 44 | } 45 | 46 | static RESULT init_magic(void) 47 | { 48 | ERRCHK_RET_MSG(!(magic = magic_open(MAGIC_MIME_TYPE)), magic_error(magic)); 49 | 50 | ERRCHK_RET_MSG(magic_load(magic, NULL) != 0, magic_error(magic)); 51 | 52 | return OK; 53 | } 54 | 55 | static RESULT create_dir(char *buf, size_t len) 56 | { 57 | char dir[len]; 58 | strncpy(dir, buf, LEN(dir) - 1); 59 | ERRCHK_RET_ERN(mkpath(dir, 0700) == -1); 60 | 61 | return OK; 62 | } 63 | 64 | static RESULT get_config_file(char *buf, size_t len) 65 | { 66 | ERRCHK_RET_OK(get_config_dir(buf, len, "ctpv/")); 67 | ERRCHK_RET_OK(create_dir(buf, len)); 68 | 69 | strncat(buf, "config", len - 1); 70 | 71 | if (access(buf, F_OK) != 0) 72 | close(creat(buf, 0600)); 73 | 74 | return OK; 75 | } 76 | 77 | static RESULT config(int prevs) 78 | { 79 | char config_file[FILENAME_MAX]; 80 | ERRCHK_RET_OK(get_config_file(config_file, LEN(config_file))); 81 | 82 | ERRCHK_RET_OK(config_load(&parser, prevs ? previews : NULL, config_file)); 83 | 84 | return OK; 85 | } 86 | 87 | static RESULT init_previews(void) 88 | { 89 | /* 20 is some arbitrary number, it's here in order to 90 | * to save one realloc() if user has less then 20 custom previews */ 91 | previews = vectorPreview_new(LEN(b_previews) + 20); 92 | vectorPreview_append_arr(previews, b_previews, LEN(b_previews)); 93 | 94 | ERRCHK_RET_OK(config(1)); 95 | 96 | previews_init(previews->buf, previews->len); 97 | 98 | return OK; 99 | } 100 | 101 | static const char *get_mimetype(const char *path) 102 | { 103 | const char *r = magic_file(magic, path); 104 | if (!r) { 105 | FUNCFAILED("magic_file", magic_error(magic)); 106 | return NULL; 107 | } 108 | 109 | return r; 110 | } 111 | 112 | static inline void file_access_err(char *f, int errno_) 113 | { 114 | print_errorf("failed to access '%s': %s", f, strerror(errno_)); 115 | } 116 | 117 | static RESULT get_input_file(struct InputFile *input_f, char *f) 118 | { 119 | if (!f) { 120 | print_error("file not given"); 121 | return ERR; 122 | } 123 | 124 | input_f->link[0] = input_f->path[0] = '\0'; 125 | 126 | if (realpath(f, input_f->path) == NULL) { 127 | file_access_err(input_f->path, errno); 128 | return ERR; 129 | } 130 | 131 | ssize_t link_len = readlink(f, input_f->link, LEN(input_f->link)); 132 | if (link_len == -1 && errno != EINVAL) { 133 | FUNCFAILED("readlink", strerror(errno)); 134 | return ERR; 135 | } 136 | 137 | input_f->link[link_len] = '\0'; 138 | 139 | return OK; 140 | } 141 | 142 | static RESULT is_newer(int *resp, char *f1, char *f2) 143 | { 144 | struct stat stat1, stat2; 145 | ERRCHK_RET_ERN(lstat(f1, &stat1) == -1); 146 | ERRCHK_RET_ERN(lstat(f2, &stat2) == -1); 147 | 148 | #if __APPLE__ 149 | struct timespec *t1 = &stat1.st_ctimespec; 150 | struct timespec *t2 = &stat2.st_ctimespec; 151 | #else 152 | struct timespec *t1 = &stat1.st_ctim; 153 | struct timespec *t2 = &stat2.st_ctim; 154 | #endif 155 | 156 | time_t sec_d = t1->tv_sec - t2->tv_sec; 157 | if (sec_d < 0) 158 | goto older; 159 | else if (sec_d == 0 && t1->tv_nsec <= t2->tv_nsec) 160 | goto older; 161 | 162 | *resp = 1; 163 | return OK; 164 | 165 | older: 166 | *resp = 0; 167 | return OK; 168 | } 169 | 170 | static void md5_string(char *buf, size_t len, char *s) 171 | { 172 | unsigned char out[MD5_DIGEST_LENGTH]; 173 | char b[16]; 174 | 175 | MD5((const unsigned char *)s, strlen(s), out); 176 | 177 | buf[0] = '\0'; 178 | for (unsigned int i = 0; i < LEN(out); i++) { 179 | snprintf(b, LEN(b)-1, "%02x", out[i]); 180 | strncat(buf, b, len); 181 | } 182 | } 183 | 184 | static RESULT get_cache_file(char *dir, size_t dir_len, char *filename, 185 | size_t filename_len, char *file) 186 | { 187 | ERRCHK_RET_OK(get_cache_dir(dir, dir_len, "ctpv/")); 188 | ERRCHK_RET_OK(create_dir(dir, dir_len)); 189 | 190 | size_t dir_str_len = strlen(dir); 191 | 192 | memcpy(filename, dir, filename_len); 193 | 194 | md5_string(filename + dir_str_len, filename_len - dir_str_len - 1, file); 195 | 196 | /* Remove dash at the end */ 197 | dir[dir_str_len-1] = '\0'; 198 | 199 | return OK; 200 | } 201 | 202 | static RESULT check_cache(int *resp, char *file, char *cache_file) 203 | { 204 | if (access(cache_file, F_OK) != 0) { 205 | *resp = 0; 206 | return OK; 207 | } 208 | 209 | return is_newer(resp, cache_file, file); 210 | } 211 | 212 | #define GET_PARG(a, i) (a) = (argc > (i) ? argv[i] : NULL) 213 | 214 | static RESULT preview(int argc, char *argv[]) 215 | { 216 | char *f, *w, *h, *x, *y, *id; 217 | char w_buf[24], h_buf[24], y_buf[24]; 218 | long w_l, h_l, y_l; 219 | 220 | GET_PARG(f, 0); 221 | GET_PARG(w, 1); 222 | GET_PARG(h, 2); 223 | GET_PARG(x, 3); 224 | GET_PARG(y, 4); 225 | GET_PARG(id, 5); 226 | 227 | if (!w) 228 | w = "80"; 229 | if (!h) 230 | h = "40"; 231 | if (!x) 232 | x = "0"; 233 | if (!y) 234 | y = "0"; 235 | 236 | ERRCHK_RET_OK(strtol_w(&w_l, w, NULL, 10)); 237 | ERRCHK_RET_OK(strtol_w(&h_l, h, NULL, 10)); 238 | ERRCHK_RET_OK(strtol_w(&y_l, y, NULL, 10)); 239 | 240 | ERRCHK_RET_OK(init_previews()); 241 | 242 | struct InputFile input_f; 243 | ERRCHK_RET_OK(get_input_file(&input_f, f)); 244 | 245 | if (!ctpv.opts.nosymlinkinfo && *input_f.link) { 246 | printf("\033[1;36mSymlink points to:\033[m\n\t%s\n\n", input_f.link); 247 | fflush(stdout); 248 | 249 | y_l += 3; 250 | h_l -= 3; 251 | } 252 | 253 | /* To some reason lf chops off last 2 characters of a line */ 254 | w_l = MAX(0, w_l - 2); 255 | 256 | snprintf(w_buf, LEN(w_buf), "%ld", w_l); 257 | snprintf(h_buf, LEN(h_buf), "%ld", h_l); 258 | snprintf(y_buf, LEN(y_buf), "%ld", y_l); 259 | 260 | w = w_buf; 261 | h = h_buf; 262 | y = y_buf; 263 | 264 | ERRCHK_RET_OK(init_magic()); 265 | 266 | const char *mimetype; 267 | ERRCHK_RET(!(mimetype = get_mimetype(input_f.path))); 268 | 269 | char cache_dir[FILENAME_MAX], cache_file[FILENAME_MAX]; 270 | ERRCHK_RET_OK(get_cache_file(cache_dir, LEN(cache_file), cache_file, 271 | LEN(cache_file), input_f.path)); 272 | 273 | int cache_valid; 274 | ERRCHK_RET_OK(check_cache(&cache_valid, input_f.path, cache_file)); 275 | 276 | PreviewArgs args = { 277 | .f = input_f.path, 278 | .w = w, 279 | .h = h, 280 | .x = x, 281 | .y = y, 282 | .id = id, 283 | .cache_dir = cache_dir, 284 | .cache_file = cache_file, 285 | .cache_valid = cache_valid, 286 | }; 287 | 288 | return preview_run(get_ext(input_f.path), mimetype, &args); 289 | } 290 | 291 | static RESULT server(void) 292 | { 293 | return server_listen(ctpv.server_id_s); 294 | } 295 | 296 | static RESULT clear(void) 297 | { 298 | ERRCHK_RET_OK(config(0)); 299 | return server_clear(ctpv.server_id_s); 300 | } 301 | 302 | static RESULT end(void) 303 | { 304 | ERRCHK_RET_OK(config(0)); 305 | return server_end(ctpv.server_id_s); 306 | } 307 | 308 | static RESULT list(void) 309 | { 310 | ERRCHK_RET_OK(init_previews()); 311 | 312 | size_t len; 313 | Preview p, **list = previews_get(&len); 314 | const char *n, *e, *t, *s; 315 | 316 | const char header_name[] = "Name", header_ext[] = "Extension", 317 | header_mime[] = "MIME type"; 318 | 319 | int width_name = 0, width_ext = 0; 320 | 321 | for (size_t i = 0; i < len + 1; i++) { 322 | if (i < len) { 323 | p = *list[i]; 324 | n = p.name; 325 | e = p.ext; 326 | } else { 327 | n = header_name; 328 | e = header_ext; 329 | } 330 | 331 | int name_len = strlennull(n); 332 | int ext_len = strlennull(e); 333 | width_name = MAX(width_name, name_len); 334 | width_ext = MAX(width_ext, ext_len); 335 | } 336 | 337 | width_name += 2, width_ext += 2; 338 | 339 | puts("List of available previews:\n"); 340 | printf("\t%-*s %-*s %s\n\n", width_name, header_name, width_ext, header_ext, 341 | header_mime); 342 | 343 | for (size_t i = 0; i < len; i++) { 344 | p = *list[i]; 345 | e = p.ext; 346 | t = p.type; 347 | s = p.subtype; 348 | 349 | if (!e) 350 | e = any_type; 351 | 352 | if (!t) { 353 | t = any_type; 354 | s = any_type; 355 | } else if (!s) { 356 | s = any_type; 357 | } 358 | 359 | printf("\t%-*s .%-*s %s/%s\n", width_name, p.name, width_ext - 1, e, t, 360 | s); 361 | } 362 | 363 | puts("\nNote: '" ANY_TYPE "' means that it matches any.\n"); 364 | 365 | return OK; 366 | } 367 | 368 | static RESULT mime(int argc, char *argv[]) 369 | { 370 | const char *mimetype; 371 | struct InputFile input_f; 372 | 373 | if (argc <= 0) { 374 | print_error("files are not specified"); 375 | return ERR; 376 | } 377 | 378 | ERRCHK_RET_OK(init_magic()); 379 | 380 | for (int i = 0; i < argc; i++) { 381 | ERRCHK_RET_OK(get_input_file(&input_f, argv[i])); 382 | 383 | mimetype = get_mimetype(input_f.path); 384 | ERRCHK_RET(!mimetype); 385 | 386 | if (argc > 1) 387 | printf("%s:\t", argv[i]); 388 | 389 | printf(".%s ", get_ext(input_f.path)); 390 | puts(mimetype); 391 | } 392 | 393 | return OK; 394 | } 395 | 396 | static RESULT help(void) 397 | { 398 | printf("Usage: %s [OPTION]... FILE\n\n", program); 399 | printf("Options:\n%s", help_txt); 400 | return OK; 401 | } 402 | 403 | static RESULT version(void) 404 | { 405 | printf("%s version %s\n", program, VERSION); 406 | return OK; 407 | } 408 | 409 | int main(int argc, char *argv[]) 410 | { 411 | program = argc > 0 ? argv[0] : "ctpv"; 412 | 413 | ctpv.opts.shell = "/bin/sh"; 414 | ctpv.debug = 0; 415 | 416 | int c; 417 | while ((c = getopt(argc, argv, "hs:c:e:lmdv")) != -1) { 418 | switch (c) { 419 | case 'h': 420 | ctpv.mode = MODE_HELP; 421 | break; 422 | case 's': 423 | ctpv.mode = MODE_SERVER; 424 | ctpv.server_id_s = optarg; 425 | break; 426 | case 'c': 427 | ctpv.mode = MODE_CLEAR; 428 | ctpv.server_id_s = optarg; 429 | break; 430 | case 'e': 431 | ctpv.mode = MODE_END; 432 | ctpv.server_id_s = optarg; 433 | break; 434 | case 'l': 435 | ctpv.mode = MODE_LIST; 436 | break; 437 | case 'm': 438 | ctpv.mode = MODE_MIME; 439 | break; 440 | case 'd': 441 | ctpv.debug = 1; 442 | break; 443 | case 'v': 444 | ctpv.mode = MODE_VERSION; 445 | break; 446 | default: 447 | return EXIT_FAILURE; 448 | } 449 | } 450 | 451 | argc -= optind; 452 | argv = &argv[optind]; 453 | 454 | enum Result ret; 455 | switch (ctpv.mode) { 456 | case MODE_PREVIEW: 457 | ret = preview(argc, argv); 458 | break; 459 | case MODE_HELP: 460 | ret = help(); 461 | break; 462 | case MODE_SERVER: 463 | ret = server(); 464 | break; 465 | case MODE_CLEAR: 466 | ret = clear(); 467 | break; 468 | case MODE_END: 469 | ret = end(); 470 | break; 471 | case MODE_LIST: 472 | ret = list(); 473 | break; 474 | case MODE_MIME: 475 | ret = mime(argc, argv); 476 | break; 477 | case MODE_VERSION: 478 | ret = version(); 479 | break; 480 | default: 481 | PRINTINTERR("unknown mode: %d", ctpv.mode); 482 | ret = ERR; 483 | break; 484 | } 485 | 486 | cleanup(); 487 | 488 | return ret == OK ? EXIT_SUCCESS : EXIT_FAILURE; 489 | } 490 | -------------------------------------------------------------------------------- /src/ctpv.h: -------------------------------------------------------------------------------- 1 | #ifndef CTPV_H 2 | #define CTPV_H 3 | 4 | #define ANY_TYPE "*" 5 | 6 | struct CTPV { 7 | enum { 8 | MODE_PREVIEW = 0, /* default mode */ 9 | MODE_HELP, 10 | MODE_SERVER, 11 | MODE_CLEAR, 12 | MODE_END, 13 | MODE_LIST, 14 | MODE_MIME, 15 | MODE_VERSION, 16 | } mode; 17 | char *server_id_s; 18 | struct { 19 | int forcekitty, forcekittyanim, forcechafa, noimages, nosymlinkinfo; 20 | int autochafa, chafasixel, showgpg; 21 | char *shell; 22 | } opts; 23 | int debug; 24 | }; 25 | 26 | extern struct CTPV ctpv; 27 | 28 | extern const char any_type[]; 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /src/error.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "error.h" 4 | #include "utils.h" 5 | 6 | void print_error(const char *error_msg) 7 | { 8 | /* We print errors to stdout because lf file manager 9 | * doesn't print stderr in the preview window. */ 10 | fprintf(stdout, "%s: %s\n", program, error_msg); 11 | fflush(stdout); 12 | } 13 | 14 | void print_errorf(const char *format, ...) 15 | { 16 | char s[512]; 17 | FORMATTED_STRING(s, format); 18 | 19 | print_error(s); 20 | } 21 | 22 | void print_int_error(const char *file, unsigned long line, const char *msg) 23 | { 24 | print_errorf("%s:%lu: internal error: %s", file, line, msg); 25 | } 26 | 27 | void print_int_errorf(const char *file, unsigned long line, const char *format, ...) 28 | { 29 | char s[512]; 30 | FORMATTED_STRING(s, format); 31 | 32 | print_int_error(file, line, s); 33 | } 34 | -------------------------------------------------------------------------------- /src/error.h: -------------------------------------------------------------------------------- 1 | #ifndef ERROR_H 2 | #define ERROR_H 3 | 4 | #include 5 | #include 6 | 7 | #include "utils.h" 8 | 9 | /* 10 | * Print internal error 11 | */ 12 | #define PRINTINTERR(format, ...) \ 13 | print_int_error##__VA_OPT__(f)(__FILE__, __LINE__, format __VA_OPT__(, ) __VA_ARGS__) 14 | 15 | #define FUNCFAILED(f, ...) \ 16 | PRINTINTERR(f "() failed" __VA_OPT__(": %s", __VA_ARGS__)) 17 | 18 | #define ERRCHK_PRINT_(...) \ 19 | do { \ 20 | PRINTINTERR(__VA_ARGS__); \ 21 | } while (0) 22 | 23 | /* 24 | * If cond is true, return ERR. Also call print_error or 25 | * print_errorf if error message or format string is given. 26 | */ 27 | #define ERRCHK_RET(cond, ...) \ 28 | do { \ 29 | if (cond) { \ 30 | __VA_OPT__(ERRCHK_PRINT_(__VA_ARGS__);) \ 31 | return ERR; \ 32 | } \ 33 | } while (0) 34 | 35 | #define ERRCHK_GOTO(cond, ret, label, ...) \ 36 | do { \ 37 | if (cond) { \ 38 | __VA_OPT__(ERRCHK_PRINT_(__VA_ARGS__);) \ 39 | ret = ERR; \ 40 | goto label; \ 41 | } \ 42 | } while (0) 43 | 44 | #define ERRCHK_MSG_(x) "'" x "'" 45 | 46 | #define ERRCHK_RET_MSG(cond, ...) \ 47 | ERRCHK_RET(cond, ERRCHK_MSG_(#cond) __VA_OPT__(": %s", ) __VA_ARGS__) 48 | 49 | #define ERRCHK_GOTO_MSG(cond, ret, label, ...) \ 50 | ERRCHK_GOTO(cond, ret, label, \ 51 | ERRCHK_MSG_(#cond) __VA_OPT__(": %s", ) __VA_ARGS__) 52 | 53 | #define ERRCHK_RET_ERN(cond) ERRCHK_RET_MSG(cond, strerror(errno)) 54 | #define ERRCHK_GOTO_ERN(cond, ret, label) ERRCHK_GOTO_MSG(cond, ret, label, strerror(errno)) 55 | 56 | /* 57 | * Shortcut for ERRCHK_*_RET(expr != OK) 58 | */ 59 | #define ERRCHK_RET_OK(e) ERRCHK_RET((e) != OK) 60 | #define ERRCHK_GOTO_OK(e, r, l) ERRCHK_GOTO((e) != OK, r, l) 61 | 62 | void print_error(const char *error_msg); 63 | void print_errorf(const char *format, ...); 64 | void print_int_error(const char *file, unsigned long line, const char *msg); 65 | void print_int_errorf(const char *file, unsigned long line, const char *format, ...); 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /src/lexer.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "error.h" 5 | #include "lexer.h" 6 | #include "ulist.h" 7 | 8 | #define READ_PUNCT(c, t, s) read_punct((c), (t), (s), LEN(s) - 1) 9 | 10 | #define EOF_CHAR (-1) 11 | 12 | typedef int (*Predicate)(int); 13 | 14 | typedef struct { 15 | unsigned int pos, len, eof; 16 | FILE *f; 17 | char buf[1024]; 18 | } InputBuffer; 19 | 20 | typedef struct { 21 | unsigned int back, front; 22 | Token toks[16]; 23 | } TokenQueue; 24 | 25 | struct Lexer { 26 | enum LexerOpts opts; 27 | unsigned line, col; 28 | struct { 29 | unsigned int line, col; 30 | } tok_pos; 31 | InputBuffer input_buf; 32 | TokenQueue tok_queue; 33 | UList *text_buf; 34 | }; 35 | 36 | static char block_open[] = "{{", 37 | block_close[] = "}}", 38 | slash[] = "/", 39 | star[] = "*", 40 | dot[] = "."; 41 | 42 | static void add_token_queue(Lexer *ctx, Token tok) 43 | { 44 | ctx->tok_queue.toks[ctx->tok_queue.back] = tok; 45 | ctx->tok_queue.back = (ctx->tok_queue.back + 1) % LEN(ctx->tok_queue.toks); 46 | } 47 | 48 | static Token remove_token_queue(Lexer *ctx) 49 | { 50 | Token tok = ctx->tok_queue.toks[ctx->tok_queue.front]; 51 | ctx->tok_queue.front = (ctx->tok_queue.front + 1) % LEN(ctx->tok_queue.toks); 52 | return tok; 53 | } 54 | 55 | static inline int is_empty_token_queue(Lexer *ctx) 56 | { 57 | return ctx->tok_queue.back == ctx->tok_queue.front; 58 | } 59 | 60 | static void init_input_buf(InputBuffer *b, FILE *f) 61 | { 62 | b->pos = 0; 63 | b->len = 0; 64 | b->eof = 0; 65 | b->f = f; 66 | } 67 | 68 | static int peekn_char(Lexer *ctx, unsigned int i) 69 | { 70 | InputBuffer *b = &ctx->input_buf; 71 | 72 | if (b->pos + i < b->len) 73 | goto exit; 74 | 75 | if (b->eof || (i > 0 && i >= b->len)) 76 | return EOF_CHAR; 77 | 78 | if (i > 0) { 79 | assert(i < LEN(b->buf)); 80 | memmove(b->buf, b->buf + (b->len - i), i * sizeof(*b->buf)); 81 | } 82 | 83 | b->len = i + fread(b->buf + i, sizeof(*b->buf), LEN(b->buf) - i, b->f); 84 | b->pos = 0; 85 | 86 | if (b->len != LEN(b->buf)) { 87 | if (feof(b->f)) 88 | b->eof = 1; 89 | else if (ferror(b->f)) 90 | PRINTINTERR("fread() failed"); 91 | 92 | if (b->len == 0) 93 | return EOF_CHAR; 94 | } 95 | 96 | exit: 97 | return b->buf[b->pos + i]; 98 | } 99 | 100 | static inline char peek_char(Lexer *ctx) 101 | { 102 | return peekn_char(ctx, 0); 103 | } 104 | 105 | static char nextn_char(Lexer *ctx, unsigned int i) 106 | { 107 | char c = peekn_char(ctx, i); 108 | 109 | ctx->col++; 110 | 111 | if (c == '\n') { 112 | ctx->col = 1; 113 | ctx->line++; 114 | } 115 | 116 | ctx->input_buf.pos++; 117 | 118 | return c; 119 | } 120 | 121 | static inline char next_char(Lexer *ctx) 122 | { 123 | return nextn_char(ctx, 0); 124 | } 125 | 126 | static void skipn_char(Lexer *ctx, int n) 127 | { 128 | for (int i = 0; i < n; i++) 129 | next_char(ctx); 130 | } 131 | 132 | static inline void add_text_buf(Lexer *ctx, char c) 133 | { 134 | ulist_append(ctx->text_buf, &c); 135 | } 136 | 137 | static inline void record_text(Lexer *ctx) 138 | { 139 | ulist_lock(ctx->text_buf); 140 | } 141 | 142 | static inline char *get_text(Lexer *ctx) 143 | { 144 | return ulist_unlock(ctx->text_buf); 145 | } 146 | 147 | Lexer *lexer_init(FILE *f) 148 | { 149 | Lexer *ctx; 150 | 151 | if (!(ctx = malloc(sizeof(*ctx)))) { 152 | FUNCFAILED("malloc", strerror(errno)); 153 | abort(); 154 | } 155 | 156 | init_input_buf(&ctx->input_buf, f); 157 | lexer_set_opts(ctx, LEX_OPT_NONE); 158 | ctx->text_buf = ulist_new(sizeof(char), 1024); 159 | ctx->line = ctx->col = 1; 160 | ctx->tok_queue.back = ctx->tok_queue.front = 0; 161 | 162 | return ctx; 163 | } 164 | 165 | void lexer_set_opts(Lexer *ctx, enum LexerOpts flags) 166 | { 167 | ctx->opts = flags; 168 | } 169 | 170 | void lexer_free(Lexer *ctx) 171 | { 172 | ulist_free(ctx->text_buf); 173 | free(ctx); 174 | } 175 | 176 | static int cmp_nextn(Lexer *ctx, int n, char *s) 177 | { 178 | char c; 179 | int i = 0; 180 | 181 | while (1) { 182 | c = peekn_char(ctx, i); 183 | if (i >= n || *s == '\0' || c != *s) 184 | break; 185 | 186 | s++; 187 | i++; 188 | } 189 | 190 | return i == n ? 0 : ((unsigned char)c - *(unsigned char *)s); 191 | } 192 | 193 | static void ignore_comments(Lexer *ctx) 194 | { 195 | char c; 196 | 197 | while (peek_char(ctx) == '#') { 198 | do { 199 | c = next_char(ctx); 200 | } while (c != '\n'); 201 | } 202 | } 203 | 204 | static void read_while(Lexer *ctx, Predicate p, int add) 205 | { 206 | char c; 207 | 208 | while ((c = peek_char(ctx)) >= 0 && p(c)) { 209 | if (add) 210 | add_text_buf(ctx, c); 211 | 212 | next_char(ctx); 213 | } 214 | 215 | if (add) 216 | add_text_buf(ctx, '\0'); 217 | } 218 | 219 | static inline Token get_tok(Lexer *ctx, enum TokenType type) 220 | { 221 | return (Token){ .type = type, 222 | .line = ctx->tok_pos.line, 223 | .col = ctx->tok_pos.col }; 224 | } 225 | 226 | static inline Token read_new_line(Lexer *ctx) 227 | { 228 | Token tok = get_tok(ctx, TOK_NULL); 229 | 230 | while (peek_char(ctx) == '\n') { 231 | next_char(ctx); 232 | tok.type = TOK_NEW_LN; 233 | } 234 | 235 | return tok; 236 | } 237 | 238 | static inline int issymbol(int c) 239 | { 240 | return isalnum(c) || c == '_' || c == '-'; 241 | } 242 | 243 | static inline int isnotquote(int c) 244 | { 245 | return (c != '"'); 246 | } 247 | 248 | static inline Token read_symbol(Lexer *ctx) 249 | { 250 | char c = peek_char(ctx); 251 | 252 | if (!isalpha(c)) 253 | return get_tok(ctx, TOK_NULL); 254 | 255 | record_text(ctx); 256 | read_while(ctx, issymbol, 1); 257 | 258 | Token tok = get_tok(ctx, TOK_STR); 259 | tok.val.s = get_text(ctx); 260 | 261 | return tok; 262 | } 263 | 264 | static inline Token read_string(Lexer *ctx) 265 | { 266 | char c = next_char(ctx); 267 | 268 | if (isnotquote(c)) 269 | return get_tok(ctx, TOK_NULL); 270 | 271 | record_text(ctx); 272 | read_while(ctx, isnotquote, 1); 273 | 274 | Token tok = get_tok(ctx, TOK_STR); 275 | tok.val.s = get_text(ctx); 276 | 277 | // Skip ending quote 278 | next_char(ctx); 279 | 280 | return tok; 281 | } 282 | 283 | static inline Token read_int(Lexer *ctx) 284 | { 285 | int positive = 1; 286 | 287 | if (peek_char(ctx) == '-') { 288 | positive = 0; 289 | next_char(ctx); 290 | } 291 | 292 | if (!isdigit(peek_char(ctx))) 293 | return get_tok(ctx, TOK_NULL); 294 | 295 | record_text(ctx); 296 | read_while(ctx, isdigit, 1); 297 | 298 | Token tok; 299 | char *text = get_text(ctx); 300 | 301 | /* If NUMISTEXT option is set, do not convert string to integer */ 302 | if (ctx->opts & LEX_OPT_NUMISTEXT) { 303 | tok = get_tok(ctx, TOK_STR); 304 | tok.val.s = text; 305 | return tok; 306 | } 307 | 308 | int i = atoi(text); 309 | 310 | if (!positive) 311 | i *= -1; 312 | 313 | tok = get_tok(ctx, TOK_INT); 314 | tok.val.i = i; 315 | 316 | return tok; 317 | } 318 | 319 | static Token read_punct(Lexer *ctx, int type, char *s, int n) 320 | { 321 | Token tok; 322 | 323 | if (peek_char(ctx) == EOF_CHAR) 324 | return get_tok(ctx, TOK_EOF); 325 | 326 | int ret = cmp_nextn(ctx, n, s); 327 | 328 | if (ret == 0) 329 | tok = get_tok(ctx, type); 330 | else 331 | return get_tok(ctx, TOK_NULL); 332 | 333 | skipn_char(ctx, n); 334 | 335 | return tok; 336 | } 337 | 338 | static inline Token read_block_open(Lexer *ctx) 339 | { 340 | return READ_PUNCT(ctx, TOK_BLK_OPEN, block_open); 341 | } 342 | 343 | static inline Token read_block_close(Lexer *ctx) 344 | { 345 | return READ_PUNCT(ctx, TOK_BLK_CLS, block_close); 346 | } 347 | 348 | static Token read_block(Lexer *ctx) 349 | { 350 | Token open_tok, body_tok, close_tok; 351 | 352 | if ((open_tok = read_block_open(ctx)).type == TOK_NULL) 353 | return get_tok(ctx, TOK_NULL); 354 | 355 | record_text(ctx); 356 | 357 | while (1) { 358 | close_tok = read_block_close(ctx); 359 | 360 | if (close_tok.type == TOK_EOF) { 361 | PARSEERROR(*ctx, "unclosed block"); 362 | return get_tok(ctx, TOK_ERR); 363 | } else if (close_tok.type != TOK_NULL) { 364 | break; 365 | } 366 | 367 | add_text_buf(ctx, next_char(ctx)); 368 | } 369 | 370 | add_text_buf(ctx, '\0'); 371 | 372 | body_tok = get_tok(ctx, TOK_STR); 373 | body_tok.val.s = get_text(ctx); 374 | 375 | add_token_queue(ctx, body_tok); 376 | 377 | if (close_tok.type != TOK_NULL) 378 | add_token_queue(ctx, close_tok); 379 | 380 | return open_tok; 381 | } 382 | 383 | #define ATTEMPT_READ(c, func) \ 384 | do { \ 385 | Token t = (func)(c); \ 386 | if (t.type != TOK_NULL) \ 387 | return t; \ 388 | } while (0) 389 | 390 | #define ATTEMPT_READ_CHAR(ctx, tok, ch, type_) \ 391 | do { \ 392 | char c = peek_char(ctx); \ 393 | if (c == (ch)) { \ 394 | (tok).type = (type_); \ 395 | next_char(ctx); \ 396 | return (tok); \ 397 | } \ 398 | } while (0) 399 | 400 | Token lexer_get_token(Lexer *ctx) 401 | { 402 | if (!is_empty_token_queue(ctx)) 403 | return remove_token_queue(ctx); 404 | 405 | read_while(ctx, isblank, 0); 406 | ignore_comments(ctx); 407 | 408 | ctx->tok_pos.line = ctx->line; 409 | ctx->tok_pos.col = ctx->col; 410 | 411 | Token tok = get_tok(ctx, TOK_NULL); 412 | 413 | ATTEMPT_READ_CHAR(ctx, tok, EOF_CHAR, TOK_EOF); 414 | ATTEMPT_READ_CHAR(ctx, tok, '/', TOK_SLASH); 415 | ATTEMPT_READ_CHAR(ctx, tok, '*', TOK_STAR); 416 | ATTEMPT_READ_CHAR(ctx, tok, '.', TOK_DOT); 417 | 418 | ATTEMPT_READ(ctx, read_new_line); 419 | ATTEMPT_READ(ctx, read_symbol); 420 | ATTEMPT_READ(ctx, read_int); 421 | ATTEMPT_READ(ctx, read_block); 422 | ATTEMPT_READ(ctx, read_string); 423 | 424 | PARSEERROR((*ctx), "cannot handle character: %c", peek_char(ctx)); 425 | return get_tok(ctx, TOK_ERR); 426 | } 427 | 428 | char *lexer_token_type_str(enum TokenType type) 429 | { 430 | switch (type) { 431 | case TOK_NULL: 432 | return ""; 433 | case TOK_EOF: 434 | return ""; 435 | case TOK_ERR: 436 | return ""; 437 | case TOK_NEW_LN: 438 | return ""; 439 | case TOK_BLK_OPEN: 440 | return block_open; 441 | case TOK_BLK_CLS: 442 | return block_close; 443 | case TOK_SLASH: 444 | return slash; 445 | case TOK_STAR: 446 | return star; 447 | case TOK_DOT: 448 | return dot; 449 | case TOK_INT: 450 | return ""; 451 | case TOK_STR: 452 | return ""; 453 | } 454 | 455 | PRINTINTERR("unknown type: %d", type); 456 | abort(); 457 | } 458 | -------------------------------------------------------------------------------- /src/lexer.h: -------------------------------------------------------------------------------- 1 | #ifndef LEXER_H 2 | #define LEXER_H 3 | 4 | #include 5 | #include 6 | 7 | #define PARSEERROR(c, format, ...) \ 8 | print_errorf("config:%u:%u: " format, (c).line, \ 9 | (c).col __VA_OPT__(, ) __VA_ARGS__) 10 | 11 | typedef struct Lexer Lexer; 12 | 13 | enum LexerOpts { 14 | LEX_OPT_NONE = 0, 15 | LEX_OPT_NUMISTEXT = 1 << 0, 16 | }; 17 | 18 | typedef struct { 19 | unsigned int line, col; 20 | enum TokenType { 21 | TOK_NULL, 22 | TOK_EOF, 23 | TOK_ERR, 24 | TOK_NEW_LN, 25 | TOK_BLK_OPEN, 26 | TOK_BLK_CLS, 27 | TOK_SLASH, 28 | TOK_STAR, 29 | TOK_DOT, 30 | TOK_INT, 31 | TOK_STR, 32 | } type; 33 | union { 34 | int i; 35 | char *s; 36 | } val; 37 | } Token; 38 | 39 | Lexer *lexer_init(FILE *f); 40 | void lexer_set_opts(Lexer *ctx, enum LexerOpts flags); 41 | void lexer_free(Lexer *ctx); 42 | Token lexer_get_token(Lexer *ctx); 43 | char *lexer_token_type_str(enum TokenType type); 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /src/preview.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "utils.h" 6 | #include "error.h" 7 | #include "shell.h" 8 | #include "server.h" 9 | #include "preview.h" 10 | 11 | #define FAILED_PREVIEW_EC NOTEXIST_EC 12 | #define ENOUGH_READ_EC 141 13 | 14 | #define PREVP_SIZE sizeof(Preview *) 15 | 16 | #define MIMETYPE_MAX 64 17 | 18 | VECTOR_GEN_SOURCE(Preview, Preview) 19 | 20 | static struct { 21 | size_t len; 22 | Preview **list; 23 | } previews; 24 | 25 | static int cmp_previews(const void *p1, const void *p2) 26 | { 27 | Preview *pr1 = *(Preview **)p1; 28 | Preview *pr2 = *(Preview **)p2; 29 | 30 | int i; 31 | 32 | if ((i = pr2->order - pr1->order) != 0) 33 | return i; 34 | 35 | if ((i = strcmpnull(pr1->ext, pr2->ext)) != 0) 36 | return -i; 37 | 38 | if ((i = strcmpnull(pr1->type, pr2->type)) != 0) 39 | return -i; 40 | 41 | if ((i = strcmpnull(pr1->subtype, pr2->subtype)) != 0) 42 | return -i; 43 | 44 | if ((i = pr2->priority - pr1->priority) != 0) 45 | return i; 46 | 47 | return i; 48 | } 49 | 50 | void previews_init(Preview *ps, size_t len) 51 | { 52 | previews.len = len; 53 | 54 | previews.list = malloc(len * PREVP_SIZE); 55 | if (!previews.list) { 56 | FUNCFAILED("malloc", strerror(errno)); 57 | abort(); 58 | } 59 | 60 | for (size_t i = 0; i < len; i++) 61 | previews.list[i] = &ps[i]; 62 | 63 | qsort(previews.list, previews.len, PREVP_SIZE, cmp_previews); 64 | } 65 | 66 | void previews_cleanup(void) 67 | { 68 | if (!previews.list) 69 | return; 70 | 71 | free(previews.list); 72 | previews.list = NULL; 73 | previews.len = 0; 74 | } 75 | 76 | static void break_mimetype(char *mimetype, char **type, char **subtype) 77 | { 78 | *type = mimetype[0] == '\0' ? NULL : mimetype; 79 | *subtype = NULL; 80 | 81 | char *s = strchr(mimetype, '/'); 82 | if (!s) { 83 | PRINTINTERR("invalid mimetype: '%s'", mimetype); 84 | abort(); 85 | } 86 | 87 | *s = '\0'; 88 | *subtype = &s[1]; 89 | } 90 | 91 | static Preview *find_preview(const char *type, const char *subtype, 92 | const char *ext, size_t *i) 93 | { 94 | Preview *p; 95 | const char *rext; 96 | 97 | for (; *i < previews.len; (*i)++) { 98 | p = previews.list[*i]; 99 | 100 | if (ext && (p->attrs & PREV_ATTR_EXT_SHORT)) { 101 | if ((rext = strrchr(ext, '.'))) 102 | rext += 1; 103 | } else { 104 | rext = ext; 105 | } 106 | 107 | if (p->ext && strcmpnull(p->ext, rext) != 0) 108 | continue; 109 | 110 | if (p->type && strcmpnull(p->type, type) != 0) 111 | continue; 112 | 113 | if (p->subtype && strcmpnull(p->subtype, subtype) != 0) 114 | continue; 115 | 116 | return p; 117 | } 118 | 119 | return NULL; 120 | } 121 | 122 | static void check_init_previews(void) 123 | { 124 | if (!previews.list) { 125 | PRINTINTERR("init_previews() not called"); 126 | abort(); 127 | } 128 | } 129 | 130 | static RESULT run(Preview *p, int *exitcode, int *signal) 131 | { 132 | int pipe_fds[2]; 133 | ERRCHK_RET_ERN(pipe(pipe_fds) == -1); 134 | 135 | int sp_arg[] = { pipe_fds[0], pipe_fds[1], STDERR_FILENO }; 136 | 137 | enum Result ret = run_script(p->script, p->script_len, exitcode, signal, spawn_redirect, sp_arg); 138 | 139 | close(pipe_fds[1]); 140 | 141 | if (*exitcode != FAILED_PREVIEW_EC) { 142 | char buf[256]; 143 | int len; 144 | while ((len = read(pipe_fds[0], buf, sizeof(buf))) > 0) { 145 | write(STDOUT_FILENO, buf, len); 146 | } 147 | 148 | if (len == -1) { 149 | FUNCFAILED("read", strerror(errno)); 150 | ret = ERR; 151 | } 152 | } 153 | 154 | close(pipe_fds[0]); 155 | 156 | return ret; 157 | } 158 | 159 | #define SET_PENV(n, v) \ 160 | do { \ 161 | if (v) \ 162 | ERRCHK_RET_ERN(setenv((n), (v), 1) != 0); \ 163 | } while (0) 164 | 165 | RESULT preview_run(const char *ext, const char *mimetype, PreviewArgs *pa) 166 | { 167 | if (pa->id || (pa->id = getenv("id"))) 168 | ERRCHK_RET_OK(server_set_fifo_var(pa->id)); 169 | 170 | SET_PENV("f", pa->f); 171 | SET_PENV("w", pa->w); 172 | SET_PENV("h", pa->h); 173 | SET_PENV("x", pa->x); 174 | SET_PENV("y", pa->y); 175 | SET_PENV("id", pa->id); 176 | SET_PENV("cache_f", pa->cache_file); 177 | SET_PENV("cache_d", pa->cache_dir); 178 | 179 | { 180 | char *s = pa->cache_valid ? "1" : ""; 181 | SET_PENV("cache_valid", s); 182 | } 183 | 184 | SET_PENV("m", mimetype); 185 | SET_PENV("e", ext); 186 | 187 | check_init_previews(); 188 | 189 | Preview *p; 190 | size_t i = 0; 191 | int exitcode, signal; 192 | char mimetype_c[MIMETYPE_MAX], *t, *s; 193 | 194 | strncpy(mimetype_c, mimetype, LEN(mimetype_c) - 1); 195 | break_mimetype(mimetype_c, &t, &s); 196 | 197 | run: 198 | p = find_preview(t, s, ext, &i); 199 | if (!p) { 200 | puts("ctpv: no previews found"); 201 | return ERR; 202 | } 203 | 204 | if (ctpv.debug) 205 | fprintf(stderr, "Running preview: %s\n", p->name); 206 | 207 | ERRCHK_RET_OK(run(p, &exitcode, &signal)); 208 | 209 | switch (exitcode) { 210 | case FAILED_PREVIEW_EC: 211 | if (ctpv.debug) 212 | fprintf(stderr, "Preview %s failed\n", p->name); 213 | i++; 214 | goto run; 215 | case ENOUGH_READ_EC: 216 | exitcode = 0; 217 | break; 218 | } 219 | 220 | if (signal == SIGPIPE) 221 | exitcode = 0; 222 | 223 | return exitcode == 0 ? OK : ERR; 224 | } 225 | 226 | Preview **previews_get(size_t *len) 227 | { 228 | check_init_previews(); 229 | *len = previews.len; 230 | return previews.list; 231 | } 232 | -------------------------------------------------------------------------------- /src/preview.h: -------------------------------------------------------------------------------- 1 | #ifndef PREVIEW_H 2 | #define PREVIEW_H 3 | 4 | #include 5 | 6 | #include "vector.h" 7 | #include "result.h" 8 | 9 | enum PreviewAttr { 10 | PREV_ATTR_NONE = 0, 11 | PREV_ATTR_EXT_SHORT = 1 << 0, 12 | }; 13 | 14 | typedef struct { 15 | char *name, *ext, *type, *subtype, *script; 16 | int order, priority; 17 | enum PreviewAttr attrs; 18 | size_t script_len; 19 | } Preview; 20 | 21 | VECTOR_GEN_HEADER(Preview, Preview) 22 | 23 | typedef struct { 24 | char *f, *w, *h, *x, *y, *id; 25 | char *cache_file, *cache_dir; 26 | int cache_valid; 27 | } PreviewArgs; 28 | 29 | void previews_init(Preview *ps, size_t len); 30 | void previews_cleanup(void); 31 | RESULT preview_run(const char *ext, const char *mimetype, PreviewArgs *pa); 32 | Preview **previews_get(size_t *len); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /src/result.h: -------------------------------------------------------------------------------- 1 | #ifndef RESULT_H 2 | #define RESULT_H 3 | 4 | #include "attrs.h" 5 | 6 | #define RESULT enum Result WARN_UNUSED_RESULT 7 | 8 | enum Result { 9 | OK, 10 | ERR, 11 | }; 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "error.h" 11 | #include "utils.h" 12 | #include "shell.h" 13 | #include "../gen/server.h" 14 | 15 | #define FIFO_FILENAME_SIZE 256 16 | 17 | static int ueberzug_pid; 18 | 19 | static volatile int do_exit = 0; 20 | 21 | static void kill_ueberzug(void) 22 | { 23 | if (kill(ueberzug_pid, SIGTERM) == -1) { 24 | if (errno == ESRCH) 25 | print_error("ueberzug is not running"); 26 | else 27 | FUNCFAILED("kill", strerror(errno)); 28 | } 29 | 30 | waitpid(ueberzug_pid, NULL, 0); 31 | } 32 | 33 | static void sig_handler_exit(int s) 34 | { 35 | do_exit = 1; 36 | } 37 | 38 | static RESULT open_fifo(int *fd, char *f) 39 | { 40 | ERRCHK_RET_ERN((*fd = open(f, O_RDONLY | O_NONBLOCK)) == -1); 41 | 42 | return OK; 43 | } 44 | 45 | static RESULT listen(char *fifo) 46 | { 47 | enum Result ret = OK; 48 | 49 | struct pollfd pollfd = { .fd = -1, .events = POLLIN }; 50 | ERRCHK_GOTO_OK(open_fifo(&pollfd.fd, fifo), ret, exit); 51 | 52 | /* 53 | * We don't register actual handlers because when one occures, 54 | * poll() returns 0, which will break the loop and a normal 55 | * exit will happen. 56 | */ 57 | ERRCHK_GOTO_OK(register_signal(SIGINT, sig_handler_exit), ret, fifo); 58 | ERRCHK_GOTO_OK(register_signal(SIGTERM, sig_handler_exit), ret, fifo); 59 | 60 | int pipe_fds[2]; 61 | ERRCHK_GOTO_ERN(pipe(pipe_fds) == -1, ret, signal); 62 | 63 | char *args[] = { "ueberzug", "layer", NULL }; 64 | int sp_arg[] = { pipe_fds[1], pipe_fds[0], STDIN_FILENO }; 65 | if (spawn(args, &ueberzug_pid, NULL, NULL, spawn_redirect, sp_arg) != OK) 66 | ret = ERR; 67 | 68 | close(pipe_fds[0]); 69 | 70 | /* If spawn() failed */ 71 | if (ret != OK) 72 | goto close; 73 | 74 | /* 75 | * "Listen" to fifo and redirect all the input to ueberzug 76 | * instance. 77 | */ 78 | int poll_ret, len; 79 | while ((poll_ret = poll(&pollfd, 1, 100) >= 0)) { 80 | if (do_exit) 81 | goto close; 82 | 83 | if (poll_ret == 0) 84 | continue; 85 | 86 | if (pollfd.revents & POLLIN) { 87 | static char buf[1024]; 88 | while ((len = read(pollfd.fd, buf, sizeof(buf))) > 0) { 89 | /* But first byte equal to 0 means "exit" */ 90 | if (buf[0] == 0) 91 | goto close; 92 | write(pipe_fds[1], buf, len); 93 | } 94 | } 95 | 96 | if (pollfd.revents & POLLHUP) { 97 | close(pollfd.fd); 98 | ERRCHK_GOTO_OK(open_fifo(&pollfd.fd, fifo), ret, close); 99 | } 100 | } 101 | 102 | 103 | ERRCHK_GOTO_ERN(poll_ret < 0, ret, close); 104 | 105 | close: 106 | close(pipe_fds[1]); 107 | kill_ueberzug(); 108 | 109 | signal: 110 | signal(SIGINT, SIG_DFL); 111 | signal(SIGTERM, SIG_DFL); 112 | 113 | fifo: 114 | if (pollfd.fd >= 0) 115 | close(pollfd.fd); 116 | 117 | exit: 118 | return ret; 119 | } 120 | 121 | static RESULT check_ueberzug(int *exitcode) 122 | { 123 | char *args[] = SHELL_ARGS("command -v ueberzug > /dev/null"); 124 | return spawn(args, NULL, exitcode, NULL, NULL, NULL); 125 | } 126 | 127 | static void get_fifo_name(char *buf, size_t len, const char *id_s) 128 | { 129 | snprintf(buf, len-1, "/tmp/ctpvfifo.%s", id_s); 130 | } 131 | 132 | RESULT server_listen(const char *id_s) 133 | { 134 | enum Result ret = OK; 135 | 136 | int exitcode; 137 | ERRCHK_GOTO_OK(check_ueberzug(&exitcode), ret, exit); 138 | 139 | if (exitcode == 127) { 140 | ret = ERR; 141 | print_error("ueberzug is not installed"); 142 | goto exit; 143 | } 144 | 145 | char fifo[FIFO_FILENAME_SIZE]; 146 | get_fifo_name(fifo, LEN(fifo), id_s); 147 | 148 | if (mkfifo(fifo, 0600) == -1) { 149 | if (errno == EEXIST) 150 | print_errorf("server with id %s is already running or fifo %s still exists", id_s, fifo); 151 | else 152 | FUNCFAILED("mkfifo", strerror(errno)); 153 | ret = ERR; 154 | goto exit; 155 | } 156 | 157 | ERRCHK_GOTO_OK(listen(fifo), ret, fifo); 158 | 159 | fifo: 160 | if (remove(fifo) == -1 && errno != ENOENT) 161 | FUNCFAILED("remove", strerror(errno)); 162 | 163 | exit: 164 | return ret; 165 | } 166 | 167 | static inline RESULT run_server_script(char *script, size_t script_len, char *arg) 168 | { 169 | return run_script(script, script_len, NULL, NULL, NULL, NULL); 170 | } 171 | 172 | RESULT server_set_fifo_var(const char *id_s) 173 | { 174 | char fifo[FIFO_FILENAME_SIZE]; 175 | get_fifo_name(fifo, LEN(fifo), id_s); 176 | ERRCHK_RET_ERN(setenv("fifo", fifo, 1) != 0); 177 | 178 | return OK; 179 | } 180 | 181 | RESULT server_clear(const char *id_s) 182 | { 183 | ERRCHK_RET_OK(server_set_fifo_var(id_s)); 184 | 185 | return run_server_script(scr_clear_sh, LEN(scr_clear_sh), (char *)id_s); 186 | } 187 | 188 | RESULT server_end(const char *id_s) 189 | { 190 | ERRCHK_RET_OK(server_set_fifo_var(id_s)); 191 | 192 | return run_server_script(scr_end_sh, LEN(scr_end_sh), (char *)id_s); 193 | } 194 | -------------------------------------------------------------------------------- /src/server.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVER_H 2 | #define SERVER_H 3 | 4 | #include "result.h" 5 | 6 | RESULT server_listen(const char *id_s); 7 | RESULT server_set_fifo_var(const char *id_s); 8 | RESULT server_clear(const char *id_s); 9 | RESULT server_end(const char *id_s); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /src/shell.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "ctpv.h" 4 | #include "shell.h" 5 | #include "error.h" 6 | #include "../gen/helpers.h" 7 | 8 | /* 9 | * Returns string with sh/helpers.sh prepended 10 | * 11 | * User must call free() 12 | */ 13 | static char *prepend_helpers(char *str, size_t len) 14 | { 15 | char *buf, *b; 16 | size_t l, helpers_len = LEN(scr_helpers_sh) - 1; 17 | 18 | if (!(buf = malloc(sizeof(*buf) * (helpers_len + len)))) { 19 | FUNCFAILED("malloc", strerror(errno)); 20 | abort(); 21 | } 22 | 23 | b = buf; 24 | l = helpers_len; 25 | memcpy(b, scr_helpers_sh, l * sizeof(*b)); 26 | 27 | b += l; 28 | l = len; 29 | memcpy(b, str, l * sizeof(*b)); 30 | 31 | return buf; 32 | } 33 | 34 | #define OPT_SETENV_INT(name) \ 35 | ERRCHK_RET_ERN(setenv((#name), (ctpv.opts.name ? "1" : ""), 1) == -1) 36 | 37 | #define OPT_SETENV_STR(name) \ 38 | ERRCHK_RET_ERN(setenv((#name), (ctpv.opts.name ? ctpv.opts.name : ""), 1) == -1) 39 | 40 | RESULT run_script(char *script, size_t script_len, int *exitcode, int *signal, 41 | SpawnProg sp, void *sp_arg) 42 | { 43 | OPT_SETENV_INT(forcekitty); 44 | OPT_SETENV_INT(forcekittyanim); 45 | OPT_SETENV_INT(forcechafa); 46 | OPT_SETENV_INT(noimages); 47 | OPT_SETENV_INT(nosymlinkinfo); 48 | OPT_SETENV_INT(autochafa); 49 | OPT_SETENV_INT(chafasixel); 50 | OPT_SETENV_INT(showgpg); 51 | OPT_SETENV_STR(shell); 52 | 53 | char *scr = prepend_helpers(script, script_len); 54 | char *args[] = SHELL_ARGS(scr); 55 | enum Result ret = spawn(args, NULL, exitcode, signal, sp, sp_arg); 56 | 57 | free(scr); 58 | 59 | return ret; 60 | } 61 | -------------------------------------------------------------------------------- /src/shell.h: -------------------------------------------------------------------------------- 1 | #ifndef SHELL_H 2 | #define SHELL_H 3 | 4 | #include "utils.h" 5 | #include "ctpv.h" 6 | 7 | #define SHELL_ARGS(script, ...) \ 8 | { ctpv.opts.shell, "-c", script, ctpv.opts.shell, __VA_ARGS__ __VA_OPT__(,) NULL } 9 | 10 | RESULT run_script(char *script, size_t script_len, int *exitcode, int *signal, 11 | SpawnProg sp, void *sp_arg); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/ulist.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "ulist.h" 5 | #include "error.h" 6 | 7 | /* 8 | * Unrolled linked list 9 | */ 10 | 11 | #define DEFAULT_CAP 256 12 | #define NO_LOCK -1 13 | 14 | #define ULIST_BUF(node) ((void *)&(node).buf) 15 | #define ULIST_BUF_AT(list, node, i) (ULIST_BUF(node) + i * (list).size) 16 | 17 | struct UList { 18 | size_t size; 19 | ssize_t lock_i; 20 | struct UListNode *head, *tail; 21 | }; 22 | 23 | struct UListNode { 24 | size_t len, cap; 25 | struct UListNode *next; 26 | void *buf; 27 | }; 28 | 29 | static struct UListNode *ulist_node_new(UList *l, size_t cap) 30 | { 31 | struct UListNode *n; 32 | 33 | if (cap == 0) 34 | cap = DEFAULT_CAP; 35 | 36 | /* Store buffer and node data in the same chunk */ 37 | if (!(n = malloc(sizeof(*n) - sizeof(n->buf) + (cap * l->size)))) { 38 | FUNCFAILED("malloc", strerror(errno)); 39 | abort(); 40 | } 41 | 42 | n->cap = cap; 43 | n->len = 0; 44 | n->next = NULL; 45 | 46 | return n; 47 | } 48 | 49 | UList *ulist_new(size_t size, size_t cap) 50 | { 51 | UList *l; 52 | 53 | if (!(l = malloc(sizeof(*l)))) { 54 | FUNCFAILED("malloc", strerror(errno)); 55 | abort(); 56 | } 57 | 58 | l->size = size; 59 | l->lock_i = NO_LOCK; 60 | l->head = l->tail = ulist_node_new(l, cap); 61 | 62 | return l; 63 | } 64 | 65 | void ulist_free(UList *l) 66 | { 67 | struct UListNode *node = l->head, *next; 68 | 69 | while (node) { 70 | next = node->next; 71 | free(node); 72 | node = next; 73 | } 74 | 75 | free(l); 76 | } 77 | 78 | static inline int is_locked(UList *l) 79 | { 80 | return l->lock_i != NO_LOCK; 81 | } 82 | 83 | void ulist_append_arr(UList *l, void *arr, size_t len) 84 | { 85 | struct UListNode *new, *node = l->tail; 86 | size_t cap = node->cap; 87 | 88 | while (node->len + len > cap) 89 | cap *= 2; 90 | 91 | if (cap != node->cap) { 92 | node->next = new = ulist_node_new(l, cap); 93 | 94 | if (is_locked(l)) { 95 | new->len += node->len - l->lock_i; 96 | memcpy(ULIST_BUF(*new), ULIST_BUF_AT(*l, *node, l->lock_i), 97 | new->len * l->size); 98 | 99 | node->len = l->lock_i; 100 | l->lock_i = 0; 101 | } 102 | 103 | node = l->tail = new; 104 | } 105 | 106 | memcpy(ULIST_BUF_AT(*l, *node, node->len), arr, len * l->size); 107 | node->len += len; 108 | } 109 | 110 | void ulist_append(UList *l, void *val) 111 | { 112 | ulist_append_arr(l, val, 1); 113 | } 114 | 115 | /* 116 | * Ensure that all the elements appended will be placed in memory 117 | * one after another. Useful for storing strings. 118 | */ 119 | void ulist_lock(UList *l) 120 | { 121 | l->lock_i = l->tail->len; 122 | } 123 | 124 | void *ulist_unlock(UList *l) 125 | { 126 | ssize_t i = l->lock_i; 127 | l->lock_i = NO_LOCK; 128 | 129 | return ULIST_BUF_AT(*l, *l->tail, i); 130 | } 131 | -------------------------------------------------------------------------------- /src/ulist.h: -------------------------------------------------------------------------------- 1 | #ifndef ULIST_H 2 | #define ULIST_H 3 | 4 | #include 5 | 6 | /* 7 | * Unrolled linked list 8 | */ 9 | 10 | typedef struct UList UList; 11 | 12 | UList *ulist_new(size_t size, size_t cap); 13 | void ulist_free(UList *l); 14 | void ulist_append_arr(UList *l, void *arr, size_t len); 15 | void ulist_append(UList *l, void *val); 16 | void ulist_lock(UList *l); 17 | void *ulist_unlock(UList *l); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /src/utils.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "error.h" 11 | #include "utils.h" 12 | 13 | char *program = NULL; 14 | 15 | RESULT spawn_redirect(const void *arg) 16 | { 17 | int *fds = (int *)arg; 18 | 19 | ERRCHK_RET_ERN(close(fds[0]) == -1); 20 | ERRCHK_RET_ERN(dup2(fds[1], fds[2]) == -1); 21 | 22 | return OK; 23 | } 24 | 25 | RESULT spawn_wait(int pid, int *exitcode, int *signal) 26 | { 27 | int stat; 28 | ERRCHK_RET_ERN(waitpid(pid, &stat, 0) == -1); 29 | 30 | if (exitcode) 31 | *exitcode = -1; 32 | 33 | if (signal) 34 | *signal = -1; 35 | 36 | if (WIFEXITED(stat)) { 37 | if (exitcode) 38 | *exitcode = WEXITSTATUS(stat); 39 | } else if (WIFSIGNALED(stat)) { 40 | if (signal) 41 | *signal = WTERMSIG(stat); 42 | } 43 | 44 | return OK; 45 | } 46 | 47 | /* 48 | * Run command 49 | * 50 | * If cpid is NULL, wait for the command to finish executing; 51 | * otherwise store pid in cpid 52 | * 53 | * cfunc is a function to call when child process is created 54 | */ 55 | RESULT spawn(char *args[], int *cpid, int *exitcode, int *signal, 56 | SpawnProg cfunc, const void *carg) 57 | { 58 | if (exitcode) 59 | *exitcode = -1; 60 | 61 | int pid = fork(); 62 | ERRCHK_RET_ERN(pid == -1); 63 | 64 | /* Child process */ 65 | if (pid == 0) { 66 | if (cfunc && (cfunc(carg) != OK)) 67 | exit(EXIT_FAILURE); 68 | 69 | execvp(args[0], args); 70 | if (errno == ENOENT) 71 | exit(NOTEXIST_EC); 72 | 73 | FUNCFAILED("exec", strerror(errno)); 74 | exit(EXIT_FAILURE); 75 | } 76 | 77 | if (cpid) 78 | *cpid = pid; 79 | 80 | if (exitcode || signal) 81 | ERRCHK_RET_OK(spawn_wait(pid, exitcode, signal)); 82 | 83 | return OK; 84 | } 85 | 86 | int strcmpnull(const char *s1, const char *s2) 87 | { 88 | if (!s1 && !s2) 89 | return 0; 90 | else if (s1 && !s2) 91 | return 1; 92 | else if (!s1 && s2) 93 | return -1; 94 | 95 | return strcmp(s1, s2); 96 | } 97 | 98 | int strlennull(const char *s) 99 | { 100 | return s ? strlen(s) : 0; 101 | } 102 | 103 | static RESULT get_xdg_dir(char *buf, size_t len, char *var, char *var_sub, char *name) 104 | { 105 | char *home, *dir, dir_buf[FILENAME_MAX]; 106 | 107 | if (!(dir = getenv(var))) { 108 | home = getenv("HOME"); 109 | ERRCHK_RET(!home, "HOME env var does not exist"); 110 | 111 | snprintf(dir_buf, LEN(dir_buf)-1, "%s/%s", home, var_sub); 112 | dir = dir_buf; 113 | } 114 | 115 | snprintf(buf, len - 1, "%s/%s", dir, name); 116 | return OK; 117 | } 118 | 119 | RESULT get_cache_dir(char *buf, size_t len, char *name) 120 | { 121 | return get_xdg_dir(buf, len, "XDG_CACHE_HOME", ".cache", name); 122 | } 123 | 124 | RESULT get_config_dir(char *buf, size_t len, char *name) 125 | { 126 | return get_xdg_dir(buf, len, "XDG_CONFIG_HOME", ".config", name); 127 | } 128 | 129 | int mkpath(char* file_path, int mode) 130 | { 131 | for (char* p = strchr(file_path + 1, '/'); p; p = strchr(p + 1, '/')) { 132 | *p = '\0'; 133 | 134 | if (mkdir(file_path, mode) == -1) { 135 | if (errno != EEXIST) { 136 | *p = '/'; 137 | return -1; 138 | } 139 | } 140 | 141 | *p = '/'; 142 | } 143 | 144 | return 0; 145 | } 146 | 147 | const char *get_ext(const char *path) 148 | { 149 | const char *dot = NULL, *s = path + strlen(path) - 1; 150 | 151 | for (; ; s--) { 152 | if (*s == '.') { 153 | dot = s + 1; 154 | continue; 155 | } 156 | 157 | if (!isalnum(*s) || s == path) 158 | break; 159 | } 160 | 161 | return dot; 162 | } 163 | 164 | RESULT register_signal(int sig, SigHandler handler) 165 | { 166 | ERRCHK_RET_ERN(signal(sig, handler) == SIG_ERR); 167 | return OK; 168 | } 169 | 170 | RESULT strtol_w(long *res, char *s, char **endptr, int base) 171 | { 172 | char *endptr_int; 173 | 174 | errno = 0; 175 | *res = strtol(s, &endptr_int, base); 176 | 177 | if (errno != 0) { 178 | FUNCFAILED("strtol", strerror(errno)); 179 | return ERR; 180 | } 181 | 182 | if (endptr_int[0] != '\0') { 183 | PRINTINTERR("strtol: invalid number %s", s); 184 | return ERR; 185 | } 186 | 187 | if (endptr) 188 | *endptr = endptr_int; 189 | 190 | return OK; 191 | } 192 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | #include 5 | #include 6 | 7 | #include "result.h" 8 | 9 | #define NOTEXIST_EC 127 10 | 11 | #define LEN(a) (sizeof(a) / sizeof((a)[0])) 12 | #define MAX(a, b) ((a) > (b) ? (a) : (b)) 13 | #define MIN(a, b) ((a) < (b) ? (a) : (b)) 14 | 15 | #define STRINGIZE(x) STRINGIZE2(x) 16 | #define STRINGIZE2(x) #x 17 | 18 | #define FORMATTED_STRING(arr, format) \ 19 | do { \ 20 | va_list args; \ 21 | va_start(args, (format)); \ 22 | vsnprintf((arr), LEN(arr) - 1, (format), args); \ 23 | va_end(args); \ 24 | } while (0) 25 | 26 | typedef enum Result (*SpawnProg)(const void *); 27 | 28 | typedef void (*SigHandler)(int); 29 | 30 | extern char *program; 31 | 32 | RESULT spawn_redirect(const void *arg); 33 | RESULT spawn_wait(int pid, int *exitcode, int *signal); 34 | RESULT spawn(char *args[], int *cpid, int *exitcode, int *signal, 35 | SpawnProg cfunc, const void *carg); 36 | 37 | int strcmpnull(const char *s1, const char *s2); 38 | int strlennull(const char *s); 39 | 40 | RESULT get_cache_dir(char *buf, size_t len, char *name); 41 | RESULT get_config_dir(char *buf, size_t len, char *name); 42 | 43 | int mkpath(char* file_path, int mode); 44 | const char *get_ext(const char *path); 45 | 46 | RESULT register_signal(int sig, SigHandler handler); 47 | RESULT strtol_w(long *res, char *s, char **endptr, int base); 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /src/vector.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "error.h" 4 | #include "vector.h" 5 | 6 | #define DEFAULT_CAP 16 7 | 8 | VECTOR_GEN_SOURCE(Char, char) 9 | 10 | Vector *vector_new(size_t size, size_t cap) 11 | { 12 | Vector *v; 13 | 14 | if (cap == 0) 15 | cap = DEFAULT_CAP; 16 | 17 | if (!(v = malloc(sizeof(*v)))) { 18 | FUNCFAILED("malloc", strerror(errno)); 19 | abort(); 20 | } 21 | 22 | if (!(v->buf = malloc(size * cap))) { 23 | FUNCFAILED("malloc", strerror(errno)); 24 | abort(); 25 | } 26 | 27 | v->size = size; 28 | v->cap = cap; 29 | v->len = 0; 30 | 31 | return v; 32 | } 33 | 34 | void vector_free(Vector *vec) 35 | { 36 | free(vec->buf); 37 | free(vec); 38 | } 39 | 40 | static void resize_if_needed(Vector *vec, size_t new_len) 41 | { 42 | void *p; 43 | size_t cap = vec->cap; 44 | 45 | while (new_len > cap) 46 | cap *= 2; 47 | 48 | if (cap == vec->cap) 49 | return; 50 | 51 | if (!(p = realloc(vec->buf, vec->size * cap))) { 52 | vector_free(vec); 53 | FUNCFAILED("realloc", strerror(errno)); 54 | abort(); 55 | } 56 | 57 | vec->buf = p; 58 | vec->cap = cap; 59 | } 60 | 61 | size_t vector_append_arr(Vector *vec, void *arr, size_t len) 62 | { 63 | size_t old_len = vec->len; 64 | 65 | resize_if_needed(vec, vec->len + len); 66 | 67 | memcpy(vec->buf + vec->len * vec->size, arr, len * vec->size); 68 | vec->len += len; 69 | 70 | return old_len; 71 | } 72 | 73 | size_t vector_append(Vector *vec, void *val) 74 | { 75 | return vector_append_arr(vec, val, 1); 76 | } 77 | 78 | void *vector_get(Vector *vec, size_t i) 79 | { 80 | return vec->buf + i * vec->size; 81 | } 82 | 83 | void vector_resize(Vector *vec, size_t len) 84 | { 85 | resize_if_needed(vec, len); 86 | 87 | vec->len = len; 88 | } 89 | 90 | void vector_remove(Vector *vec, size_t i) 91 | { 92 | memcpy(vec->buf + i * vec->size, vec->buf + (--vec->len) * vec->size, 93 | vec->size); 94 | } 95 | -------------------------------------------------------------------------------- /src/vector.h: -------------------------------------------------------------------------------- 1 | #ifndef VECTOR_H 2 | #define VECTOR_H 3 | 4 | #include 5 | 6 | #include "attrs.h" 7 | 8 | #define VECTOR_TYPE(name, type) \ 9 | typedef struct { \ 10 | size_t len, cap, size; \ 11 | type *buf; \ 12 | } Vector##name 13 | 14 | #define VECTOR_SIGN(name, type, func, ret, ...) \ 15 | ret vector##name##_##func(__VA_ARGS__) 16 | 17 | #define VECTOR_SIGN_V(name, type, func, ret, ...) \ 18 | VECTOR_SIGN(name, type, func, ret, \ 19 | Vector##name *vec __VA_OPT__(, ) __VA_ARGS__) 20 | 21 | #define VECTOR_SIGN_NEW(name, type) VECTOR_SIGN(name, type, new, Vector##name *, size_t cap) 22 | #define VECTOR_SIGN_FREE(name, type) VECTOR_SIGN_V(name, type, free, void) 23 | #define VECTOR_SIGN_APPEND_ARR(name, type) VECTOR_SIGN_V(name, type, append_arr, size_t, type *arr, size_t len) 24 | #define VECTOR_SIGN_APPEND(name, type) VECTOR_SIGN_V(name, type, append, size_t, type val) 25 | #define VECTOR_SIGN_GET(name, type) VECTOR_SIGN_V(name, type, get, type, size_t i) 26 | #define VECTOR_SIGN_RESIZE(name, type) VECTOR_SIGN_V(name, type, resize, void, size_t len) 27 | #define VECTOR_SIGN_REMOVE(name, type) VECTOR_SIGN_V(name, type, remove, void, size_t i) 28 | 29 | #define VECTOR_GEN_SOURCE_(name, type, spec) \ 30 | inline spec VECTOR_SIGN_NEW(name, type) \ 31 | { \ 32 | return (Vector##name *)vector_new(sizeof(type), cap); \ 33 | } \ 34 | inline spec VECTOR_SIGN_FREE(name, type) \ 35 | { \ 36 | vector_free((Vector *)vec); \ 37 | } \ 38 | inline spec VECTOR_SIGN_APPEND_ARR(name, type) \ 39 | { \ 40 | return vector_append_arr((Vector *)vec, arr, len); \ 41 | } \ 42 | inline spec VECTOR_SIGN_APPEND(name, type) \ 43 | { \ 44 | return vector_append((Vector *)vec, &val); \ 45 | } \ 46 | inline spec VECTOR_SIGN_GET(name, type) \ 47 | { \ 48 | return *(type *)vector_get((Vector *)vec, i); \ 49 | } \ 50 | inline spec VECTOR_SIGN_RESIZE(name, type) \ 51 | { \ 52 | vector_resize((Vector *)vec, len); \ 53 | } \ 54 | inline spec VECTOR_SIGN_REMOVE(name, type) \ 55 | { \ 56 | vector_remove((Vector *)vec, i); \ 57 | } 58 | 59 | #define VECTOR_GEN_SOURCE(name, type) VECTOR_GEN_SOURCE_(name, type, ) 60 | #define VECTOR_GEN_SOURCE_STATIC(name, type) \ 61 | VECTOR_TYPE(name, type); \ 62 | VECTOR_GEN_SOURCE_(name, type, static UNUSED) 63 | 64 | #define VECTOR_GEN_HEADER(name, type) \ 65 | VECTOR_TYPE(name, type); \ 66 | VECTOR_SIGN_NEW(name, type); \ 67 | VECTOR_SIGN_FREE(name, type); \ 68 | VECTOR_SIGN_APPEND_ARR(name, type); \ 69 | VECTOR_SIGN_APPEND(name, type); \ 70 | VECTOR_SIGN_GET(name, type); \ 71 | VECTOR_SIGN_RESIZE(name, type); \ 72 | VECTOR_SIGN_REMOVE(name, type); 73 | 74 | VECTOR_TYPE(, void); 75 | 76 | Vector *vector_new(size_t size, size_t cap); 77 | void vector_free(Vector *vec); 78 | size_t vector_append_arr(Vector *vec, void *arr, size_t len); 79 | size_t vector_append(Vector *vec, void *arr); 80 | void *vector_get(Vector *vec, size_t i); 81 | void vector_resize(Vector *vec, size_t len); 82 | void vector_remove(Vector *vec, size_t i); 83 | 84 | VECTOR_GEN_HEADER(Char, char) 85 | 86 | #endif 87 | -------------------------------------------------------------------------------- /version.h: -------------------------------------------------------------------------------- 1 | #define VERSION "1.1" 2 | --------------------------------------------------------------------------------