Squashed 'clipmenu/' content from commit e0ec3bc6
git-subtree-dir: clipmenu git-subtree-split: e0ec3bc6608ecc70ed17e8da76e27bf90b1e8e90
This commit is contained in:
commit
b4e3116f02
32 changed files with 5760 additions and 0 deletions
11
.clang-format
Normal file
11
.clang-format
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
BinPackArguments: true
|
||||||
|
BreakStringLiterals: false
|
||||||
|
ColumnLimit: 80
|
||||||
|
IndentCaseLabels: true
|
||||||
|
IndentPPDirectives: BeforeHash
|
||||||
|
IndentWidth: 4
|
||||||
|
InsertBraces: true
|
||||||
|
PenaltyBreakComment: 0
|
||||||
|
SortIncludes: true
|
||||||
|
SpaceBeforeParens: ControlStatementsExceptForEachMacros
|
||||||
|
UseTab: Never
|
||||||
28
.github/workflows/ci.yml
vendored
Normal file
28
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
jobs:
|
||||||
|
build_and_test:
|
||||||
|
name: CI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- run: sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
|
||||||
|
- run: sudo apt-get update
|
||||||
|
|
||||||
|
- uses: awalsh128/cache-apt-pkgs-action@v1
|
||||||
|
with:
|
||||||
|
packages: gcc-10 clang clang-format clang-tidy cppcheck xvfb xsel libxfixes-dev libx11-dev
|
||||||
|
version: 1.0
|
||||||
|
|
||||||
|
- run: gcc --version
|
||||||
|
|
||||||
|
- run: make clean analyse
|
||||||
|
- run: make tests
|
||||||
|
- run: make integration_tests
|
||||||
|
env:
|
||||||
|
NO_PID_NAMESPACE: 1
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
src/clipmenud
|
||||||
|
src/clipctl
|
||||||
|
src/clipdel
|
||||||
|
src/clipdelmenu
|
||||||
|
src/clipmenu
|
||||||
|
src/clipserve
|
||||||
|
tests/test
|
||||||
|
tests/test_store
|
||||||
|
*.o
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2014-present Christopher Down
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
135
Makefile
Normal file
135
Makefile
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
CFLAGS := -std=gnu11 -O2 -Wall -Wextra -Wshadow -Wpointer-arith \
|
||||||
|
-Wcast-align -Wmissing-prototypes -Wstrict-overflow -Wformat=2 \
|
||||||
|
-Wwrite-strings -Warray-bounds -Wstrict-prototypes \
|
||||||
|
-Wno-maybe-uninitialized \
|
||||||
|
-Werror $(CFLAGS)
|
||||||
|
CPPFLAGS += -I/usr/X11R6/include -L/usr/X11R6/lib
|
||||||
|
LDLIBS += -lX11 -lXfixes
|
||||||
|
PREFIX ?= /usr/local
|
||||||
|
bindir := $(PREFIX)/bin
|
||||||
|
datarootdir := $(PREFIX)/share
|
||||||
|
mandir := $(datarootdir)/man
|
||||||
|
systemd_user_dir = $(DESTDIR)$(PREFIX)/lib/systemd/user
|
||||||
|
debug_cflags := -D_FORTIFY_SOURCE=2 -fsanitize=leak -fsanitize=address \
|
||||||
|
-fsanitize=undefined -Og -ggdb -fno-omit-frame-pointer \
|
||||||
|
-fstack-protector-strong
|
||||||
|
c_files := $(wildcard src/*.c)
|
||||||
|
h_files := $(wildcard src/*.h)
|
||||||
|
libs := $(filter $(c_files:.c=.o), $(h_files:.h=.o))
|
||||||
|
|
||||||
|
man1_files = clipctl.1 clipdel.1 clipdelmenu.1 clipmenu.1 clipmenud.1 clipserve.1
|
||||||
|
man5_files = clipmenu.conf.5
|
||||||
|
|
||||||
|
bins := clipctl clipmenud clipdel clipdelmenu clipserve clipmenu
|
||||||
|
|
||||||
|
all: $(addprefix src/,$(bins))
|
||||||
|
|
||||||
|
src/%: src/%.c $(libs)
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) $^ $(LDFLAGS) $(LDLIBS) -o $@
|
||||||
|
|
||||||
|
src/%.o: src/%.c src/%.h
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
debug: all
|
||||||
|
debug: CFLAGS+=$(debug_cflags)
|
||||||
|
|
||||||
|
install: all
|
||||||
|
@for f in $(man1_files); do \
|
||||||
|
install -Dp -m 644 man/$$f $(DESTDIR)$(mandir)/man1/$$f; \
|
||||||
|
done
|
||||||
|
@for f in $(man5_files); do \
|
||||||
|
install -Dp -m 644 man/$$f $(DESTDIR)$(mandir)/man5/$$f; \
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p $(DESTDIR)$(bindir)/
|
||||||
|
install -pt $(DESTDIR)$(bindir)/ $(addprefix src/,$(bins))
|
||||||
|
mkdir -p $(systemd_user_dir)
|
||||||
|
sed 's|@bindir@|$(bindir)|g' init/clipmenud.service.in > $(systemd_user_dir)/clipmenud.service
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -f $(addprefix $(DESTDIR)$(PREFIX)/bin/,$(bins))
|
||||||
|
rm -f "$(DESTDIR)${PREFIX}/lib/systemd/user/clipmenud.service"
|
||||||
|
rm -f $(addprefix $(DESTDIR)$(mandir)/man1/,$(man1_files))
|
||||||
|
rm -f $(addprefix $(DESTDIR)$(mandir)/man5/,$(man5_files))
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f src/*.o src/*~ $(addprefix src/,$(bins)) tests/test_store
|
||||||
|
|
||||||
|
clang_supports_unsafe_buffer_usage := $(shell clang -x c -c /dev/null -o /dev/null -Werror -Wunsafe-buffer-usage > /dev/null 2>&1; echo $$?)
|
||||||
|
ifeq ($(clang_supports_unsafe_buffer_usage),0)
|
||||||
|
extra_clang_flags := -Wno-unsafe-buffer-usage -Wno-missing-include-dirs \
|
||||||
|
-Wno-unknown-warning-option \
|
||||||
|
-Wno-unused-command-line-argument \
|
||||||
|
-Wno-error
|
||||||
|
else
|
||||||
|
extra_clang_flags := -Wno-missing-include-dirs \
|
||||||
|
-Wno-unknown-warning-option \
|
||||||
|
-Wno-unused-command-line-argument \
|
||||||
|
-Wno-error
|
||||||
|
endif
|
||||||
|
|
||||||
|
c_analyse_targets := $(c_files:%=%-analyse)
|
||||||
|
h_analyse_targets := $(h_files:%=%-analyse)
|
||||||
|
|
||||||
|
analyse: CFLAGS+=$(debug_cflags)
|
||||||
|
analyse: cppcheck $(c_analyse_targets) $(h_analyse_targets)
|
||||||
|
|
||||||
|
$(c_analyse_targets): %-analyse: %
|
||||||
|
# -W options here are not clang compatible, so out of generic CFLAGS
|
||||||
|
gcc $< -o /dev/null -c \
|
||||||
|
-std=gnu99 -Ofast -fwhole-program -Wall -Wextra \
|
||||||
|
-Wlogical-op -Wduplicated-cond \
|
||||||
|
-fanalyzer $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(LDLIBS)
|
||||||
|
clang $< -o /dev/null -c -std=gnu99 -Ofast \
|
||||||
|
$(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(LDLIBS) \
|
||||||
|
$(extra_clang_flags)
|
||||||
|
$(MAKE) $*-shared-analyse
|
||||||
|
|
||||||
|
$(h_analyse_targets): %-analyse:
|
||||||
|
$(MAKE) $*-shared-analyse
|
||||||
|
|
||||||
|
%-shared-analyse: %
|
||||||
|
# clang-analyzer-unix.Malloc does not understand _drop_()
|
||||||
|
clang-tidy $< --quiet -checks=-clang-analyzer-unix.Malloc -- -std=gnu99
|
||||||
|
clang-format --dry-run --Werror $<
|
||||||
|
|
||||||
|
# --suppress=missingIncludeSystem:
|
||||||
|
#
|
||||||
|
# Without this there's a bunch of noise from cppcheck from the system headers
|
||||||
|
# themselves.
|
||||||
|
#
|
||||||
|
# --suppress=unusedFunction:
|
||||||
|
#
|
||||||
|
# cppcheck does not understand _drop_ and marks those as unused. This is
|
||||||
|
# already well checked by Clang/GCC, just leave it to them.
|
||||||
|
#
|
||||||
|
# --suppress=unmatchedSuppression:
|
||||||
|
#
|
||||||
|
# We run both locally and on CI, so there may be some suppressions that
|
||||||
|
# depending on version do not match in one version but do on another.
|
||||||
|
#
|
||||||
|
# --suppress=unusedStructMember
|
||||||
|
#
|
||||||
|
# Structs are used across translation units and cppcheck gets this wrong.
|
||||||
|
cppcheck: $(c_files) $(h_files)
|
||||||
|
cppcheck $(c_files) $(h_files) --std=c99 --quiet --inline-suppr --force \
|
||||||
|
--enable=all \
|
||||||
|
--suppress=missingIncludeSystem \
|
||||||
|
--suppress=unusedFunction \
|
||||||
|
--suppress=unmatchedSuppression \
|
||||||
|
--suppress=checkersReport \
|
||||||
|
--suppress=unusedStructMember \
|
||||||
|
--check-level=exhaustive \
|
||||||
|
--max-ctu-depth=10 --error-exitcode=1
|
||||||
|
|
||||||
|
tests: tests/test_store
|
||||||
|
tests/test_store
|
||||||
|
|
||||||
|
integration_tests:
|
||||||
|
tests/x_integration_tests
|
||||||
|
|
||||||
|
tests/test_store: tests/test_store.c src/store.o src/util.o
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -I./src -o $@ $^ $(LDLIBS)
|
||||||
|
|
||||||
|
.PHONY: all debug install uninstall clean analyse tests integration_tests \
|
||||||
|
cppcheck
|
||||||
97
README.md
Normal file
97
README.md
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
clipmenu is a simple clipboard manager using [dmenu][], [rofi][] or similar.
|
||||||
|
|
||||||
|
# Demo
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
## clipmenud
|
||||||
|
|
||||||
|
Start `clipmenud`, then run `clipmenu` to select something to put on the
|
||||||
|
clipboard. For systemd users, a user service called `clipmenud` is packaged as
|
||||||
|
part of the project.
|
||||||
|
|
||||||
|
If you start X via `startx`/`xinit` and use the systemd user service, make sure
|
||||||
|
`$DISPLAY` is set so clipmenud knows which X server to use. Most distributions
|
||||||
|
include scripts in `/etc/X11/xinit/xinitrc.d/` for this, so if they are present
|
||||||
|
you can usually just source them in your `~/.xinitrc`. Alternatively, before
|
||||||
|
launching the clipmenud service, run:
|
||||||
|
|
||||||
|
systemctl --user import-environment DISPLAY
|
||||||
|
|
||||||
|
## clipmenu
|
||||||
|
|
||||||
|
You may wish to bind a shortcut in your window manager to launch `clipmenu`.
|
||||||
|
|
||||||
|
All args passed to clipmenu are transparently dispatched to dmenu. That is, if
|
||||||
|
you usually call dmenu with args to set colours and other properties, you can
|
||||||
|
invoke clipmenu in exactly the same way to get the same effect, like so:
|
||||||
|
|
||||||
|
clipmenu -i -fn Terminus:size=8 -nb '#002b36' -nf '#839496' -sb '#073642' -sf '#93a1a1'
|
||||||
|
|
||||||
|
By default, clipmenu also adds dmenu-compatible defaults such as `-p` and `-l`.
|
||||||
|
If that gets in the way for a custom launcher, set `launcher_pass_dmenu_args` to
|
||||||
|
0 in `clipmenu.conf`.
|
||||||
|
|
||||||
|
For a full list of configuration options (and their environment variable
|
||||||
|
equivalents), please see `man clipmenu.conf`.
|
||||||
|
|
||||||
|
There is also `clipdel` to delete clips, `clipdelmenu` for interactive deletion,
|
||||||
|
and `clipctl` to enable or disable clipboard monitoring.
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
The behaviour of `clipmenud` can be customised through a config file. As some
|
||||||
|
examples of things you can change:
|
||||||
|
|
||||||
|
* Customising the maximum number of clips stored (default 1000)
|
||||||
|
* Disabling clip collection temporarily with `clipctl disable`, reenabling with
|
||||||
|
`clipctl enable`
|
||||||
|
* Deduplicating repeated clips so only the newest remains
|
||||||
|
* Not storing clipboard changes from certain applications, like password
|
||||||
|
managers
|
||||||
|
* Taking direct ownership of the clipboard
|
||||||
|
* ...and much more.
|
||||||
|
|
||||||
|
See `man clipmenu.conf` to view all possible configuration variables and what
|
||||||
|
they do.
|
||||||
|
|
||||||
|
# Supported launchers
|
||||||
|
|
||||||
|
Any dmenu-compliant application will work, but here are `CM_LAUNCHER`
|
||||||
|
configurations that are known to work:
|
||||||
|
|
||||||
|
- `dmenu` (the default)
|
||||||
|
- `fzf`
|
||||||
|
- `rofi`
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
Several distributions, including Arch and Nix, provide clipmenu as an official
|
||||||
|
package called `clipmenu`.
|
||||||
|
|
||||||
|
## Manual installation
|
||||||
|
|
||||||
|
If your distribution doesn't provide a package, you can manually install using
|
||||||
|
`make install` (or better yet, create a package for your distribution!).
|
||||||
|
|
||||||
|
# How does it work?
|
||||||
|
|
||||||
|
## clipmenud
|
||||||
|
|
||||||
|
1. clipmenud passively monitors X11 clipboard selections (PRIMARY, CLIPBOARD,
|
||||||
|
and SECONDARY) for changes using XFixes (no polling).
|
||||||
|
2. If `clipmenud` detects changes to the clipboard contents, it writes them out
|
||||||
|
to storage and indexes using a hash as the filename.
|
||||||
|
|
||||||
|
## clipmenu
|
||||||
|
|
||||||
|
1. `clipmenu` reads the index to find all available clips.
|
||||||
|
2. `dmenu` (or another configured launcher) is executed to allow the user to
|
||||||
|
select a clip.
|
||||||
|
3. After selection, the clip is put onto the PRIMARY and CLIPBOARD X
|
||||||
|
selections.
|
||||||
|
|
||||||
|
[dmenu]: http://tools.suckless.org/dmenu/
|
||||||
|
[rofi]: https://github.com/DaveDavenport/Rofi
|
||||||
21
init/clipmenud.service.in
Normal file
21
init/clipmenud.service.in
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Clipmenu daemon
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=@bindir@/clipmenud
|
||||||
|
Restart=always
|
||||||
|
RestartSec=500ms
|
||||||
|
|
||||||
|
MemoryDenyWriteExecute=yes
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
ProtectControlGroups=yes
|
||||||
|
ProtectKernelTunables=yes
|
||||||
|
RestrictAddressFamilies=
|
||||||
|
RestrictRealtime=yes
|
||||||
|
|
||||||
|
# We don't need to do any clean up, so if something hangs (borked X server,
|
||||||
|
# etc), it's going to stay that way. Just forcefully kill and get it over with.
|
||||||
|
TimeoutStopSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
56
man/clipctl.1
Normal file
56
man/clipctl.1
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
.TH CLIPCTL 1
|
||||||
|
.SH NAME
|
||||||
|
clipctl \- control the clipmenud daemon
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B clipctl
|
||||||
|
ACTION
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B clipctl
|
||||||
|
communicates with the clipmenud daemon to control clipboard collection. It
|
||||||
|
sends a signal to clipmenud so that clipboard monitoring is enabled, disabled,
|
||||||
|
or toggled, or it simply displays the current status.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B enable
|
||||||
|
Instruct clipmenud to enable clipboard collection.
|
||||||
|
.TP
|
||||||
|
.B disable
|
||||||
|
Instruct clipmenud to disable clipboard collection.
|
||||||
|
.TP
|
||||||
|
.B toggle
|
||||||
|
Invert the current state of clipboard collection.
|
||||||
|
.TP
|
||||||
|
.B status
|
||||||
|
Print the current state of clipboard collection.
|
||||||
|
.TP
|
||||||
|
.B version
|
||||||
|
Print the major clipmenu version.
|
||||||
|
.TP
|
||||||
|
.B cache-dir
|
||||||
|
Print the cache directory used by clipmenud.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
clipctl also accepts one dash option:
|
||||||
|
|
||||||
|
.B \-h, \--help
|
||||||
|
Display the help message (invokes the manual page).
|
||||||
|
.SH CONFIGURATION
|
||||||
|
See
|
||||||
|
.BR clipmenu.conf (5).
|
||||||
|
.SH DEPENDENCIES
|
||||||
|
clipctl requires a running instance of the
|
||||||
|
.BR clipmenud
|
||||||
|
daemon.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR clipdel (1),
|
||||||
|
.BR clipmenu (1),
|
||||||
|
.BR clipmenud (1),
|
||||||
|
.BR clipmenu.conf (5)
|
||||||
|
.SH AUTHOR
|
||||||
|
Chris Down
|
||||||
|
.MT chris@chrisdown.name
|
||||||
|
.ME
|
||||||
|
.SH REPORTING BUGS
|
||||||
|
Please send bug reports to
|
||||||
|
.UR https://github.com/cdown/clipmenu/issues
|
||||||
|
.UE .
|
||||||
53
man/clipdel.1
Normal file
53
man/clipdel.1
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
.TH CLIPDEL 1
|
||||||
|
.SH NAME
|
||||||
|
clipdel \- delete entries from the clip store
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B clipdel
|
||||||
|
[OPTION...] PATTERN
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B clipdel
|
||||||
|
removes clipboard entries from the clip store managed by clipmenu. By default,
|
||||||
|
it performs a dry-run and prints any matching entries. With the -d flag,
|
||||||
|
matching entries are permanently removed.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-d
|
||||||
|
Real deletion mode. Matching clipboard entries will be removed.
|
||||||
|
.TP
|
||||||
|
.B \-F
|
||||||
|
Perform a literal (fixed-string) match instead of interpreting the pattern as a regular expression.
|
||||||
|
.TP
|
||||||
|
.B \-N
|
||||||
|
Interpret PATTERN as the amount of entries to delete starting from the oldest.
|
||||||
|
E.g
|
||||||
|
.I "\-N 1"
|
||||||
|
deletes the oldest entry.
|
||||||
|
.TP
|
||||||
|
.B \-n
|
||||||
|
Same as
|
||||||
|
.B \-N
|
||||||
|
but starting from newest.
|
||||||
|
.TP
|
||||||
|
.B \-v
|
||||||
|
Invert the matching condition; entries that do not match the given pattern are selected for deletion.
|
||||||
|
.TP
|
||||||
|
.B \-h, \--help
|
||||||
|
Display the help message (invokes the manual page).
|
||||||
|
.SH CONFIGURATION
|
||||||
|
See
|
||||||
|
.BR clipmenu.conf (5).
|
||||||
|
.SH DEPENDENCIES
|
||||||
|
clipdel requires access to the clip store directory as defined in the configuration.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR clipctl (1),
|
||||||
|
.BR clipmenu (1),
|
||||||
|
.BR clipmenud (1),
|
||||||
|
.BR clipmenu.conf (5)
|
||||||
|
.SH AUTHOR
|
||||||
|
Chris Down
|
||||||
|
.MT chris@chrisdown.name
|
||||||
|
.ME
|
||||||
|
.SH REPORTING BUGS
|
||||||
|
Please send bug reports to
|
||||||
|
.UR https://github.com/cdown/clipmenu/issues
|
||||||
|
.UE .
|
||||||
43
man/clipdelmenu.1
Normal file
43
man/clipdelmenu.1
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
.TH CLIPDELMENU 1
|
||||||
|
.SH NAME
|
||||||
|
clipdelmenu \- interactive launcher for deleting clipboard entries
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B clipdelmenu
|
||||||
|
[OPTION...]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B clipdelmenu
|
||||||
|
provides an interactive interface for browsing and deleting clipboard entries
|
||||||
|
stored by clipmenud. It launches a menu using a configured launcher
|
||||||
|
(e.g., dmenu, rofi, or another custom command) and displays a numbered list of
|
||||||
|
stored clips. Once a selection is made, the chosen clip(s) are permanently
|
||||||
|
deleted from the clip store. Multiple clips can be selected at once using the
|
||||||
|
launcher's multi-select feature (e.g., Ctrl-Enter in dmenu).
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-h, \--help
|
||||||
|
Display the help message (invokes the manual page).
|
||||||
|
|
||||||
|
Other than that, command-line arguments are passed directly to the underlying
|
||||||
|
launcher.
|
||||||
|
.SH CONFIGURATION
|
||||||
|
See
|
||||||
|
.BR clipmenu.conf (5).
|
||||||
|
.SH DEPENDENCIES
|
||||||
|
clipdelmenu requires a launcher capable of a dmenu-like interface (e.g., rofi or a
|
||||||
|
custom command). Population of the clipstore requires
|
||||||
|
.BR clipmenud
|
||||||
|
to be running.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR clipctl (1),
|
||||||
|
.BR clipdel (1),
|
||||||
|
.BR clipmenu (1),
|
||||||
|
.BR clipmenud (1),
|
||||||
|
.BR clipmenu.conf (5)
|
||||||
|
.SH AUTHOR
|
||||||
|
Chris Down
|
||||||
|
.MT chris@chrisdown.name
|
||||||
|
.ME
|
||||||
|
.SH REPORTING BUGS
|
||||||
|
Please send bug reports to
|
||||||
|
.UR https://github.com/cdown/clipmenu/issues
|
||||||
|
.UE .
|
||||||
42
man/clipmenu.1
Normal file
42
man/clipmenu.1
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
.TH CLIPMENU 1
|
||||||
|
.SH NAME
|
||||||
|
clipmenu \- interactive launcher for clipboard entry selection
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B clipmenu
|
||||||
|
[OPTION...]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B clipmenu
|
||||||
|
provides an interactive interface for browsing and selecting clipboard entries
|
||||||
|
stored by clipmenud. It launches a menu using a configured launcher
|
||||||
|
(e.g., dmenu, rofi, or another custom command) and displays a numbered list of
|
||||||
|
stored clips. Once a selection is made, clipmenu calls
|
||||||
|
.BR clipserve
|
||||||
|
to set the chosen clip as the current X11 selections (PRIMARY and CLIPBOARD).
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-h, \--help
|
||||||
|
Display the help message (invokes the manual page).
|
||||||
|
|
||||||
|
Other than that, command-line arguments are passed directly to the underlying
|
||||||
|
launcher.
|
||||||
|
.SH CONFIGURATION
|
||||||
|
See
|
||||||
|
.BR clipmenu.conf (5).
|
||||||
|
.SH DEPENDENCIES
|
||||||
|
clipmenu requires a launcher capable of a dmenu-like interface (e.g., rofi or a
|
||||||
|
custom command). Population of the clipstore requires
|
||||||
|
.BR clipmenud
|
||||||
|
to be running.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR clipctl (1),
|
||||||
|
.BR clipdel (1),
|
||||||
|
.BR clipmenud (1),
|
||||||
|
.BR clipmenu.conf (5)
|
||||||
|
.SH AUTHOR
|
||||||
|
Chris Down
|
||||||
|
.MT chris@chrisdown.name
|
||||||
|
.ME
|
||||||
|
.SH REPORTING BUGS
|
||||||
|
Please send bug reports to
|
||||||
|
.UR https://github.com/cdown/clipmenu/issues
|
||||||
|
.UE .
|
||||||
124
man/clipmenu.conf.5
Normal file
124
man/clipmenu.conf.5
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
.TH CLIPMENU.CONF 5
|
||||||
|
.SH NAME
|
||||||
|
clipmenu.conf \- configuration file for clipmenu applications
|
||||||
|
.SH DESCRIPTION
|
||||||
|
clipmenu.conf defines the runtime settings for clipmenu applications (including
|
||||||
|
clipmenu itself, clipmenud, clipctl, clipdel, clipdelmenu, and clipserve).
|
||||||
|
.SH FORMAT
|
||||||
|
Each non-comment line in clipmenu.conf consists of a key followed by one or
|
||||||
|
more whitespace-separated values. Blank lines and lines starting with '#' are
|
||||||
|
ignored.
|
||||||
|
.PP
|
||||||
|
For example:
|
||||||
|
.EX
|
||||||
|
max_clips 1000
|
||||||
|
selections clipboard primary
|
||||||
|
.EE
|
||||||
|
.SH CONFIGURATION OPTIONS
|
||||||
|
.TP
|
||||||
|
.B max_clips
|
||||||
|
Specifies the maximum number of clipboard entries to retain in the clip store.
|
||||||
|
Default: 1000.
|
||||||
|
.TP
|
||||||
|
.B max_clips_batch
|
||||||
|
Provides a buffer above max_clips; when the number of entries exceeds
|
||||||
|
(max_clips + max_clips_batch), the clip store is trimmed back to max_clips
|
||||||
|
entries. Default: 100.
|
||||||
|
.TP
|
||||||
|
.B oneshot
|
||||||
|
If set to 1, clipmenud processes clipboard selections only once before exiting.
|
||||||
|
Default: 0.
|
||||||
|
.TP
|
||||||
|
.B deduplicate
|
||||||
|
If set to 1, clipmenud keeps only the newest copy of duplicate clips instead of
|
||||||
|
storing every duplicate entry. Default: 0.
|
||||||
|
.TP
|
||||||
|
.B own_clipboard
|
||||||
|
Determines whether clipmenud should claim ownership of the X11 clipboard. Works
|
||||||
|
together with own_selections. Default: 0.
|
||||||
|
.TP
|
||||||
|
.B own_selections
|
||||||
|
Specifies which X11 selections (e.g., "clipboard" or "primary") clipmenud
|
||||||
|
should actively own when
|
||||||
|
.B own_clipboard
|
||||||
|
is enabled. When a clip from one of these selections is stored, clipmenud uses
|
||||||
|
clipserve to own those selections. This does not affect which selections are
|
||||||
|
monitored; use
|
||||||
|
.B selections
|
||||||
|
for that. Default: "clipboard".
|
||||||
|
If this is set to an empty value, it is treated as if
|
||||||
|
.B own_clipboard
|
||||||
|
was disabled.
|
||||||
|
.TP
|
||||||
|
.B selections
|
||||||
|
Lists the X11 selections to monitor for changes. Valid values include
|
||||||
|
"clipboard", "primary", and "secondary". Default: "clipboard primary".
|
||||||
|
.TP
|
||||||
|
.B ignore_window
|
||||||
|
Defines a regular expression matching window titles to exclude from clipboard
|
||||||
|
monitoring. Unset by default.
|
||||||
|
.TP
|
||||||
|
.B launcher
|
||||||
|
Specifies the launcher command to use with clipmenu. Alternative choices
|
||||||
|
include rofi's dmenu mode or a custom command. Default: "dmenu".
|
||||||
|
.TP
|
||||||
|
.B launcher_pass_dmenu_args
|
||||||
|
When enabled, clipmenu and clipdelmenu add dmenu-compatible defaults (such as
|
||||||
|
-p and -l) before any user-provided arguments. Default: 1.
|
||||||
|
.TP
|
||||||
|
.B touch_on_select
|
||||||
|
When an entry is selected via
|
||||||
|
.BR clipmenu (1)
|
||||||
|
it will also be moved up to the newest slot.
|
||||||
|
Default: 0.
|
||||||
|
.TP
|
||||||
|
.B partial_merge_secs
|
||||||
|
Specifies the time window in seconds during which consecutive clipboard updates
|
||||||
|
where the previous entry was a substring of the new one are merged together
|
||||||
|
instead of being stored separately. This is useful for applications that
|
||||||
|
incrementally update the clipboard (e.g. progressive text selection in
|
||||||
|
browsers). Set to 0 to disable partial merging entirely. Default: 2.
|
||||||
|
.TP
|
||||||
|
.B cm_dir
|
||||||
|
Overrides the default base directory for clipmenu runtime data. The clip store
|
||||||
|
is created at a subdirectory named clipmenu.<version>.<uid> inside this
|
||||||
|
directory. By default, it uses XDG_RUNTIME_DIR, TMPDIR, or /tmp if neither are
|
||||||
|
set. The path must be absolute. A leading ~ expands to $HOME.
|
||||||
|
.SH FILE LOCATION
|
||||||
|
Typically, clipmenu.conf is located at
|
||||||
|
.BR ~/.config/clipmenu/clipmenu.conf
|
||||||
|
.
|
||||||
|
The file location can be overridden with $CM_CONFIG, which must be an absolute
|
||||||
|
path. A leading ~ expands to $HOME. If $CM_CONFIG is unset, $XDG_CONFIG_HOME is
|
||||||
|
used (falling back to ~/.config).
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B CM_CONFIG
|
||||||
|
Absolute path (or a path beginning with ~) to the configuration file. This
|
||||||
|
takes precedence over $XDG_CONFIG_HOME.
|
||||||
|
.TP
|
||||||
|
.B XDG_CONFIG_HOME
|
||||||
|
Base directory for configuration files. If unset, ~/.config is used.
|
||||||
|
.TP
|
||||||
|
.B CM_DEBUG
|
||||||
|
If set to 1, enable debug logging.
|
||||||
|
.TP
|
||||||
|
.B Config environment variables
|
||||||
|
CM_MAX_CLIPS, CM_MAX_CLIPS_BATCH, CM_ONESHOT, CM_DEDUPLICATE, CM_OWN_CLIPBOARD,
|
||||||
|
CM_SELECTIONS, CM_OWN_SELECTIONS, CM_IGNORE_WINDOW, CM_LAUNCHER,
|
||||||
|
CM_LAUNCHER_PASS_DMENU_ARGS, and CM_DIR are environment variable equivalents for
|
||||||
|
the configuration keys of the same name. These override values from the config
|
||||||
|
file and are provided for compatibility with versions prior to 7.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR clipctl (1),
|
||||||
|
.BR clipdel (1),
|
||||||
|
.BR clipmenu (1),
|
||||||
|
.BR clipmenud (1),
|
||||||
|
.SH AUTHOR
|
||||||
|
Chris Down
|
||||||
|
.MT chris@chrisdown.name
|
||||||
|
.ME
|
||||||
|
.SH REPORTING BUGS
|
||||||
|
Please send bug reports to
|
||||||
|
.UR https://github.com/cdown/clipmenu/issues
|
||||||
|
.UE .
|
||||||
32
man/clipmenud.1
Normal file
32
man/clipmenud.1
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
.TH CLIPMENUD 1
|
||||||
|
.SH NAME
|
||||||
|
clipmenud \- daemon for clipboard monitoring and storage
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B clipmenud
|
||||||
|
runs in the background, monitoring X11 clipboard selections (including PRIMARY,
|
||||||
|
CLIPBOARD, and SECONDARY). It stores new clipboard entries into a persistent
|
||||||
|
clip store. clipmenud responds to signals sent by
|
||||||
|
.BR clipctl
|
||||||
|
to enable or disable clipboard collection.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-h, \--help
|
||||||
|
Display the help message (invokes the manual page).
|
||||||
|
.SH CONFIGURATION
|
||||||
|
See
|
||||||
|
.BR clipmenu.conf (5).
|
||||||
|
.SH DEPENDENCIES
|
||||||
|
clipmenud requires an X11 environment with the XFixes extension and access to the clip store directory as defined in the configuration.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR clipctl (1),
|
||||||
|
.BR clipdel (1),
|
||||||
|
.BR clipmenu (1),
|
||||||
|
.BR clipmenu.conf (5)
|
||||||
|
.SH AUTHOR
|
||||||
|
Chris Down
|
||||||
|
.MT chris@chrisdown.name
|
||||||
|
.ME
|
||||||
|
.SH REPORTING BUGS
|
||||||
|
Please send bug reports to
|
||||||
|
.UR https://github.com/cdown/clipmenu/issues
|
||||||
|
.UE .
|
||||||
44
man/clipserve.1
Normal file
44
man/clipserve.1
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
.TH CLIPSERVE 1
|
||||||
|
.SH NAME
|
||||||
|
clipserve \- serve a selected clipboard entry to X11 selections
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B clipserve
|
||||||
|
[\-s selection]... <hash>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B clipserve
|
||||||
|
serves the clipboard content identified by a hash from the clip store on the
|
||||||
|
configured X11 selections. By default, it serves both PRIMARY and CLIPBOARD.
|
||||||
|
|
||||||
|
This program is not usually invoked directly, but is instead called from inside
|
||||||
|
other clipmenu applications.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-h, \--help
|
||||||
|
Display the help message (invokes the manual page).
|
||||||
|
.TP
|
||||||
|
.B \-s, \--selection
|
||||||
|
Specify a selection to serve. Valid values are "primary", "clipboard", and
|
||||||
|
"secondary". This option may be repeated. If not provided, PRIMARY and
|
||||||
|
CLIPBOARD are used.
|
||||||
|
.TP
|
||||||
|
clipserve requires a hexadecimal hash of the clipboard entry to be served.
|
||||||
|
.SH CONFIGURATION
|
||||||
|
See
|
||||||
|
.BR clipmenu.conf (5).
|
||||||
|
.SH DEPENDENCIES
|
||||||
|
clipserve needs a valid clip store and an operational X11 environment to
|
||||||
|
function correctly.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR clipctl (1),
|
||||||
|
.BR clipdel (1),
|
||||||
|
.BR clipmenu (1),
|
||||||
|
.BR clipmenud (1),
|
||||||
|
.BR clipmenu.conf (5)
|
||||||
|
.SH AUTHOR
|
||||||
|
Chris Down
|
||||||
|
.MT chris@chrisdown.name
|
||||||
|
.ME
|
||||||
|
.SH REPORTING BUGS
|
||||||
|
Please send bug reports to
|
||||||
|
.UR https://github.com/cdown/clipmenu/issues
|
||||||
|
.UE .
|
||||||
152
src/clipctl.c
Normal file
152
src/clipctl.c
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the clipmenud service is enabled.
|
||||||
|
*/
|
||||||
|
static bool is_enabled(struct config *cfg) {
|
||||||
|
_drop_(fclose) FILE *file = fopen(get_enabled_path(cfg), "r");
|
||||||
|
die_on(!file, "Failed to open enabled file: %s\n", strerror(errno));
|
||||||
|
return fgetc(file) == '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the process ID of the clipmenud daemon.
|
||||||
|
*
|
||||||
|
* -EEXIST is returned if multiple instances are detected. -ENOENT is returned
|
||||||
|
* if no instances are found.
|
||||||
|
*/
|
||||||
|
static pid_t get_clipmenud_pid(struct config *cfg) {
|
||||||
|
int fd = open(get_session_lock_path(cfg), O_RDWR | O_CLOEXEC);
|
||||||
|
if (fd < 0) {
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
|
||||||
|
close(fd);
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
bool would_block;
|
||||||
|
#if EWOULDBLOCK == EAGAIN
|
||||||
|
would_block = errno == EWOULDBLOCK;
|
||||||
|
#else
|
||||||
|
would_block = errno == EWOULDBLOCK || errno == EAGAIN;
|
||||||
|
#endif
|
||||||
|
if (!would_block) {
|
||||||
|
close(fd);
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
char buf[32];
|
||||||
|
ssize_t read_sz = read(fd, buf, sizeof(buf) - 1);
|
||||||
|
close(fd);
|
||||||
|
if (read_sz <= 0) {
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
buf[read_sz] = '\0';
|
||||||
|
char *newline = strchr(buf, '\n');
|
||||||
|
if (newline) {
|
||||||
|
*newline = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t pid;
|
||||||
|
if (str_to_uint64(buf, &pid) < 0) {
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kill((pid_t)pid, 0) < 0 && errno == ESRCH) {
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (pid_t)pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if clipmenud should be enabled based on the given mode string and
|
||||||
|
* clipmenu state.
|
||||||
|
*/
|
||||||
|
static bool _nonnull_ should_enable(struct config *cfg, const char *mode_str) {
|
||||||
|
if (streq(mode_str, "enable")) {
|
||||||
|
return true;
|
||||||
|
} else if (streq(mode_str, "disable")) {
|
||||||
|
return false;
|
||||||
|
} else if (streq(mode_str, "toggle")) {
|
||||||
|
return !is_enabled(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
die("Unknown command: %s\n", mode_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
_drop_(config_free) struct config cfg = setup("clipctl");
|
||||||
|
exec_man_on_help(argc, argv);
|
||||||
|
die_on(argc != 2,
|
||||||
|
"Usage: clipctl <enable|disable|toggle|status|version|cache-dir>\n");
|
||||||
|
|
||||||
|
const char *cmd = argv[1];
|
||||||
|
|
||||||
|
if (streq(cmd, "cache-dir")) {
|
||||||
|
printf("%s\n", get_cache_dir(&cfg));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (streq(cmd, "version")) {
|
||||||
|
printf("%d\n", CLIPMENU_VERSION);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pid_t pid = get_clipmenud_pid(&cfg);
|
||||||
|
die_on(pid == -ENOENT, "clipmenud is not running\n");
|
||||||
|
expect(pid > 0);
|
||||||
|
|
||||||
|
if (streq(cmd, "status")) {
|
||||||
|
printf("%s\n", is_enabled(&cfg) ? "enabled" : "disabled");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool want_enable = should_enable(&cfg, cmd);
|
||||||
|
|
||||||
|
expect(kill(pid, want_enable ? SIGUSR2 : SIGUSR1) == 0);
|
||||||
|
dbg("Sent signal to pid %d\n", pid);
|
||||||
|
|
||||||
|
unsigned int delay_ms = 1;
|
||||||
|
unsigned int total_wait_ms = 0;
|
||||||
|
const unsigned int max_wait_ms = 1000;
|
||||||
|
|
||||||
|
while (total_wait_ms < max_wait_ms) {
|
||||||
|
if (is_enabled(&cfg) == want_enable) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct timespec req = {.tv_sec = delay_ms / 1000,
|
||||||
|
.tv_nsec = (delay_ms % 1000) * 1000000L};
|
||||||
|
struct timespec rem;
|
||||||
|
|
||||||
|
while (nanosleep(&req, &rem) != 0) {
|
||||||
|
expect(errno == EINTR);
|
||||||
|
req = rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_wait_ms += delay_ms;
|
||||||
|
delay_ms *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_enabled(&cfg) == want_enable) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
die("Failed to %s clipmenud within %u ms\n",
|
||||||
|
want_enable ? "enable" : "disable", max_wait_ms);
|
||||||
|
}
|
||||||
162
src/clipdel.c
Normal file
162
src/clipdel.c
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <regex.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "store.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The deletion mode for clipdel operations.
|
||||||
|
*/
|
||||||
|
enum delete_mode {
|
||||||
|
DELETE_DRY_RUN,
|
||||||
|
DELETE_REAL,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match type for clipdel operations.
|
||||||
|
*/
|
||||||
|
enum match_type {
|
||||||
|
MATCH_REGEX,
|
||||||
|
MATCH_LITERAL,
|
||||||
|
MATCH_COUNT_NEWEST,
|
||||||
|
MATCH_COUNT_OLDEST,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the application state for a clipdel operation in preparation for
|
||||||
|
* passing it as private data to the cs_remove callback.
|
||||||
|
*/
|
||||||
|
struct clipdel_state {
|
||||||
|
enum delete_mode mode;
|
||||||
|
enum match_type type;
|
||||||
|
bool invert_match;
|
||||||
|
union {
|
||||||
|
regex_t rgx;
|
||||||
|
const char *needle;
|
||||||
|
struct {
|
||||||
|
uint64_t current;
|
||||||
|
uint64_t num_delete;
|
||||||
|
} count;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for cs_remove. In order for the delete to actually happen, we must
|
||||||
|
* be running DELETE_REAL.
|
||||||
|
*/
|
||||||
|
static enum cs_remove_action _nonnull_ remove_if_match(uint64_t hash _unused_,
|
||||||
|
const char *line,
|
||||||
|
void *private) {
|
||||||
|
struct clipdel_state *state = private;
|
||||||
|
int ret;
|
||||||
|
bool matches;
|
||||||
|
switch (state->type) {
|
||||||
|
case MATCH_LITERAL:
|
||||||
|
matches = strstr(line, state->needle) != NULL;
|
||||||
|
break;
|
||||||
|
case MATCH_REGEX:
|
||||||
|
ret = regexec(&state->rgx, line, 0, NULL, 0);
|
||||||
|
expect(ret == 0 || ret == REG_NOMATCH);
|
||||||
|
matches = ret == 0;
|
||||||
|
break;
|
||||||
|
case MATCH_COUNT_NEWEST:
|
||||||
|
case MATCH_COUNT_OLDEST:
|
||||||
|
if (state->count.current < state->count.num_delete) {
|
||||||
|
matches = true;
|
||||||
|
++state->count.current;
|
||||||
|
} else {
|
||||||
|
matches = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
die("unreachable\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wants_del = state->invert_match ? !matches : matches;
|
||||||
|
if (wants_del) {
|
||||||
|
puts(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state->mode == DELETE_REAL && wants_del ? CS_ACTION_REMOVE
|
||||||
|
: CS_ACTION_KEEP;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
const char usage[] = "Usage: clipdel [-d] [-F] [-N] [-n] [-v] pattern";
|
||||||
|
|
||||||
|
_drop_(config_free) struct config cfg = setup("clipdel");
|
||||||
|
|
||||||
|
struct clipdel_state state = {0};
|
||||||
|
|
||||||
|
int opt;
|
||||||
|
while ((opt = getopt(argc, argv, "dFNnvh")) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'd':
|
||||||
|
state.mode = DELETE_REAL;
|
||||||
|
break;
|
||||||
|
case 'F':
|
||||||
|
state.type = MATCH_LITERAL;
|
||||||
|
break;
|
||||||
|
case 'N':
|
||||||
|
state.type = MATCH_COUNT_OLDEST;
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
state.type = MATCH_COUNT_NEWEST;
|
||||||
|
break;
|
||||||
|
case 'v':
|
||||||
|
state.invert_match = true;
|
||||||
|
break;
|
||||||
|
case 'h':
|
||||||
|
exec_man();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
die("%s\n", usage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
die_on(optind >= argc, "%s\n", usage);
|
||||||
|
|
||||||
|
_drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY);
|
||||||
|
_drop_(close) int snip_fd =
|
||||||
|
open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600);
|
||||||
|
expect(content_dir_fd >= 0 && snip_fd >= 0);
|
||||||
|
|
||||||
|
_drop_(cs_destroy) struct clip_store cs;
|
||||||
|
expect(cs_init(&cs, snip_fd, content_dir_fd) == 0);
|
||||||
|
|
||||||
|
enum cs_iter_direction direction = CS_ITER_OLDEST_FIRST;
|
||||||
|
switch (state.type) {
|
||||||
|
case MATCH_REGEX:
|
||||||
|
die_on(regcomp(&state.rgx, argv[optind], REG_EXTENDED | REG_NOSUB),
|
||||||
|
"Could not compile regex\n");
|
||||||
|
break;
|
||||||
|
case MATCH_LITERAL:
|
||||||
|
state.needle = argv[optind];
|
||||||
|
break;
|
||||||
|
case MATCH_COUNT_NEWEST:
|
||||||
|
case MATCH_COUNT_OLDEST:
|
||||||
|
die_on(str_to_uint64(argv[optind], &state.count.num_delete) < 0,
|
||||||
|
"Bad argument, expected integer: %s\n", argv[optind]);
|
||||||
|
state.count.current = 0;
|
||||||
|
if (state.type == MATCH_COUNT_NEWEST) {
|
||||||
|
direction = CS_ITER_NEWEST_FIRST;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
die("unreachable\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(cs_remove(&cs, direction, remove_if_match, &state) == 0);
|
||||||
|
|
||||||
|
if (state.type == MATCH_REGEX) {
|
||||||
|
regfree(&state.rgx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
45
src/clipdelmenu.c
Normal file
45
src/clipdelmenu.c
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "menu_util.h"
|
||||||
|
#include "store.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
struct delete_state {
|
||||||
|
uint64_t hash_to_delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
// cppcheck-suppress-begin constParameterCallback
|
||||||
|
static enum cs_remove_action _nonnull_
|
||||||
|
remove_if_hash_match(uint64_t hash, const char *line _unused_, void *private) {
|
||||||
|
const struct delete_state *state = private;
|
||||||
|
return hash == state->hash_to_delete ? CS_ACTION_REMOVE : CS_ACTION_KEEP;
|
||||||
|
}
|
||||||
|
// cppcheck-suppress-end constParameterCallback
|
||||||
|
|
||||||
|
static int _nonnull_ clipdelmenu_action(struct config *cfg, uint64_t hash) {
|
||||||
|
_drop_(close) int content_dir_fd = open(get_cache_dir(cfg), O_RDONLY);
|
||||||
|
_drop_(close) int snip_fd =
|
||||||
|
open(get_line_cache_path(cfg), O_RDWR | O_CREAT, 0600);
|
||||||
|
expect(content_dir_fd >= 0 && snip_fd >= 0);
|
||||||
|
|
||||||
|
_drop_(cs_destroy) struct clip_store cs;
|
||||||
|
expect(cs_init(&cs, snip_fd, content_dir_fd) == 0);
|
||||||
|
|
||||||
|
struct delete_state del_state = {.hash_to_delete = hash};
|
||||||
|
expect(cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_hash_match,
|
||||||
|
&del_state) == 0);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
menu_set_user_args(argc, argv);
|
||||||
|
|
||||||
|
_drop_(config_free) struct config cfg = setup("clipdelmenu");
|
||||||
|
exec_man_on_help(argc, argv);
|
||||||
|
|
||||||
|
return menu_prompt_and_act(&cfg, "clipdelmenu", clipdelmenu_action);
|
||||||
|
}
|
||||||
42
src/clipmenu.c
Normal file
42
src/clipmenu.c
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "menu_util.h"
|
||||||
|
#include "store.h"
|
||||||
|
#include "util.h"
|
||||||
|
#include "x.h"
|
||||||
|
|
||||||
|
static int _nonnull_ clipmenu_action(struct config *cfg, uint64_t hash) {
|
||||||
|
if (cfg->touch_on_select) {
|
||||||
|
_drop_(close) int content_dir_fd = open(get_cache_dir(cfg), O_RDONLY);
|
||||||
|
_drop_(close) int snip_fd =
|
||||||
|
open(get_line_cache_path(cfg), O_RDWR | O_CREAT, 0600);
|
||||||
|
expect(content_dir_fd >= 0 && snip_fd >= 0);
|
||||||
|
|
||||||
|
_drop_(cs_destroy) struct clip_store cs;
|
||||||
|
expect(cs_init(&cs, snip_fd, content_dir_fd) == 0);
|
||||||
|
int ret = cs_make_newest(&cs, hash);
|
||||||
|
if (ret == -ENOENT) {
|
||||||
|
fprintf(stderr, "Selected clip no longer exists\n");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
expect(ret == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
run_clipserve(hash, NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
menu_set_user_args(argc, argv);
|
||||||
|
|
||||||
|
_drop_(config_free) struct config cfg = setup("clipmenu");
|
||||||
|
exec_man_on_help(argc, argv);
|
||||||
|
|
||||||
|
return menu_prompt_and_act(&cfg, "clipmenu", clipmenu_action);
|
||||||
|
}
|
||||||
765
src/clipmenud.c
Normal file
765
src/clipmenud.c
Normal file
|
|
@ -0,0 +1,765 @@
|
||||||
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/extensions/Xfixes.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <regex.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "store.h"
|
||||||
|
#include "util.h"
|
||||||
|
#include "x.h"
|
||||||
|
|
||||||
|
static Display *dpy;
|
||||||
|
static struct clip_store cs;
|
||||||
|
static struct config cfg;
|
||||||
|
static Window win;
|
||||||
|
|
||||||
|
static int enabled = 1;
|
||||||
|
static int sig_pipe[2];
|
||||||
|
|
||||||
|
static Atom timestamp_atom;
|
||||||
|
static Atom incr_atom;
|
||||||
|
static struct incr_transfer *it_list;
|
||||||
|
|
||||||
|
static struct cm_selections sels[CM_SEL_MAX];
|
||||||
|
|
||||||
|
static Time last_disable_time = 0;
|
||||||
|
static Time last_enable_time = 0;
|
||||||
|
|
||||||
|
static bool has_owned_selections(void) {
|
||||||
|
for (size_t i = 0; i < CM_SEL_MAX; i++) {
|
||||||
|
if (cfg.owned_selections[i].active) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum clip_text_source {
|
||||||
|
CLIP_TEXT_SOURCE_X,
|
||||||
|
CLIP_TEXT_SOURCE_MALLOC,
|
||||||
|
CLIP_TEXT_SOURCE_INVALID
|
||||||
|
};
|
||||||
|
|
||||||
|
struct clip_text {
|
||||||
|
char *data;
|
||||||
|
enum clip_text_source source;
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct clip_text last_text[CM_SEL_MAX] = {
|
||||||
|
{NULL, CLIP_TEXT_SOURCE_MALLOC},
|
||||||
|
{NULL, CLIP_TEXT_SOURCE_MALLOC},
|
||||||
|
{NULL, CLIP_TEXT_SOURCE_MALLOC},
|
||||||
|
};
|
||||||
|
static struct timespec last_text_time[CM_SEL_MAX];
|
||||||
|
|
||||||
|
static void free_clip_text(struct clip_text *ct) {
|
||||||
|
expect(ct->source != CLIP_TEXT_SOURCE_INVALID);
|
||||||
|
|
||||||
|
if (ct->data) {
|
||||||
|
if (ct->source == CLIP_TEXT_SOURCE_X) {
|
||||||
|
XFree(ct->data);
|
||||||
|
} else {
|
||||||
|
free(ct->data);
|
||||||
|
}
|
||||||
|
ct->data = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ct->source = CLIP_TEXT_SOURCE_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct timespec get_monotonic_time(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
expect(clock_gettime(CLOCK_MONOTONIC, &ts) == 0);
|
||||||
|
return ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool within_partial_merge_window(struct timespec current_time,
|
||||||
|
struct timespec last_time) {
|
||||||
|
time_t elapsed_secs = current_time.tv_sec - last_time.tv_sec;
|
||||||
|
|
||||||
|
if (elapsed_secs < cfg.partial_merge_secs) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (elapsed_secs > cfg.partial_merge_secs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return current_time.tv_nsec <= last_time.tv_nsec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cppcheck-suppress [constParameterCallback,unmatchedSuppression]
|
||||||
|
static Bool is_timestamp_event(Display *display _unused_, XEvent *event,
|
||||||
|
XPointer arg) {
|
||||||
|
const void *argp = arg;
|
||||||
|
return event->type == PropertyNotify &&
|
||||||
|
event->xproperty.atom == *(const Atom *)argp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current X server time by triggering a PropertyNotify.
|
||||||
|
*/
|
||||||
|
static Time get_current_server_time(void) {
|
||||||
|
XEvent ev;
|
||||||
|
XChangeProperty(dpy, win, timestamp_atom, XA_INTEGER, 32, PropModeReplace,
|
||||||
|
NULL, 0);
|
||||||
|
XIfEvent(dpy, &ev, is_timestamp_event, (XPointer)×tamp_atom);
|
||||||
|
return ev.xproperty.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a text s1 is a possible partial of s2.
|
||||||
|
*
|
||||||
|
* Chromium and some other badly behaved applications spam PRIMARY during
|
||||||
|
* selection, so if you're selecting the text "abc", you get three clips: "a",
|
||||||
|
* "ab", and "abc" (or "c", "bc", "abc" if selecting right to left). Attempt to
|
||||||
|
* detect these. It's possible we were not fast enough to get all of them, so
|
||||||
|
* unfortunately we can't check for strlen(s1)+1 either. It's also possible the
|
||||||
|
* user first expands, and then retracts the selection, so we need to handle
|
||||||
|
* that too.
|
||||||
|
*/
|
||||||
|
static bool is_possible_partial(const char *s1, const char *s2) {
|
||||||
|
size_t len1 = strlen(s1), len2 = strlen(s2);
|
||||||
|
|
||||||
|
// Is one a prefix of the other?
|
||||||
|
if (strncmp(s1, s2, len1 < len2 ? len1 : len2) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is one a suffix of the other?
|
||||||
|
if (len1 < len2) {
|
||||||
|
return strcmp(s1, s2 + len2 - len1) == 0;
|
||||||
|
} else {
|
||||||
|
return strcmp(s2, s1 + len1 - len2) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the converted text put into our clip atom. In order for this to
|
||||||
|
* happen a conversion must have been performed in an earlier iteration with
|
||||||
|
* XConvertSelection.
|
||||||
|
*/
|
||||||
|
static struct clip_text get_clipboard_text(Atom clip_atom) {
|
||||||
|
struct clip_text ct = {NULL, CLIP_TEXT_SOURCE_X};
|
||||||
|
unsigned char *cur_text;
|
||||||
|
Atom actual_type;
|
||||||
|
int actual_format;
|
||||||
|
unsigned long nitems, bytes_after;
|
||||||
|
|
||||||
|
int res = XGetWindowProperty(dpy, win, clip_atom, 0L, (~0L), False,
|
||||||
|
AnyPropertyType, &actual_type, &actual_format,
|
||||||
|
&nitems, &bytes_after, &cur_text);
|
||||||
|
if (res != Success) {
|
||||||
|
return ct;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actual_type == incr_atom) {
|
||||||
|
dbg("Unexpected INCR transfer detected\n");
|
||||||
|
XFree(cur_text);
|
||||||
|
return ct;
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.data = (char *)cur_text;
|
||||||
|
|
||||||
|
return ct;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the given string contains any non-whitespace characters.
|
||||||
|
*/
|
||||||
|
static bool is_salient_text(const char *str) {
|
||||||
|
if (!str) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; *str; str++) {
|
||||||
|
if (!isspace((unsigned char)*str)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the current enabled status to a designated status file.
|
||||||
|
*/
|
||||||
|
static void write_status(void) {
|
||||||
|
_drop_(close) int fd =
|
||||||
|
open(get_enabled_path(&cfg), O_WRONLY | O_CREAT, 0600);
|
||||||
|
die_on(fd < 0, "Failed to update status: %s\n", strerror(errno));
|
||||||
|
dprintf(fd, "%d", (int)enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the given window title matches the title of the clipserve
|
||||||
|
* window.
|
||||||
|
*/
|
||||||
|
static bool is_clipserve(const char *win_title) {
|
||||||
|
return win_title && streq(win_title, "clipserve");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a window with the given title should be ignored based on user
|
||||||
|
* configuration.
|
||||||
|
*/
|
||||||
|
static bool is_ignored_window(char *win_title) {
|
||||||
|
if (!win_title || !cfg.ignore_window.set) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int ret = regexec(&cfg.ignore_window.rgx, win_title, 0, NULL, 0);
|
||||||
|
expect(ret == 0 || ret == REG_NOMATCH);
|
||||||
|
return !ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable or enable clip collection based on received signals.
|
||||||
|
*/
|
||||||
|
static void handle_signal_event(int signo) {
|
||||||
|
dbg("Got signal %d\n", signo);
|
||||||
|
switch (signo) {
|
||||||
|
case SIGUSR1:
|
||||||
|
// If we're already disabled, we need to keep the original
|
||||||
|
// timestamp to properly filter all events that were queued during
|
||||||
|
// any part of the disabled period.
|
||||||
|
if (enabled) {
|
||||||
|
last_disable_time = get_current_server_time();
|
||||||
|
}
|
||||||
|
enabled = 0;
|
||||||
|
dbg("Clipboard collection disabled by signal at time %lu\n",
|
||||||
|
(unsigned long)last_disable_time);
|
||||||
|
break;
|
||||||
|
case SIGUSR2:
|
||||||
|
// If we're already enabled, we need to keep the original timestamp
|
||||||
|
// so we don't mistakenly filter out valid messages.
|
||||||
|
if (!enabled) {
|
||||||
|
last_enable_time = get_current_server_time();
|
||||||
|
}
|
||||||
|
enabled = 1;
|
||||||
|
dbg("Clipboard collection enabled by signal at time %lu\n",
|
||||||
|
(unsigned long)last_enable_time);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
write_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_signal_pipe(void) {
|
||||||
|
unsigned char sigs[32];
|
||||||
|
ssize_t read_sz;
|
||||||
|
while ((read_sz = read(sig_pipe[0], sigs, sizeof(sigs))) > 0) {
|
||||||
|
for (ssize_t i = 0; i < read_sz; i++) {
|
||||||
|
handle_signal_event((int)sigs[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (read_sz < 0) {
|
||||||
|
expect(errno == EAGAIN || errno == EINTR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void signal_handler(int signo) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
unsigned char sig = (unsigned char)signo;
|
||||||
|
ssize_t written = write(sig_pipe[1], &sig, sizeof(sig));
|
||||||
|
(void)written;
|
||||||
|
errno = saved_errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_fd_flags(int fd) {
|
||||||
|
int flags = fcntl(fd, F_GETFL);
|
||||||
|
expect(flags >= 0);
|
||||||
|
expect(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0);
|
||||||
|
|
||||||
|
int fd_flags = fcntl(fd, F_GETFD);
|
||||||
|
expect(fd_flags >= 0);
|
||||||
|
expect(fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Something changed about the watched selection, consider converting it to our
|
||||||
|
* desired property type.
|
||||||
|
*/
|
||||||
|
static void handle_xfixes_selection_notify(XFixesSelectionNotifyEvent *se) {
|
||||||
|
if (last_disable_time > 0 && se->timestamp >= last_disable_time &&
|
||||||
|
se->timestamp < last_enable_time) {
|
||||||
|
dbg("Ignoring selection event from disabled period (event time: %lu, disabled: %lu, enabled: %lu)\n",
|
||||||
|
(unsigned long)se->timestamp, (unsigned long)last_disable_time,
|
||||||
|
(unsigned long)last_enable_time);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum selection_type sel =
|
||||||
|
selection_atom_to_selection_type(se->selection, sels);
|
||||||
|
if (sel == CM_SEL_INVALID) {
|
||||||
|
dbg("Received XFixesSelectionNotify for unknown sel\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_drop_(XFree) char *win_title = get_window_title(dpy, se->owner);
|
||||||
|
if (is_clipserve(win_title) || is_ignored_window(win_title)) {
|
||||||
|
dbg("Ignoring clip from window titled '%s'\n", win_title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbg("Notified about selection update. Selection: %s, Owner: '%s' (0x%lx)\n",
|
||||||
|
cfg.selections[sel].name, strnull(win_title), (unsigned long)se->owner);
|
||||||
|
|
||||||
|
struct incr_transfer *it = it_list;
|
||||||
|
while (it) {
|
||||||
|
struct incr_transfer *next = it->next;
|
||||||
|
if (it->property == sels[sel].storage) {
|
||||||
|
it_dbg(it, "Cleaning up stale INCR transfer\n");
|
||||||
|
free(it->data);
|
||||||
|
it_remove(&it_list, it);
|
||||||
|
free(it);
|
||||||
|
}
|
||||||
|
it = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
XConvertSelection(dpy, se->selection,
|
||||||
|
XInternAtom(dpy, "UTF8_STRING", False), sels[sel].storage,
|
||||||
|
win, CurrentTime);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Something changed about the watched selection, but we don't explicitly
|
||||||
|
* listen for SelectionNotify, so in reality this only happens in response to
|
||||||
|
* an explicit request to tell us that there is no owner. In that case, return
|
||||||
|
* -ENOENT.
|
||||||
|
*/
|
||||||
|
static int handle_selection_notify(const XSelectionEvent *se) {
|
||||||
|
if (se->property == None) {
|
||||||
|
enum selection_type sel =
|
||||||
|
selection_atom_to_selection_type(se->selection, sels);
|
||||||
|
if (sel == CM_SEL_INVALID) {
|
||||||
|
dbg("Received no owner notification for unknown sel\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
dbg("X reports that %s has no current owner\n",
|
||||||
|
cfg.selections[sel].name);
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trims the clip store if the number of clips exceeds the configured batch
|
||||||
|
* size.
|
||||||
|
*/
|
||||||
|
static void maybe_trim(void) {
|
||||||
|
size_t cur_clips;
|
||||||
|
expect(cs_len(&cs, &cur_clips) == 0);
|
||||||
|
if (cur_clips > (size_t)cfg.max_clips + (size_t)cfg.max_clips_batch) {
|
||||||
|
expect(cs_trim(&cs, CS_ITER_NEWEST_FIRST, (size_t)cfg.max_clips) == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the clipboard text for a selection. If the text is a possible partial
|
||||||
|
* of the last clip on the same selection and it was received shortly
|
||||||
|
* afterwards, replace instead of adding.
|
||||||
|
*/
|
||||||
|
static uint64_t store_clip(enum selection_type sel, struct clip_text *ct) {
|
||||||
|
dbg("Clipboard text is considered salient, storing\n");
|
||||||
|
struct timespec current_time = get_monotonic_time();
|
||||||
|
uint64_t hash;
|
||||||
|
|
||||||
|
if (cfg.partial_merge_secs > 0 && last_text[sel].data &&
|
||||||
|
within_partial_merge_window(current_time, last_text_time[sel]) &&
|
||||||
|
is_possible_partial(last_text[sel].data, ct->data)) {
|
||||||
|
dbg("Possible partial of last clip on %s, replacing\n",
|
||||||
|
cfg.selections[sel].name);
|
||||||
|
expect(cs_replace(&cs, CS_ITER_NEWEST_FIRST, 0, ct->data, &hash) == 0);
|
||||||
|
} else {
|
||||||
|
expect(cs_add(&cs, ct->data, &hash,
|
||||||
|
cfg.deduplicate ? CS_DUPE_KEEP_LAST : CS_DUPE_KEEP_ALL) ==
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
free_clip_text(&last_text[sel]);
|
||||||
|
last_text[sel] = *ct;
|
||||||
|
last_text_time[sel] = current_time;
|
||||||
|
|
||||||
|
// The caller no longer owns this data.
|
||||||
|
ct->data = NULL;
|
||||||
|
ct->source = CLIP_TEXT_SOURCE_INVALID;
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the final data collected during an INCR transfer.
|
||||||
|
*/
|
||||||
|
static void incr_receive_finish(struct incr_transfer *it) {
|
||||||
|
enum selection_type sel =
|
||||||
|
storage_atom_to_selection_type(it->property, sels);
|
||||||
|
if (sel == CM_SEL_INVALID) {
|
||||||
|
it_dbg(it, "Received INCR finish for unknown sel\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
it_dbg(it, "Finished (bytes buffered: %zu)\n", it->data_size);
|
||||||
|
char *text = malloc(it->data_size + 1);
|
||||||
|
expect(text);
|
||||||
|
memcpy(text, it->data, it->data_size);
|
||||||
|
text[it->data_size] = '\0';
|
||||||
|
|
||||||
|
struct clip_text ct = {text, CLIP_TEXT_SOURCE_MALLOC};
|
||||||
|
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
first_line(ct.data, line);
|
||||||
|
it_dbg(it, "First line: %s\n", line);
|
||||||
|
|
||||||
|
if (is_salient_text(ct.data)) {
|
||||||
|
uint64_t hash = store_clip(sel, &ct);
|
||||||
|
maybe_trim();
|
||||||
|
if (cfg.own_clipboard && has_owned_selections()) {
|
||||||
|
run_clipserve(hash, cfg.owned_selections);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
it_dbg(it, "Clipboard text is whitespace only, ignoring\n");
|
||||||
|
free_clip_text(&ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(it->data);
|
||||||
|
it_remove(&it_list, it);
|
||||||
|
free(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define INCR_DATA_START_BYTES 1024 * 1024
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acknowledge and start an INCR transfer.
|
||||||
|
*/
|
||||||
|
static void incr_receive_start(const XPropertyEvent *pe) {
|
||||||
|
struct incr_transfer *it = malloc(sizeof(struct incr_transfer));
|
||||||
|
expect(it);
|
||||||
|
*it = (struct incr_transfer){
|
||||||
|
.property = pe->atom,
|
||||||
|
.requestor = pe->window,
|
||||||
|
.data_size = 0,
|
||||||
|
.data_capacity = INCR_DATA_START_BYTES,
|
||||||
|
.data = malloc(INCR_DATA_START_BYTES),
|
||||||
|
};
|
||||||
|
expect(it->data);
|
||||||
|
|
||||||
|
it_dbg(it, "Starting transfer\n");
|
||||||
|
it_add(&it_list, it);
|
||||||
|
|
||||||
|
// Signal readiness for chunks
|
||||||
|
XDeleteProperty(dpy, win, pe->atom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continue receiving data during an INCR transfer.
|
||||||
|
*/
|
||||||
|
static void incr_receive_data(const XPropertyEvent *pe,
|
||||||
|
struct incr_transfer *it) {
|
||||||
|
if (pe->state != PropertyNewValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
it_dbg(it, "Receiving chunk (bytes buffered: %zu)\n", it->data_size);
|
||||||
|
|
||||||
|
_drop_(XFree) unsigned char *chunk = NULL;
|
||||||
|
Atom actual_type;
|
||||||
|
int actual_format;
|
||||||
|
unsigned long nitems, bytes_after;
|
||||||
|
XGetWindowProperty(dpy, win, pe->atom, 0, LONG_MAX, False, AnyPropertyType,
|
||||||
|
&actual_type, &actual_format, &nitems, &bytes_after,
|
||||||
|
&chunk);
|
||||||
|
|
||||||
|
size_t chunk_size = nitems * (actual_format / 8);
|
||||||
|
|
||||||
|
if (chunk_size == 0) {
|
||||||
|
it_dbg(it, "Transfer complete\n");
|
||||||
|
incr_receive_finish(it);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it->data_size + chunk_size > it->data_capacity) {
|
||||||
|
it->data_capacity = (it->data_size + chunk_size) * 2;
|
||||||
|
it->data = realloc(it->data, it->data_capacity);
|
||||||
|
expect(it->data);
|
||||||
|
it_dbg(it, "Expanded data buffer to %zu bytes\n", it->data_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(it->data + it->data_size, chunk, chunk_size);
|
||||||
|
it->data_size += chunk_size;
|
||||||
|
|
||||||
|
// Signal readiness for next chunk
|
||||||
|
XDeleteProperty(dpy, win, pe->atom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Something changed in our clip storage atoms. Work out whether we want to
|
||||||
|
* store the new content as a clipboard entry.
|
||||||
|
*/
|
||||||
|
static int handle_property_notify(const XPropertyEvent *pe) {
|
||||||
|
if (pe->state != PropertyNewValue && pe->state != PropertyDelete) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum selection_type sel = storage_atom_to_selection_type(pe->atom, sels);
|
||||||
|
if (sel == CM_SEL_INVALID) {
|
||||||
|
dbg("Received PropertyNotify for unknown sel\n");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this property corresponds to an INCR transfer in progress
|
||||||
|
struct incr_transfer *it = it_list;
|
||||||
|
while (it) {
|
||||||
|
if (it->property == pe->atom && it->requestor == pe->window) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
it = it->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it) {
|
||||||
|
incr_receive_data(pe, it);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not an INCR transfer in progress. Check if this is an INCR transfer
|
||||||
|
// starting
|
||||||
|
Atom actual_type;
|
||||||
|
int actual_format;
|
||||||
|
unsigned long nitems, bytes_after;
|
||||||
|
_drop_(XFree) unsigned char *prop = NULL;
|
||||||
|
|
||||||
|
XGetWindowProperty(dpy, win, pe->atom, 0, 0, False, AnyPropertyType,
|
||||||
|
&actual_type, &actual_format, &nitems, &bytes_after,
|
||||||
|
&prop);
|
||||||
|
|
||||||
|
if (actual_type == incr_atom) {
|
||||||
|
incr_receive_start(pe);
|
||||||
|
} else {
|
||||||
|
dbg("Received non-INCR PropertyNotify\n");
|
||||||
|
|
||||||
|
// store_clip will take care of freeing this later when it's gone from
|
||||||
|
// last_text.
|
||||||
|
struct clip_text ct = get_clipboard_text(pe->atom);
|
||||||
|
if (!ct.data) {
|
||||||
|
dbg("Failed to get clipboard text\n");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
first_line(ct.data, line);
|
||||||
|
dbg("First line: %s\n", line);
|
||||||
|
|
||||||
|
if (is_salient_text(ct.data)) {
|
||||||
|
uint64_t hash = store_clip(sel, &ct);
|
||||||
|
maybe_trim();
|
||||||
|
/* We only own CLIPBOARD because otherwise the behaviour is wonky:
|
||||||
|
*
|
||||||
|
* 1. When you select in a browser and press ^V, it repastes what
|
||||||
|
* you have selected instead of the previous content
|
||||||
|
* 2. urxvt and some other terminal emulators will unhilight on
|
||||||
|
* PRIMARY ownership being taken away from them
|
||||||
|
*/
|
||||||
|
if (cfg.own_clipboard && has_owned_selections()) {
|
||||||
|
run_clipserve(hash, cfg.owned_selections);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dbg("Clipboard text is whitespace only, ignoring\n");
|
||||||
|
free_clip_text(&ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process X11 events, returning when we have either processed one clip, or
|
||||||
|
* have received an indication that the selection is not owned.
|
||||||
|
*
|
||||||
|
* The usual sequence is:
|
||||||
|
*
|
||||||
|
* 1. Get an XFixesSelectionNotify that we have a new selection.
|
||||||
|
* 2. Call XConvertSelection() on it to get a string in our prop.
|
||||||
|
* 3. Wait for a PropertyNotify that says that's ready.
|
||||||
|
* 4. When it's ready, store it, and return from the function.
|
||||||
|
*
|
||||||
|
* Another possible outcome, especially when trying to get the initial state at
|
||||||
|
* startup, is that we get a SelectionNotify even with owner == None, which
|
||||||
|
* means the selection is unowned. At that point we also return, since it's
|
||||||
|
* clear that an explicit request has been nacked.
|
||||||
|
*/
|
||||||
|
static int handle_x11_event(int evt_base) {
|
||||||
|
while (XPending(dpy)) {
|
||||||
|
XEvent evt;
|
||||||
|
XNextEvent(dpy, &evt);
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
dbg("Got X event, but ignoring as collection is disabled\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret;
|
||||||
|
if (evt.type == evt_base + XFixesSelectionNotify) {
|
||||||
|
handle_xfixes_selection_notify((XFixesSelectionNotifyEvent *)&evt);
|
||||||
|
} else if (evt.type == PropertyNotify) {
|
||||||
|
ret = handle_property_notify((XPropertyEvent *)&evt);
|
||||||
|
if (ret == 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
} else if (evt.type == SelectionNotify) {
|
||||||
|
ret = handle_selection_notify((XSelectionEvent *)&evt);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -EINPROGRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continuously wait for and process X11 or signal events until we fully
|
||||||
|
* process success or failure for a clip.
|
||||||
|
*
|
||||||
|
* The usual sequence is:
|
||||||
|
*
|
||||||
|
* 1. Get an XFixesSelectionNotify that we have a new selection.
|
||||||
|
* 2. Call XConvertSelection() on it to get a string in our prop.
|
||||||
|
* 3. Wait for a PropertyNotify that says that's ready.
|
||||||
|
* 4. When it's ready, store it, and return from the function.
|
||||||
|
*
|
||||||
|
* Another possible outcome, especially when trying to get the initial state at
|
||||||
|
* startup, is that we get a SelectionNotify even with owner == None, which
|
||||||
|
* means the selection is unowned. At that point we also return, since it's
|
||||||
|
* clear that an explicit request has been nacked.
|
||||||
|
*/
|
||||||
|
static int get_one_clip(int evt_base) {
|
||||||
|
while (1) {
|
||||||
|
// It's possible that we have more X events to process, but because of
|
||||||
|
// the way the protocol works, we won't get told about them until we
|
||||||
|
// next get an event if we wait for select(). Check for them first.
|
||||||
|
if (XPending(dpy)) {
|
||||||
|
return handle_x11_event(evt_base);
|
||||||
|
}
|
||||||
|
|
||||||
|
fd_set fds;
|
||||||
|
int x_fd = ConnectionNumber(dpy);
|
||||||
|
|
||||||
|
FD_ZERO(&fds);
|
||||||
|
FD_SET(sig_pipe[0], &fds);
|
||||||
|
FD_SET(x_fd, &fds);
|
||||||
|
|
||||||
|
int max_fd = sig_pipe[0] > x_fd ? sig_pipe[0] : x_fd;
|
||||||
|
int ret = select(max_fd + 1, &fds, NULL, NULL, NULL);
|
||||||
|
if (ret < 0) {
|
||||||
|
if (errno == EINTR) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
expect(ret > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FD_ISSET(sig_pipe[0], &fds)) {
|
||||||
|
handle_signal_pipe();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FD_ISSET(x_fd, &fds)) {
|
||||||
|
return handle_x11_event(evt_base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int setup_watches(int evt_base) {
|
||||||
|
XSelectInput(dpy, win, PropertyChangeMask);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < CM_SEL_MAX; i++) {
|
||||||
|
struct selection sel = cfg.selections[i];
|
||||||
|
if (!sel.active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Atom sel_atom = sels[i].selection;
|
||||||
|
XFixesSelectSelectionInput(dpy, win, sel_atom,
|
||||||
|
XFixesSetSelectionOwnerNotifyMask);
|
||||||
|
dbg("Getting initial value for selection %s\n", sel.name);
|
||||||
|
XConvertSelection(dpy, sel_atom, XInternAtom(dpy, "UTF8_STRING", False),
|
||||||
|
sels[i].storage, win, CurrentTime);
|
||||||
|
get_one_clip(evt_base);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _noreturn_ run(int evt_base) {
|
||||||
|
while (1) {
|
||||||
|
get_one_clip(evt_base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef UNIT_TEST
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
int evt_base;
|
||||||
|
|
||||||
|
cfg = setup("clipmenud");
|
||||||
|
exec_man_on_help(argc, argv);
|
||||||
|
die_on(argc != 1, "clipmenud doesn't accept any arguments\n");
|
||||||
|
|
||||||
|
_drop_(close) int session_fd =
|
||||||
|
open(get_session_lock_path(&cfg), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
|
||||||
|
die_on(session_fd < 0, "Failed to open session file: %s\n",
|
||||||
|
strerror(errno));
|
||||||
|
die_on(flock(session_fd, LOCK_EX | LOCK_NB) < 0,
|
||||||
|
"Failed to lock session file -- is another clipmenud running?\n");
|
||||||
|
|
||||||
|
expect(ftruncate(session_fd, 0) == 0);
|
||||||
|
expect(lseek(session_fd, 0, SEEK_SET) == 0);
|
||||||
|
dprintf(session_fd, "%ld\n", (long)getpid());
|
||||||
|
|
||||||
|
write_status();
|
||||||
|
|
||||||
|
_drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY);
|
||||||
|
_drop_(close) int snip_fd =
|
||||||
|
open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600);
|
||||||
|
expect(content_dir_fd >= 0 && snip_fd >= 0);
|
||||||
|
|
||||||
|
expect(cs_init(&cs, snip_fd, content_dir_fd) == 0);
|
||||||
|
|
||||||
|
die_on(!(dpy = XOpenDisplay(NULL)), "Cannot open display\n");
|
||||||
|
win = DefaultRootWindow(dpy);
|
||||||
|
setup_selections(dpy, sels);
|
||||||
|
|
||||||
|
incr_atom = XInternAtom(dpy, "INCR", False);
|
||||||
|
timestamp_atom = XInternAtom(dpy, "CLIPMENUD_TIMESTAMP", False);
|
||||||
|
|
||||||
|
expect(pipe(sig_pipe) == 0);
|
||||||
|
set_fd_flags(sig_pipe[0]);
|
||||||
|
set_fd_flags(sig_pipe[1]);
|
||||||
|
|
||||||
|
struct sigaction sa = {0};
|
||||||
|
sa.sa_handler = signal_handler;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = SA_RESTART;
|
||||||
|
expect(sigaction(SIGUSR1, &sa, NULL) == 0);
|
||||||
|
expect(sigaction(SIGUSR2, &sa, NULL) == 0);
|
||||||
|
expect(signal(SIGCHLD, SIG_IGN) != SIG_ERR);
|
||||||
|
|
||||||
|
int unused;
|
||||||
|
die_on(!XFixesQueryExtension(dpy, &evt_base, &unused), "XFixes missing\n");
|
||||||
|
|
||||||
|
setup_watches(evt_base);
|
||||||
|
|
||||||
|
if (!cfg.oneshot) {
|
||||||
|
run(evt_base);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(cs_destroy(&cs) == 0);
|
||||||
|
config_free(&cfg);
|
||||||
|
XCloseDisplay(dpy);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
286
src/clipserve.c
Normal file
286
src/clipserve.c
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "store.h"
|
||||||
|
#include "util.h"
|
||||||
|
#include "x.h"
|
||||||
|
|
||||||
|
static struct incr_transfer *it_list = NULL;
|
||||||
|
static Display *dpy;
|
||||||
|
static Atom incr_atom;
|
||||||
|
|
||||||
|
static size_t incr_threshold;
|
||||||
|
static size_t chunk_size;
|
||||||
|
|
||||||
|
static enum selection_type selection_name_to_type(const char *name) {
|
||||||
|
if (streq(name, "clipboard")) {
|
||||||
|
return CM_SEL_CLIPBOARD;
|
||||||
|
}
|
||||||
|
if (streq(name, "primary")) {
|
||||||
|
return CM_SEL_PRIMARY;
|
||||||
|
}
|
||||||
|
if (streq(name, "secondary")) {
|
||||||
|
return CM_SEL_SECONDARY;
|
||||||
|
}
|
||||||
|
return CM_SEL_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start an INCR transfer.
|
||||||
|
*/
|
||||||
|
static void incr_send_start(XSelectionRequestEvent *req,
|
||||||
|
struct cs_content *content) {
|
||||||
|
long incr_size = content->size;
|
||||||
|
XChangeProperty(dpy, req->requestor, req->property, incr_atom, 32,
|
||||||
|
PropModeReplace, (unsigned char *)&incr_size, 1);
|
||||||
|
|
||||||
|
struct incr_transfer *it = malloc(sizeof(struct incr_transfer));
|
||||||
|
expect(it);
|
||||||
|
*it = (struct incr_transfer){
|
||||||
|
.requestor = req->requestor,
|
||||||
|
.property = req->property,
|
||||||
|
.target = req->target,
|
||||||
|
.format = 8,
|
||||||
|
.data = (char *)content->data,
|
||||||
|
.data_size = content->size,
|
||||||
|
.offset = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
it_dbg(it, "Starting transfer\n");
|
||||||
|
it_add(&it_list, it);
|
||||||
|
|
||||||
|
// Listen for PropertyNotify events on the requestor window
|
||||||
|
XSelectInput(dpy, it->requestor, PropertyChangeMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finish an INCR transfer.
|
||||||
|
*/
|
||||||
|
static void incr_send_finish(struct incr_transfer *it) {
|
||||||
|
XChangeProperty(dpy, it->requestor, it->property, it->target, it->format,
|
||||||
|
PropModeReplace, NULL, 0);
|
||||||
|
it_dbg(it, "Transfer complete\n");
|
||||||
|
it_remove(&it_list, it);
|
||||||
|
free(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continue sending data during an INCR transfer.
|
||||||
|
*/
|
||||||
|
static void incr_send_chunk(const XPropertyEvent *pe) {
|
||||||
|
if (pe->state != PropertyDelete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct incr_transfer *it = it_list;
|
||||||
|
while (it) {
|
||||||
|
if (it->requestor == pe->window && it->property == pe->atom) {
|
||||||
|
size_t remaining = it->data_size - it->offset;
|
||||||
|
size_t this_chunk_size =
|
||||||
|
(remaining > chunk_size) ? chunk_size : remaining;
|
||||||
|
|
||||||
|
it_dbg(it,
|
||||||
|
"Sending chunk (bytes sent: %zu, bytes remaining: %zu)\n",
|
||||||
|
it->offset, remaining);
|
||||||
|
|
||||||
|
if (this_chunk_size > 0) {
|
||||||
|
XChangeProperty(dpy, it->requestor, it->property, it->target,
|
||||||
|
it->format, PropModeReplace,
|
||||||
|
(unsigned char *)(it->data + it->offset),
|
||||||
|
this_chunk_size);
|
||||||
|
it->offset += this_chunk_size;
|
||||||
|
} else {
|
||||||
|
incr_send_finish(it);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
it = it->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve clipboard content for all X11 selection requests until all selections
|
||||||
|
* have been claimed by another application.
|
||||||
|
*/
|
||||||
|
static void _nonnull_ serve_clipboard(uint64_t hash, struct cs_content *content,
|
||||||
|
const bool selection_active[CM_SEL_MAX]) {
|
||||||
|
bool running = true;
|
||||||
|
XEvent evt;
|
||||||
|
Atom targets, utf8_string;
|
||||||
|
Atom selection_atoms[CM_SEL_MAX];
|
||||||
|
Window win;
|
||||||
|
int remaining_selections;
|
||||||
|
size_t selection_atoms_len = 0;
|
||||||
|
|
||||||
|
dpy = XOpenDisplay(NULL);
|
||||||
|
expect(dpy);
|
||||||
|
|
||||||
|
// Threshold and payload size are intentionally different: the server's
|
||||||
|
// max request size is a reasonable INCR cutoff, but too aggressive as an
|
||||||
|
// outbound chunk size for some requestors.
|
||||||
|
incr_threshold = get_incr_threshold(dpy);
|
||||||
|
chunk_size = get_chunk_size(dpy);
|
||||||
|
|
||||||
|
win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 1, 1, 0, 0, 0);
|
||||||
|
XStoreName(dpy, win, "clipserve");
|
||||||
|
targets = XInternAtom(dpy, "TARGETS", False);
|
||||||
|
utf8_string = XInternAtom(dpy, "UTF8_STRING", False);
|
||||||
|
incr_atom = XInternAtom(dpy, "INCR", False);
|
||||||
|
|
||||||
|
if (selection_active[CM_SEL_PRIMARY]) {
|
||||||
|
selection_atoms[selection_atoms_len++] = XA_PRIMARY;
|
||||||
|
}
|
||||||
|
if (selection_active[CM_SEL_CLIPBOARD]) {
|
||||||
|
selection_atoms[selection_atoms_len++] =
|
||||||
|
XInternAtom(dpy, "CLIPBOARD", False);
|
||||||
|
}
|
||||||
|
if (selection_active[CM_SEL_SECONDARY]) {
|
||||||
|
selection_atoms[selection_atoms_len++] = XA_SECONDARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
die_on(selection_atoms_len == 0, "No selections configured\n");
|
||||||
|
|
||||||
|
for (size_t i = 0; i < selection_atoms_len; i++) {
|
||||||
|
bool success = false;
|
||||||
|
for (int attempts = 0; attempts < 5; attempts++) {
|
||||||
|
XSetSelectionOwner(dpy, selection_atoms[i], win, CurrentTime);
|
||||||
|
|
||||||
|
// According to ICCCM 2.1, a client acquiring a selection should
|
||||||
|
// confirm success by verifying with GetSelectionOwner.
|
||||||
|
if (XGetSelectionOwner(dpy, selection_atoms[i]) == win) {
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
die("Failed to set selection for %s\n",
|
||||||
|
XGetAtomName(dpy, selection_atoms[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remaining_selections = (int)selection_atoms_len;
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
XNextEvent(dpy, &evt);
|
||||||
|
switch (evt.type) {
|
||||||
|
case SelectionRequest: {
|
||||||
|
XSelectionRequestEvent *req = &evt.xselectionrequest;
|
||||||
|
XSelectionEvent sev = {.type = SelectionNotify,
|
||||||
|
.display = req->display,
|
||||||
|
.requestor = req->requestor,
|
||||||
|
.selection = req->selection,
|
||||||
|
.time = req->time,
|
||||||
|
.target = req->target,
|
||||||
|
.property = req->property};
|
||||||
|
|
||||||
|
// cppcheck-suppress constVariablePointer
|
||||||
|
// _drop_(XFree) passes this to XFree, so keep it non-const.
|
||||||
|
_drop_(XFree) char *window_title =
|
||||||
|
get_window_title(dpy, req->requestor);
|
||||||
|
dbg("Servicing request to window '%s' (0x%lX) for clip " PRI_HASH
|
||||||
|
"\n",
|
||||||
|
strnull(window_title), (unsigned long)req->requestor, hash);
|
||||||
|
|
||||||
|
if (req->target == targets) {
|
||||||
|
Atom available_targets[] = {utf8_string, XA_STRING};
|
||||||
|
XChangeProperty(dpy, req->requestor, req->property, XA_ATOM,
|
||||||
|
32, PropModeReplace,
|
||||||
|
(unsigned char *)&available_targets,
|
||||||
|
arrlen(available_targets));
|
||||||
|
} else if (req->target == utf8_string ||
|
||||||
|
req->target == XA_STRING) {
|
||||||
|
if (content->size < (off_t)incr_threshold) {
|
||||||
|
// Data size is small enough, send directly
|
||||||
|
XChangeProperty(dpy, req->requestor, req->property,
|
||||||
|
req->target, 8, PropModeReplace,
|
||||||
|
(unsigned char *)content->data,
|
||||||
|
(int)content->size);
|
||||||
|
} else {
|
||||||
|
// Initiate INCR transfer
|
||||||
|
incr_send_start(req, content);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sev.property = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
XSendEvent(dpy, req->requestor, False, 0, (XEvent *)&sev);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SelectionClear: {
|
||||||
|
if (--remaining_selections == 0) {
|
||||||
|
dbg("Finished serving clip " PRI_HASH "\n", hash);
|
||||||
|
running = false;
|
||||||
|
} else {
|
||||||
|
dbg("%d selections remaining to serve for clip " PRI_HASH
|
||||||
|
"\n",
|
||||||
|
remaining_selections, hash);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PropertyNotify: {
|
||||||
|
incr_send_chunk(&evt.xproperty);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XCloseDisplay(dpy);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
_drop_(config_free) struct config cfg = setup("clipserve");
|
||||||
|
exec_man_on_help(argc, argv);
|
||||||
|
|
||||||
|
bool selection_active[CM_SEL_MAX] = {0};
|
||||||
|
bool selection_set = false;
|
||||||
|
int argi = 1;
|
||||||
|
for (; argi < argc; argi++) {
|
||||||
|
if (streq(argv[argi], "--")) {
|
||||||
|
argi++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (streq(argv[argi], "-s") || streq(argv[argi], "--selection")) {
|
||||||
|
die_on(argi + 1 >= argc,
|
||||||
|
"Usage: clipserve [-s selection]... <hash>\n");
|
||||||
|
enum selection_type sel = selection_name_to_type(argv[++argi]);
|
||||||
|
die_on(sel == CM_SEL_INVALID, "Unknown selection: %s\n",
|
||||||
|
argv[argi]);
|
||||||
|
selection_active[sel] = true;
|
||||||
|
selection_set = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
die_on(argi != argc - 1, "Usage: clipserve [-s selection]... <hash>\n");
|
||||||
|
|
||||||
|
if (!selection_set) {
|
||||||
|
selection_active[CM_SEL_PRIMARY] = true;
|
||||||
|
selection_active[CM_SEL_CLIPBOARD] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t hash;
|
||||||
|
expect(str_to_hex64(argv[argi], &hash) == 0);
|
||||||
|
|
||||||
|
_drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY);
|
||||||
|
_drop_(close) int snip_fd =
|
||||||
|
open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600);
|
||||||
|
expect(content_dir_fd >= 0 && snip_fd >= 0);
|
||||||
|
|
||||||
|
_drop_(cs_destroy) struct clip_store cs;
|
||||||
|
expect(cs_init(&cs, snip_fd, content_dir_fd) == 0);
|
||||||
|
|
||||||
|
_drop_(cs_content_unmap) struct cs_content content;
|
||||||
|
die_on(cs_content_get(&cs, hash, &content) < 0,
|
||||||
|
"Hash " PRI_HASH " inaccessible\n", hash);
|
||||||
|
|
||||||
|
serve_clipboard(hash, &content, selection_active);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
500
src/config.c
Normal file
500
src/config.c
Normal file
|
|
@ -0,0 +1,500 @@
|
||||||
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "x.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the runtime directory for storing application data. This is _not_
|
||||||
|
* the clip store, but the place to create the directory that will become it
|
||||||
|
* (unless CM_DIR is set, in which case we use that directly).
|
||||||
|
*/
|
||||||
|
static const char *get_runtime_directory(void) {
|
||||||
|
static const char *runtime_dir = NULL;
|
||||||
|
const char *env_vars[] = {"XDG_RUNTIME_DIR", "TMPDIR"};
|
||||||
|
|
||||||
|
if (runtime_dir) {
|
||||||
|
return runtime_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < arrlen(env_vars); i++) {
|
||||||
|
const char *dir = getenv(env_vars[i]);
|
||||||
|
if (dir) {
|
||||||
|
return (runtime_dir = dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (runtime_dir = "/tmp");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the path to the clip store directory for clipmenu, creating the
|
||||||
|
* directory if it does not exist.
|
||||||
|
*/
|
||||||
|
char *get_cache_dir(struct config *cfg) {
|
||||||
|
expect(cfg->ready);
|
||||||
|
static char cache_dir[PATH_MAX];
|
||||||
|
// In case config changed, do the write anyway
|
||||||
|
snprintf_safe(cache_dir, PATH_MAX, "%s/clipmenu.%d.%ld", cfg->runtime_dir,
|
||||||
|
CLIPMENU_VERSION, (long)getuid());
|
||||||
|
die_on(mkdir(cache_dir, S_IRWXU) != 0 && errno != EEXIST,
|
||||||
|
"Failed to create directory: %s\n", cache_dir);
|
||||||
|
return cache_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_absolute_path(const char *path) {
|
||||||
|
return path && path[0] == '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
static int resolve_path(const char *input, void *output) {
|
||||||
|
char **out_path = output;
|
||||||
|
if (!input) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input[0] == '~') {
|
||||||
|
if (input[1] != '\0' && input[1] != '/') {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
if (!home) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
size_t len = strlen(home) + strlen(input);
|
||||||
|
char *expanded = malloc(len);
|
||||||
|
expect(expanded);
|
||||||
|
snprintf_safe(expanded, len, "%s%s", home, input + 1);
|
||||||
|
*out_path = expanded;
|
||||||
|
} else {
|
||||||
|
char *dup = strdup(input);
|
||||||
|
expect(dup);
|
||||||
|
*out_path = dup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_absolute_path(*out_path)) {
|
||||||
|
free(*out_path);
|
||||||
|
*out_path = NULL;
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This whole section consists of conversion functions to go from a string in
|
||||||
|
* the config file to the type we expect for `struct Config`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
static int convert_bool(const char *str, void *output) {
|
||||||
|
const char *const truthy[] = {"1", "y", "yes", "true", "on"};
|
||||||
|
const char *const falsy[] = {"0", "n", "no", "false", "off"};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < arrlen(truthy); i++) {
|
||||||
|
if (strceq(str, truthy[i])) {
|
||||||
|
*(bool *)output = true;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < arrlen(falsy); i++) {
|
||||||
|
if (strceq(str, falsy[i])) {
|
||||||
|
*(bool *)output = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int convert_positive_int(const char *str, void *output) {
|
||||||
|
char *end;
|
||||||
|
long val = strtol(str, &end, 10);
|
||||||
|
if (*end != '\0' || end == str || val < 0 || val > INT_MAX) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
*(int *)output = (int)val;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int convert_ignore_window(const char *str, void *output) {
|
||||||
|
struct ignore_window *iw = output;
|
||||||
|
iw->set = (bool)str;
|
||||||
|
if (!iw->set) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (regcomp(&iw->rgx, str, REG_EXTENDED | REG_NOSUB)) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int convert_cm_dir(const char *str, void *output) {
|
||||||
|
if (!str) {
|
||||||
|
str = get_runtime_directory();
|
||||||
|
}
|
||||||
|
return resolve_path(str, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _nonnull_ convert_launcher(const char *str, void *output) {
|
||||||
|
struct launcher *lnch = output;
|
||||||
|
|
||||||
|
lnch->custom = strdup(str);
|
||||||
|
expect(lnch->custom);
|
||||||
|
|
||||||
|
if (streq(str, "rofi")) {
|
||||||
|
lnch->ltype = LAUNCHER_ROFI;
|
||||||
|
} else {
|
||||||
|
lnch->ltype = LAUNCHER_CUSTOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define DEFAULT_SELECTION_STATE(name) \
|
||||||
|
(struct selection) { name, 0, NULL }
|
||||||
|
|
||||||
|
static char *skip_whitespace(char *str) {
|
||||||
|
while (*str != '\0' && isspace((unsigned char)*str)) {
|
||||||
|
str++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int convert_selections(const char *str, void *output) {
|
||||||
|
struct selection *sels = malloc(3 * sizeof(struct selection));
|
||||||
|
expect(sels);
|
||||||
|
sels[CM_SEL_CLIPBOARD] = DEFAULT_SELECTION_STATE("clipboard");
|
||||||
|
sels[CM_SEL_PRIMARY] = DEFAULT_SELECTION_STATE("primary");
|
||||||
|
sels[CM_SEL_SECONDARY] = DEFAULT_SELECTION_STATE("secondary");
|
||||||
|
|
||||||
|
_drop_(free) char *inner_str = strdup(str);
|
||||||
|
expect(inner_str);
|
||||||
|
char *next = skip_whitespace(inner_str);
|
||||||
|
while (*next != '\0') {
|
||||||
|
const char *token = next;
|
||||||
|
while (*next != '\0' && !isspace((unsigned char)*next)) {
|
||||||
|
next++;
|
||||||
|
}
|
||||||
|
if (*next != '\0') {
|
||||||
|
*next++ = '\0';
|
||||||
|
next = skip_whitespace(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
for (size_t i = 0; i < CM_SEL_MAX; i++) {
|
||||||
|
if (streq(token, sels[i].name)) {
|
||||||
|
sels[i].active = true;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*(struct selection **)output = sels;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the path to the clipmenu configuration file. The user can
|
||||||
|
* manually specify a path with $CM_CONFIG, otherwise, it's inferred based on
|
||||||
|
* $XDG_CONFIG_HOME or ~/.config. It's typically
|
||||||
|
* ~/.config/clipmenu/clipmenu.conf.
|
||||||
|
*/
|
||||||
|
|
||||||
|
static int get_config_file(char *config_path, size_t config_path_len) {
|
||||||
|
const char *cm_config = getenv("CM_CONFIG");
|
||||||
|
const char *xdg_config_home = getenv("XDG_CONFIG_HOME");
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
_drop_(free) char *resolved = NULL;
|
||||||
|
|
||||||
|
if (cm_config) {
|
||||||
|
int ret = resolve_path(cm_config, &resolved);
|
||||||
|
if (ret != 0) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Invalid config path from $CM_CONFIG: expected absolute "
|
||||||
|
"path or leading ~\n");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
snprintf_safe(config_path, config_path_len, "%s", resolved);
|
||||||
|
} else if (xdg_config_home) {
|
||||||
|
int ret = resolve_path(xdg_config_home, &resolved);
|
||||||
|
if (ret != 0) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Invalid config path from $XDG_CONFIG_HOME: expected "
|
||||||
|
"absolute path or leading ~\n");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
snprintf_safe(config_path, config_path_len, "%s/clipmenu/clipmenu.conf",
|
||||||
|
resolved);
|
||||||
|
} else {
|
||||||
|
die_on(!home,
|
||||||
|
"None of $CM_CONFIG, $XDG_CONFIG_HOME, or $HOME is set\n");
|
||||||
|
int ret = resolve_path(home, &resolved);
|
||||||
|
if (ret != 0) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Invalid config path from $HOME: expected absolute path or "
|
||||||
|
"leading ~\n");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
snprintf_safe(config_path, config_path_len,
|
||||||
|
"%s/.config/clipmenu/clipmenu.conf", resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int config_parse_env_vars(struct config_entry entries[],
|
||||||
|
size_t entries_len) {
|
||||||
|
for (size_t i = 0; i < entries_len; ++i) {
|
||||||
|
const char *env_var = entries[i].env_var;
|
||||||
|
if (!env_var) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const char *env_value = getenv(env_var);
|
||||||
|
if (env_value && entries[i].convert(env_value, entries[i].value) != 0) {
|
||||||
|
fprintf(stderr, "Error parsing environment variable for $%s\n",
|
||||||
|
env_var);
|
||||||
|
return -EINVAL;
|
||||||
|
} else if (env_value) {
|
||||||
|
dbg("Config entry %s is set to %s by $%s\n", entries[i].config_key,
|
||||||
|
env_value, env_var);
|
||||||
|
entries[i].is_set = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int config_parse_file(FILE *file, struct config_entry entries[],
|
||||||
|
size_t entries_len) {
|
||||||
|
if (!file) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
char line[256];
|
||||||
|
while (fgets(line, sizeof(line), file)) {
|
||||||
|
char *key = skip_whitespace(line);
|
||||||
|
if (*key == '\0' || *key == '#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *value = key;
|
||||||
|
while (*value != '\0' && !isspace((unsigned char)*value)) {
|
||||||
|
value++;
|
||||||
|
}
|
||||||
|
if (*value == '\0') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*value++ = '\0';
|
||||||
|
value = skip_whitespace(value);
|
||||||
|
|
||||||
|
char *newline = strpbrk(value, "\r\n");
|
||||||
|
if (newline) {
|
||||||
|
*newline = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < entries_len; ++i) {
|
||||||
|
if (!entries[i].is_set && streq(entries[i].config_key, key)) {
|
||||||
|
if (entries[i].convert(value, entries[i].value) != 0) {
|
||||||
|
fprintf(stderr, "Error parsing config file for %s\n",
|
||||||
|
entries[i].config_key);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
dbg("Config entry %s is set to %s by config file\n",
|
||||||
|
entries[i].config_key, value);
|
||||||
|
entries[i].is_set = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int config_apply_default_values(struct config_entry entries[],
|
||||||
|
size_t entries_len) {
|
||||||
|
for (size_t i = 0; i < entries_len; ++i) {
|
||||||
|
if (!entries[i].is_set) {
|
||||||
|
if (entries[i].convert(entries[i].default_value,
|
||||||
|
entries[i].value) != 0) {
|
||||||
|
fprintf(stderr, "Error setting default value for %s\n",
|
||||||
|
entries[i].config_key);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
dbg("Config entry %s is set to %s by fallback\n",
|
||||||
|
entries[i].config_key, entries[i].default_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the clipmenu configuration file and environment variables to set up
|
||||||
|
* the application configuration.
|
||||||
|
*
|
||||||
|
* Prior to version 7, clipmenu and friends could only be configured via
|
||||||
|
* environment variables, so these are supported for backwards compatibility.
|
||||||
|
* In general, it's more straightforward to use the config file nowadays.
|
||||||
|
*
|
||||||
|
* This is generally not expected to be called by applications -- call
|
||||||
|
* config_setup() instead, which provides the right file for you.
|
||||||
|
*/
|
||||||
|
static int config_setup_internal(FILE *file, struct config *cfg) {
|
||||||
|
struct config_entry entries[] = {
|
||||||
|
{"max_clips", "CM_MAX_CLIPS", &cfg->max_clips, convert_positive_int,
|
||||||
|
"1000", 0},
|
||||||
|
{"max_clips_batch", "CM_MAX_CLIPS_BATCH", &cfg->max_clips_batch,
|
||||||
|
convert_positive_int, "100", 0},
|
||||||
|
{"oneshot", "CM_ONESHOT", &cfg->oneshot, convert_positive_int, "0", 0},
|
||||||
|
{"deduplicate", "CM_DEDUPLICATE", &cfg->deduplicate, convert_bool, "0",
|
||||||
|
0},
|
||||||
|
{"own_clipboard", "CM_OWN_CLIPBOARD", &cfg->own_clipboard, convert_bool,
|
||||||
|
"0", 0},
|
||||||
|
{"selections", "CM_SELECTIONS", &cfg->selections, convert_selections,
|
||||||
|
"clipboard primary", 0},
|
||||||
|
{"own_selections", "CM_OWN_SELECTIONS", &cfg->owned_selections,
|
||||||
|
convert_selections, "clipboard", 0},
|
||||||
|
{"ignore_window", "CM_IGNORE_WINDOW", &cfg->ignore_window,
|
||||||
|
convert_ignore_window, NULL, 0},
|
||||||
|
{"launcher", "CM_LAUNCHER", &cfg->launcher, convert_launcher, "dmenu",
|
||||||
|
0},
|
||||||
|
{"launcher_pass_dmenu_args", "CM_LAUNCHER_PASS_DMENU_ARGS",
|
||||||
|
&cfg->launcher_pass_dmenu_args, convert_bool, "1", 0},
|
||||||
|
{"touch_on_select", NULL, &cfg->touch_on_select, convert_bool, "0", 0},
|
||||||
|
{"partial_merge_secs", NULL, &cfg->partial_merge_secs,
|
||||||
|
convert_positive_int, "2", 0},
|
||||||
|
{"cm_dir", "CM_DIR", &cfg->runtime_dir, convert_cm_dir, NULL, 0}};
|
||||||
|
|
||||||
|
size_t entries_len = arrlen(entries);
|
||||||
|
|
||||||
|
int ret = config_parse_env_vars(entries, entries_len);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
ret = config_parse_file(file, entries, entries_len);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
ret = config_apply_default_values(entries, entries_len);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg->ready = true;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frees dynamically allocated memory within the config structure.
|
||||||
|
*/
|
||||||
|
void config_free(struct config *cfg) {
|
||||||
|
free(cfg->runtime_dir);
|
||||||
|
free(cfg->launcher.custom);
|
||||||
|
free(cfg->selections);
|
||||||
|
free(cfg->owned_selections);
|
||||||
|
if (cfg->ignore_window.set) {
|
||||||
|
regfree(&cfg->ignore_window.rgx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the clipmenu configuration by loading settings from environment
|
||||||
|
* variables and the configuration file.
|
||||||
|
*/
|
||||||
|
static void config_setup(struct config *cfg) {
|
||||||
|
char config_path[PATH_MAX];
|
||||||
|
die_on(get_config_file(config_path, sizeof(config_path)) != 0,
|
||||||
|
"Invalid config\n");
|
||||||
|
_drop_(fclose) FILE *file = fopen(config_path, "r");
|
||||||
|
expect(file || errno == ENOENT);
|
||||||
|
die_on(config_setup_internal(file, cfg) != 0, "Invalid config\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static char stdout_buf[512];
|
||||||
|
const char *prog_name = "broken";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs initial setup for clipmenu applications, including setting the
|
||||||
|
* program name (used for dbg()), making sure stdout is line buffered, getting
|
||||||
|
* the config, and setting up X11 error handling.
|
||||||
|
*/
|
||||||
|
struct config setup(const char *inner_prog_name) {
|
||||||
|
struct config cfg;
|
||||||
|
prog_name = inner_prog_name;
|
||||||
|
expect(setvbuf(stdout, stdout_buf, _IOLBF, sizeof(stdout_buf)) == 0);
|
||||||
|
config_setup(&cfg);
|
||||||
|
XSetErrorHandler(xerror_handler);
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_selections(Display *dpy, struct cm_selections *sels) {
|
||||||
|
sels[CM_SEL_CLIPBOARD].selection = XInternAtom(dpy, "CLIPBOARD", False);
|
||||||
|
sels[CM_SEL_CLIPBOARD].storage =
|
||||||
|
XInternAtom(dpy, "CLIPMENUD_CUR_CLIPBOARD", False);
|
||||||
|
sels[CM_SEL_PRIMARY].selection = XA_PRIMARY;
|
||||||
|
sels[CM_SEL_PRIMARY].storage =
|
||||||
|
XInternAtom(dpy, "CLIPMENUD_CUR_PRIMARY", False);
|
||||||
|
sels[CM_SEL_SECONDARY].selection = XA_SECONDARY;
|
||||||
|
sels[CM_SEL_SECONDARY].storage =
|
||||||
|
XInternAtom(dpy, "CLIPMENUD_CUR_SECONDARY", False);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an Atom to a selection_type based on the selection atoms in the
|
||||||
|
* provided cm_selections struct. Returns CM_SEL_INVALID on error if the
|
||||||
|
* selection type is not found.
|
||||||
|
*/
|
||||||
|
enum selection_type
|
||||||
|
selection_atom_to_selection_type(Atom atom, const struct cm_selections *sels) {
|
||||||
|
for (size_t i = 0; i < CM_SEL_MAX; ++i) {
|
||||||
|
if (sels[i].selection == atom) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CM_SEL_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an Atom to a selection_type based on the storage atoms in the provided
|
||||||
|
* cm_selections struct. Returns CM_SEL_INVALID on error if the storage type is
|
||||||
|
* not found.
|
||||||
|
*/
|
||||||
|
enum selection_type
|
||||||
|
storage_atom_to_selection_type(Atom atom, const struct cm_selections *sels) {
|
||||||
|
for (size_t i = 0; i < CM_SEL_MAX; ++i) {
|
||||||
|
if (sels[i].storage == atom) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CM_SEL_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
void exec_man(void) {
|
||||||
|
execlp("man", "man", prog_name, NULL);
|
||||||
|
die("Failed to exec man: %s\n", strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
// cppcheck doesn't know this comes from an outside type
|
||||||
|
// cppcheck-suppress [constParameter,unmatchedSuppression]
|
||||||
|
void exec_man_on_help(int argc, char *argv[]) {
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (streq(argv[i], "--")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (streq(argv[i], "-h") || streq(argv[i], "--help")) {
|
||||||
|
exec_man();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/config.h
Normal file
104
src/config.h
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
#ifndef CM_CONFIG_H
|
||||||
|
#define CM_CONFIG_H
|
||||||
|
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <regex.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
#define CLIPMENU_VERSION 7
|
||||||
|
|
||||||
|
struct selection {
|
||||||
|
const char *name;
|
||||||
|
bool active;
|
||||||
|
Atom *atom;
|
||||||
|
};
|
||||||
|
enum selection_type {
|
||||||
|
CM_SEL_CLIPBOARD,
|
||||||
|
CM_SEL_PRIMARY,
|
||||||
|
CM_SEL_SECONDARY,
|
||||||
|
CM_SEL_MAX,
|
||||||
|
CM_SEL_INVALID
|
||||||
|
};
|
||||||
|
struct cm_selections {
|
||||||
|
Atom selection;
|
||||||
|
Atom storage;
|
||||||
|
};
|
||||||
|
struct ignore_window {
|
||||||
|
bool set;
|
||||||
|
regex_t rgx;
|
||||||
|
};
|
||||||
|
enum launcher_known {
|
||||||
|
LAUNCHER_ROFI,
|
||||||
|
LAUNCHER_CUSTOM,
|
||||||
|
};
|
||||||
|
struct launcher {
|
||||||
|
enum launcher_known ltype;
|
||||||
|
char *custom;
|
||||||
|
};
|
||||||
|
struct config {
|
||||||
|
bool ready;
|
||||||
|
bool debug;
|
||||||
|
char *runtime_dir;
|
||||||
|
int max_clips;
|
||||||
|
int max_clips_batch;
|
||||||
|
int oneshot;
|
||||||
|
bool deduplicate;
|
||||||
|
bool own_clipboard;
|
||||||
|
struct selection *owned_selections;
|
||||||
|
struct selection *selections;
|
||||||
|
struct ignore_window ignore_window;
|
||||||
|
struct launcher launcher;
|
||||||
|
bool launcher_pass_dmenu_args;
|
||||||
|
bool touch_on_select;
|
||||||
|
int partial_merge_secs;
|
||||||
|
};
|
||||||
|
typedef int (*conversion_func_t)(const char *, void *);
|
||||||
|
struct config_entry {
|
||||||
|
const char *config_key;
|
||||||
|
const char *env_var;
|
||||||
|
void *value;
|
||||||
|
conversion_func_t convert;
|
||||||
|
const char *default_value;
|
||||||
|
bool is_set;
|
||||||
|
};
|
||||||
|
|
||||||
|
char *get_cache_dir(struct config *cfg);
|
||||||
|
|
||||||
|
void exec_man(void);
|
||||||
|
void exec_man_on_help(int argc, char *argv[]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a function that generates and caches a path within the application's
|
||||||
|
* cache directory.
|
||||||
|
*
|
||||||
|
* For example, DEFINE_GET_PATH_FUNCTION(foo) defines a function `get_foo_path`
|
||||||
|
* that returns the path to "foo" within the cache directory.
|
||||||
|
*/
|
||||||
|
#define DEFINE_GET_PATH_FUNCTION(name) \
|
||||||
|
static inline char *get_##name##_path(struct config *cfg) { \
|
||||||
|
static char path[PATH_MAX]; \
|
||||||
|
/* Just in case the config changed, do the write anyway */ \
|
||||||
|
snprintf_safe(path, PATH_MAX, "%s/" #name, get_cache_dir(cfg)); \
|
||||||
|
return path; \
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_GET_PATH_FUNCTION(line_cache)
|
||||||
|
DEFINE_GET_PATH_FUNCTION(enabled)
|
||||||
|
DEFINE_GET_PATH_FUNCTION(session_lock)
|
||||||
|
|
||||||
|
extern const char *prog_name;
|
||||||
|
struct config _nonnull_ setup(const char *inner_prog_name);
|
||||||
|
void _nonnull_ setup_selections(Display *dpy, struct cm_selections *sels);
|
||||||
|
enum selection_type _nonnull_
|
||||||
|
selection_atom_to_selection_type(Atom atom, const struct cm_selections *sels);
|
||||||
|
enum selection_type _nonnull_
|
||||||
|
storage_atom_to_selection_type(Atom atom, const struct cm_selections *sels);
|
||||||
|
|
||||||
|
void config_free(struct config *cfg);
|
||||||
|
DEFINE_DROP_FUNC_PTR(struct config, config_free)
|
||||||
|
|
||||||
|
#endif
|
||||||
302
src/menu_util.c
Normal file
302
src/menu_util.c
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "menu_util.h"
|
||||||
|
#include "store.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
#define MAX_ARGS 32
|
||||||
|
/* "[N] " prefix + snip line + " (N lines)\n" suffix + NUL */
|
||||||
|
#define LAUNCHER_LINE_MAX \
|
||||||
|
(1 + UINT64_MAX_STRLEN + 2 + (CS_SNIP_LINE_SIZE - 1) + 2 + \
|
||||||
|
UINT64_MAX_STRLEN + 7 + 1 + 1)
|
||||||
|
|
||||||
|
static int dmenu_user_argc;
|
||||||
|
static char **dmenu_user_argv;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the base 10 padding length for a number.
|
||||||
|
*/
|
||||||
|
static int get_padding_length(size_t num) {
|
||||||
|
int digits = 1;
|
||||||
|
while (num /= 10) {
|
||||||
|
digits++;
|
||||||
|
}
|
||||||
|
return digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the launcher. Called after fork() is already done in the new child.
|
||||||
|
*/
|
||||||
|
static void _noreturn_ _nonnull_ exec_launcher(struct config *cfg,
|
||||||
|
const char *prompt,
|
||||||
|
int *input_pipe,
|
||||||
|
int *output_pipe) {
|
||||||
|
dup2(input_pipe[0], STDIN_FILENO);
|
||||||
|
close(input_pipe[1]);
|
||||||
|
close(output_pipe[0]);
|
||||||
|
dup2(output_pipe[1], STDOUT_FILENO);
|
||||||
|
|
||||||
|
const char *dmenu_args[] = {"-p", prompt, "-l", "20"};
|
||||||
|
const char **cmd = malloc(MAX_ARGS * sizeof(char *));
|
||||||
|
|
||||||
|
size_t d_i = 0;
|
||||||
|
|
||||||
|
switch (cfg->launcher.ltype) {
|
||||||
|
case LAUNCHER_ROFI:
|
||||||
|
cmd[d_i++] = "rofi";
|
||||||
|
cmd[d_i++] = "--";
|
||||||
|
cmd[d_i++] = "-dmenu";
|
||||||
|
break;
|
||||||
|
case LAUNCHER_CUSTOM:
|
||||||
|
cmd[d_i++] = cfg->launcher.custom;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
die("Unreachable\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg->launcher_pass_dmenu_args) {
|
||||||
|
expect(d_i + arrlen(dmenu_args) < MAX_ARGS);
|
||||||
|
for (size_t i = 0; i < arrlen(dmenu_args); i++) {
|
||||||
|
cmd[d_i++] = dmenu_args[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 1; i < dmenu_user_argc && d_i < MAX_ARGS - 1; i++, d_i++) {
|
||||||
|
cmd[d_i] = dmenu_user_argv[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd[d_i] = NULL;
|
||||||
|
execvp(cmd[0], (char *const *)cmd); // SUS says cmd unchanged
|
||||||
|
die("Failed to exec %s: %s\n", cmd[0], strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int dprintf_ellipsise_long_snip_line(int fd, const char *line) {
|
||||||
|
size_t line_len = strlen(line);
|
||||||
|
if (line_len == CS_SNIP_LINE_SIZE - 1) {
|
||||||
|
return dprintf(fd, "%.*s...", (int)(CS_SNIP_LINE_SIZE - 4), line);
|
||||||
|
} else {
|
||||||
|
return dprintf(fd, "%s", line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int launcher_write_all(int fd, const char *buf, size_t count) {
|
||||||
|
while (count > 0) {
|
||||||
|
ssize_t written = write(fd, buf, count);
|
||||||
|
if (written < 0) {
|
||||||
|
if (errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
buf += written;
|
||||||
|
count -= (size_t)written;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int wait_for_pid(pid_t pid, int *status) {
|
||||||
|
while (waitpid(pid, status, 0) < 0) {
|
||||||
|
if (errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parse_sel_idx(const char *line, size_t cur_clips, size_t *out_idx) {
|
||||||
|
if (line[0] != '[') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const char *start = line + 1;
|
||||||
|
const char *end = strchr(start, ']');
|
||||||
|
if (!end) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char idx_str[UINT64_MAX_STRLEN + 1];
|
||||||
|
size_t len = (size_t)(end - start);
|
||||||
|
if (len >= sizeof(idx_str)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(idx_str, start, len);
|
||||||
|
idx_str[len] = '\0';
|
||||||
|
uint64_t sel_idx;
|
||||||
|
if (str_to_uint64(idx_str, &sel_idx) < 0 || sel_idx == 0 ||
|
||||||
|
sel_idx > cur_clips) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*out_idx = (size_t)(sel_idx - 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the available clips to the launcher and reads back the user's
|
||||||
|
* selection(s), calling action for each selected clip.
|
||||||
|
*/
|
||||||
|
static int _nonnull_ interact_with_dmenu(struct config *cfg, int *input_pipe,
|
||||||
|
int *output_pipe, pid_t launcher_pid,
|
||||||
|
clip_action_fn action) {
|
||||||
|
close(input_pipe[0]);
|
||||||
|
close(output_pipe[1]);
|
||||||
|
|
||||||
|
_drop_(close) int content_dir_fd = open(get_cache_dir(cfg), O_RDONLY);
|
||||||
|
_drop_(close) int snip_fd =
|
||||||
|
open(get_line_cache_path(cfg), O_RDWR | O_CREAT, 0600);
|
||||||
|
expect(content_dir_fd >= 0 && snip_fd >= 0);
|
||||||
|
|
||||||
|
_drop_(cs_destroy) struct clip_store cs;
|
||||||
|
expect(cs_init(&cs, snip_fd, content_dir_fd) == 0);
|
||||||
|
|
||||||
|
struct ref_guard guard = cs_ref(&cs);
|
||||||
|
size_t cur_clips;
|
||||||
|
expect(cs_len(&cs, &cur_clips) == 0);
|
||||||
|
_drop_(free) uint64_t *idx_to_hash = malloc(cur_clips * sizeof(uint64_t));
|
||||||
|
expect(idx_to_hash);
|
||||||
|
int pad = get_padding_length(cur_clips);
|
||||||
|
size_t clip_idx = cur_clips;
|
||||||
|
bool launcher_closed_stdin = false;
|
||||||
|
int forced_ret = 0;
|
||||||
|
|
||||||
|
struct sigaction ignore_sa = {0};
|
||||||
|
ignore_sa.sa_handler = SIG_IGN;
|
||||||
|
sigemptyset(&ignore_sa.sa_mask);
|
||||||
|
struct sigaction old_sa;
|
||||||
|
expect(sigaction(SIGPIPE, &ignore_sa, &old_sa) == 0);
|
||||||
|
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) {
|
||||||
|
size_t idx = --clip_idx;
|
||||||
|
idx_to_hash[idx] = snip->hash;
|
||||||
|
|
||||||
|
if (launcher_closed_stdin) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char prefix[64];
|
||||||
|
size_t prefix_len =
|
||||||
|
snprintf_safe(prefix, sizeof(prefix), "[%*zu] ", pad, idx + 1);
|
||||||
|
int write_ret = launcher_write_all(input_pipe[1], prefix, prefix_len);
|
||||||
|
if (write_ret == -EPIPE) {
|
||||||
|
launcher_closed_stdin = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (write_ret < 0) {
|
||||||
|
forced_ret = EXIT_FAILURE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
write_ret = dprintf_ellipsise_long_snip_line(input_pipe[1], snip->line);
|
||||||
|
if (write_ret < 0 && errno == EPIPE) {
|
||||||
|
launcher_closed_stdin = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (write_ret < 0) {
|
||||||
|
forced_ret = EXIT_FAILURE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snip->nr_lines > 1) {
|
||||||
|
char lines_suffix[64];
|
||||||
|
size_t lines_suffix_len =
|
||||||
|
snprintf_safe(lines_suffix, sizeof(lines_suffix),
|
||||||
|
" (%zu lines)", snip->nr_lines);
|
||||||
|
write_ret = launcher_write_all(input_pipe[1], lines_suffix,
|
||||||
|
lines_suffix_len);
|
||||||
|
if (write_ret == -EPIPE) {
|
||||||
|
launcher_closed_stdin = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (write_ret < 0) {
|
||||||
|
forced_ret = EXIT_FAILURE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_ret = launcher_write_all(input_pipe[1], "\n", 1);
|
||||||
|
if (write_ret == -EPIPE) {
|
||||||
|
launcher_closed_stdin = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (write_ret < 0) {
|
||||||
|
forced_ret = EXIT_FAILURE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've written everything and have our own map, no need to hold any more
|
||||||
|
cs_unref(guard.cs);
|
||||||
|
|
||||||
|
close(input_pipe[1]);
|
||||||
|
expect(sigaction(SIGPIPE, &old_sa, NULL) == 0);
|
||||||
|
|
||||||
|
_drop_(fclose) FILE *output = fdopen(output_pipe[0], "r");
|
||||||
|
expect(output != NULL);
|
||||||
|
char line[LAUNCHER_LINE_MAX];
|
||||||
|
while (!forced_ret && fgets(line, sizeof(line), output) != NULL) {
|
||||||
|
size_t len = strlen(line);
|
||||||
|
if (len > 0 && line[len - 1] == '\n') {
|
||||||
|
line[len - 1] = '\0';
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
if (len == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size_t idx;
|
||||||
|
if (parse_sel_idx(line, cur_clips, &idx) == 0) {
|
||||||
|
int ret = action(cfg, idx_to_hash[idx]);
|
||||||
|
if (ret != 0) {
|
||||||
|
forced_ret = ret;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
forced_ret = EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int dmenu_status;
|
||||||
|
int wait_ret = wait_for_pid(launcher_pid, &dmenu_status);
|
||||||
|
|
||||||
|
if (forced_ret || wait_ret < 0 || !WIFEXITED(dmenu_status)) {
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WEXITSTATUS(dmenu_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts the user to select clip(s) via their configured launcher, and
|
||||||
|
* executes the provided action on each selected clip.
|
||||||
|
*/
|
||||||
|
int _nonnull_ menu_prompt_and_act(struct config *cfg, const char *prompt,
|
||||||
|
clip_action_fn action) {
|
||||||
|
int input_pipe[2], output_pipe[2];
|
||||||
|
expect(pipe(input_pipe) == 0 && pipe(output_pipe) == 0);
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
expect(pid >= 0);
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
exec_launcher(cfg, prompt, input_pipe, output_pipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interact_with_dmenu(cfg, input_pipe, output_pipe, pid, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the user-provided command-line arguments for launcher passthrough.
|
||||||
|
*
|
||||||
|
* This is janky and just mimics how clipmenu worked by itself, in the long run
|
||||||
|
* we should not rely on these.
|
||||||
|
*/
|
||||||
|
void menu_set_user_args(int argc, char **argv) {
|
||||||
|
dmenu_user_argc = argc;
|
||||||
|
dmenu_user_argv = argv;
|
||||||
|
}
|
||||||
18
src/menu_util.h
Normal file
18
src/menu_util.h
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#ifndef CM_MENU_UTIL_H
|
||||||
|
#define CM_MENU_UTIL_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function type for handling selected clips.
|
||||||
|
*/
|
||||||
|
typedef int (*clip_action_fn)(struct config *cfg, uint64_t hash);
|
||||||
|
|
||||||
|
void menu_set_user_args(int argc, char **argv);
|
||||||
|
|
||||||
|
int _nonnull_ menu_prompt_and_act(struct config *cfg, const char *prompt,
|
||||||
|
clip_action_fn action);
|
||||||
|
|
||||||
|
#endif
|
||||||
847
src/store.c
Normal file
847
src/store.c
Normal file
|
|
@ -0,0 +1,847 @@
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "store.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TERMINOLOGY
|
||||||
|
*
|
||||||
|
* - Clip store: A pairing of a file containing snips and a content directory
|
||||||
|
* - Clip store entry: A single pair of snip and content entry
|
||||||
|
* - Snip file: A file which maps lines to content entries using hashes
|
||||||
|
* - Snip: A (hash, line) pair in the clip store, representing a content entry
|
||||||
|
* - Content entry: The full data stored from the clipboard
|
||||||
|
* - Content directory: The directory in which the content entries are stored
|
||||||
|
*
|
||||||
|
* KEY FUNCTIONS:
|
||||||
|
*
|
||||||
|
* - cs_add - add a clip store entry
|
||||||
|
* - cs_remove - remove a clip store entry by callback
|
||||||
|
* - cs_trim - trim to the newest/oldest N entries
|
||||||
|
* - cs_snip_iter - iterate over snip hashes and lines
|
||||||
|
* - cs_content_get - get the content for a snip hash
|
||||||
|
*
|
||||||
|
* CLIP STORE DESIGN
|
||||||
|
*
|
||||||
|
* Our primary focus is on achieving high efficiency in appending new snips,
|
||||||
|
* iterating over the entire list of snips, and replacing the final snip in the
|
||||||
|
* snip file. For this reason complexity is avoided in processing deletions,
|
||||||
|
* since deletions are usually rare. This avoids us having to implement (for
|
||||||
|
* example) tombstones, which would slow things down and complicate iteration.
|
||||||
|
*
|
||||||
|
* CONTENT DIRECTORY DESIGN
|
||||||
|
*
|
||||||
|
* The content directory is extremely simple: it contains files with the same
|
||||||
|
* name as the snip hash. This allows quickly going from the one line summary
|
||||||
|
* in the snip to the full contents.
|
||||||
|
*
|
||||||
|
* SYNCHRONISATION
|
||||||
|
*
|
||||||
|
* The clip store's size may be increased or decreased by another program using
|
||||||
|
* the library, so before each operation we must take a lock and check if the
|
||||||
|
* header was updated. If it was, we update the size of the mmapped area to
|
||||||
|
* suit. The lock is implemented using flock() on cs->snip_fd, see cs_ref(),
|
||||||
|
* cs_ref_no_update(), and cs_unref().
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the needed file size in bytes for @nr_snips snips, adding the
|
||||||
|
* header.
|
||||||
|
*
|
||||||
|
* @nr_snips: The number of snips to calculate for
|
||||||
|
*/
|
||||||
|
static size_t _must_use_ cs_file_size(size_t nr_snips) {
|
||||||
|
return (nr_snips + 1) * CS_SNIP_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the consistency of the clip store's header information.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @file_size: The current size of the file
|
||||||
|
*/
|
||||||
|
static int _must_use_ _nonnull_ cs_header_validate(const struct clip_store *cs,
|
||||||
|
size_t file_size) {
|
||||||
|
if (cs->header->nr_snips > cs->header->nr_snips_alloc ||
|
||||||
|
(cs->header->nr_snips_alloc + 1) * CS_SNIP_SIZE != file_size) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _must_use_ _nonnull_ cs_remap(struct clip_store *cs, size_t old_size,
|
||||||
|
size_t new_size) {
|
||||||
|
struct cs_header *new_header = mmap(NULL, new_size, PROT_READ | PROT_WRITE,
|
||||||
|
MAP_SHARED, cs->snip_fd, 0);
|
||||||
|
if (new_header == MAP_FAILED) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
if (munmap(cs->header, old_size) < 0) {
|
||||||
|
int ret = negative_errno();
|
||||||
|
munmap(new_header, new_size);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
cs->header = new_header;
|
||||||
|
cs->snips = (struct cs_snip *)(cs->header + 1);
|
||||||
|
cs->mapped_file_size = new_size;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrease the reference count for the clip store lock, unrefing it if
|
||||||
|
* the refcount reaches zero.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
*/
|
||||||
|
void cs_unref(struct clip_store *cs) {
|
||||||
|
expect(cs->refcount > 0);
|
||||||
|
cs->refcount--;
|
||||||
|
if (cs->refcount == 0) {
|
||||||
|
expect(flock(cs->snip_fd, LOCK_UN) == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increase the reference count for the clip store lock.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
*/
|
||||||
|
static struct ref_guard _must_use_ _nonnull_
|
||||||
|
cs_ref_no_update(struct clip_store *cs) {
|
||||||
|
struct ref_guard guard = {.status = 0, .unref = cs_unref, .cs = cs};
|
||||||
|
if (cs->refcount == 0) {
|
||||||
|
expect(flock(cs->snip_fd, LOCK_EX) == 0);
|
||||||
|
}
|
||||||
|
static_assert(sizeof(cs->refcount) == sizeof(size_t),
|
||||||
|
"refcount type wrong");
|
||||||
|
expect(cs->refcount < SIZE_MAX);
|
||||||
|
cs->refcount++;
|
||||||
|
return guard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increase the reference count for the clip store lock, and remap as needed if
|
||||||
|
* the header values have changed.
|
||||||
|
*
|
||||||
|
* Even if the guard status indicates an error, you must still call cs_unref().
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
*/
|
||||||
|
struct ref_guard cs_ref(struct clip_store *cs) {
|
||||||
|
struct ref_guard guard = cs_ref_no_update(cs);
|
||||||
|
|
||||||
|
if (cs->refcount > 1) {
|
||||||
|
// We're an inner reference, so any necessary remapping has already
|
||||||
|
// been performed.
|
||||||
|
return guard;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cs->local_nr_snips != cs->header->nr_snips ||
|
||||||
|
cs->local_nr_snips_alloc != cs->header->nr_snips_alloc) {
|
||||||
|
struct stat st;
|
||||||
|
if (fstat(cs->snip_fd, &st) < 0) {
|
||||||
|
guard.status = negative_errno();
|
||||||
|
return guard;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = cs_header_validate(cs, (size_t)st.st_size);
|
||||||
|
if (ret < 0) {
|
||||||
|
guard.status = ret;
|
||||||
|
return guard;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we shrank, no need to remap, since we'll just use the new bounds.
|
||||||
|
if (cs->local_nr_snips_alloc < cs->header->nr_snips_alloc) {
|
||||||
|
int remap_ret = cs_remap(cs, cs->mapped_file_size,
|
||||||
|
cs_file_size(cs->header->nr_snips_alloc));
|
||||||
|
if (remap_ret < 0) {
|
||||||
|
guard.status = remap_ret;
|
||||||
|
return guard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cs->local_nr_snips = cs->header->nr_snips;
|
||||||
|
cs->local_nr_snips_alloc = cs->header->nr_snips_alloc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return guard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* _drop_() function for when a `ref_guard` structure goes out of scope.
|
||||||
|
*
|
||||||
|
* @guard: The guard lock
|
||||||
|
*/
|
||||||
|
void drop_cs_unref(struct ref_guard *guard) {
|
||||||
|
guard->status = -EBADF;
|
||||||
|
guard->unref(guard->cs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the clip store, releasing all of its resources.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
*/
|
||||||
|
int cs_destroy(struct clip_store *cs) {
|
||||||
|
cs->ready = false;
|
||||||
|
if (munmap(cs->header, cs->mapped_file_size)) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* _drop_() function for when a `clip_store` goes out of scope.
|
||||||
|
*
|
||||||
|
* @guard: The guard lock
|
||||||
|
*/
|
||||||
|
void drop_cs_destroy(struct clip_store *cs) { expect(cs_destroy(cs) == 0); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise a `struct clip_store` with snip_fd open to a file for snip
|
||||||
|
* storage and content_fd open to a directory for content entry storage.
|
||||||
|
*
|
||||||
|
* The snip file is extended and the header snip is written if the file size is
|
||||||
|
* zero. The file is mapped into memory until cs_destroy() is called.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to initialise
|
||||||
|
* @snip_fd: Open file descriptor for the snip file
|
||||||
|
* @content_dir_fd: Open file descriptor for the content directory
|
||||||
|
*/
|
||||||
|
int cs_init(struct clip_store *cs, int snip_fd, int content_dir_fd) {
|
||||||
|
cs->ready = false;
|
||||||
|
cs->snip_fd = snip_fd;
|
||||||
|
cs->content_dir_fd = content_dir_fd;
|
||||||
|
cs->refcount = 0;
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref_no_update(cs);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (fstat(snip_fd, &st) < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
if (st.st_size % CS_SNIP_SIZE != 0) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t file_size = (size_t)st.st_size;
|
||||||
|
if (file_size == 0) {
|
||||||
|
file_size = CS_SNIP_SIZE;
|
||||||
|
if (ftruncate(snip_fd, (off_t)file_size) < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cs->header =
|
||||||
|
mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, snip_fd, 0);
|
||||||
|
if (cs->header == MAP_FAILED) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = cs_header_validate(cs, file_size);
|
||||||
|
if (ret < 0) {
|
||||||
|
munmap(cs->header, file_size);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
cs->snips = (struct cs_snip *)(cs->header + 1);
|
||||||
|
cs->local_nr_snips = cs->header->nr_snips;
|
||||||
|
cs->local_nr_snips_alloc = cs->header->nr_snips_alloc;
|
||||||
|
cs->mapped_file_size = file_size;
|
||||||
|
cs->ready = true;
|
||||||
|
|
||||||
|
(void)guard; // Old clang will complain guard is unused, despite cleanup
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round up a number to the nearest multiple of a specified step.
|
||||||
|
*
|
||||||
|
* @n: The number to round up
|
||||||
|
* @step: The step size to round up to
|
||||||
|
*/
|
||||||
|
static size_t round_up(size_t n, size_t step) {
|
||||||
|
return ((n + step - 1) / step) * step;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust the size of the mmapped file used by the clip store to the specified
|
||||||
|
* new size. It first attempts to resize the file using ftruncate(). If this
|
||||||
|
* operation is successful, it then remaps the memory mapping to reflect the
|
||||||
|
* new size of the file.
|
||||||
|
*
|
||||||
|
* WARNING: cs->snips and cs->header may move after cs_file_resize(), so
|
||||||
|
* copied pointers from before invocation must not be used after calling this.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @new_nr_snips: The new number of snips in the snip file
|
||||||
|
*/
|
||||||
|
static int _must_use_ _nonnull_ cs_file_resize(struct clip_store *cs,
|
||||||
|
size_t new_nr_snips) {
|
||||||
|
bool grow = new_nr_snips >= cs->header->nr_snips;
|
||||||
|
|
||||||
|
if (grow && new_nr_snips <= cs->header->nr_snips_alloc) {
|
||||||
|
cs->header->nr_snips = new_nr_snips;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If this is a shrink, do it exactly: someone may be deleting sensitive
|
||||||
|
* snips and so it's best to remove them immediately. Otherwise, batch
|
||||||
|
* allocate. */
|
||||||
|
size_t new_nr_snips_alloc =
|
||||||
|
grow ? round_up(new_nr_snips, CS_SNIP_ALLOC_BATCH) : new_nr_snips;
|
||||||
|
|
||||||
|
size_t new_size = cs_file_size(new_nr_snips_alloc);
|
||||||
|
if (ftruncate(cs->snip_fd, (off_t)new_size) < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grow) {
|
||||||
|
size_t old_file_size = cs_file_size(cs->header->nr_snips_alloc);
|
||||||
|
int ret = cs_remap(cs, cs->mapped_file_size, new_size);
|
||||||
|
if (ret < 0) {
|
||||||
|
expect(ftruncate(cs->snip_fd, (off_t)old_file_size) == 0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cs->header->nr_snips = cs->local_nr_snips = new_nr_snips;
|
||||||
|
cs->header->nr_snips_alloc = cs->local_nr_snips_alloc = new_nr_snips_alloc;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a clip store snip to contain the specified hash and line content.
|
||||||
|
*
|
||||||
|
* @snip: Pointer to the snip to modify
|
||||||
|
* @hash: The new hash value for the snip
|
||||||
|
* @line: The new line content for the snip
|
||||||
|
* @nr_lines: The number of lines in the line content
|
||||||
|
*/
|
||||||
|
static void _nonnull_ cs_snip_update(struct cs_snip *snip, uint64_t hash,
|
||||||
|
const char *line, uint64_t nr_lines) {
|
||||||
|
snip->hash = hash;
|
||||||
|
snip->doomed = false;
|
||||||
|
snip->nr_lines = nr_lines;
|
||||||
|
strncpy(snip->line, line, CS_SNIP_LINE_SIZE - 1);
|
||||||
|
snip->line[CS_SNIP_LINE_SIZE - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the first non-empty line from a given text buffer and copies it to
|
||||||
|
* the output buffer. Returns the total number of lines. A final line with no
|
||||||
|
* newline is considered a line for accounting purposes.
|
||||||
|
*
|
||||||
|
* @text: The input text buffer
|
||||||
|
* @out: The output buffer. Must be at least CS_SNIP_LINE_SIZE bytes
|
||||||
|
*/
|
||||||
|
size_t first_line(const char *text, char *out) {
|
||||||
|
bool found = false;
|
||||||
|
size_t nr_lines = 0;
|
||||||
|
const char *cur = text;
|
||||||
|
|
||||||
|
out[0] = '\0';
|
||||||
|
|
||||||
|
for (; *cur; cur++) {
|
||||||
|
nr_lines += (*cur == '\n');
|
||||||
|
if (!found && *cur != '\n') {
|
||||||
|
found = true;
|
||||||
|
snprintf(out, CS_SNIP_LINE_SIZE, "%.*s", (int)strcspn(cur, "\n"),
|
||||||
|
cur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nr_lines + (found && *--cur != '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new snip consisting of a hash value and a line of text to the end of
|
||||||
|
* the clip store. The snip file size is grown as necessary to accommodate the
|
||||||
|
* new snip.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @hash: The hash value of the snip to add
|
||||||
|
* @line: The line content of the snip to add
|
||||||
|
* @nr_lines: The number of lines in the line content
|
||||||
|
*/
|
||||||
|
static int _must_use_ _nonnull_ cs_snip_add(struct clip_store *cs,
|
||||||
|
uint64_t hash, const char *line,
|
||||||
|
uint64_t nr_lines) {
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
|
||||||
|
if (guard.status < 0) {
|
||||||
|
return guard.status;
|
||||||
|
}
|
||||||
|
int ret = cs_file_resize(cs, cs->header->nr_snips + 1);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
cs_snip_update(cs->snips + cs->header->nr_snips - 1, hash, line, nr_lines);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add content to the content directory using the hash as the filename.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @hash: The hash of the content to add
|
||||||
|
* @content: The content to add to the file
|
||||||
|
* @dupe_policy: If set to CS_DUPE_KEEP_LAST, will return with -EEXIST when
|
||||||
|
* trying to insert duplicate entry.
|
||||||
|
*/
|
||||||
|
static int _must_use_ _nonnull_
|
||||||
|
cs_content_add(struct clip_store *cs, uint64_t hash, const char *content,
|
||||||
|
enum cs_dupe_policy dupe_policy) {
|
||||||
|
bool dupe = false;
|
||||||
|
size_t content_len = strlen(content);
|
||||||
|
|
||||||
|
char dir_path[CS_HASH_STR_MAX];
|
||||||
|
snprintf(dir_path, sizeof(dir_path), PRI_HASH, hash);
|
||||||
|
|
||||||
|
int ret = mkdirat(cs->content_dir_fd, dir_path, 0700);
|
||||||
|
if (ret < 0) {
|
||||||
|
if (errno != EEXIST || dupe_policy == CS_DUPE_KEEP_LAST) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
dupe = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
char base_file_path[PATH_MAX];
|
||||||
|
snprintf(base_file_path, sizeof(base_file_path), "%s/1", dir_path);
|
||||||
|
|
||||||
|
if (dupe) {
|
||||||
|
// This clip already exists, just create a link for refcounting
|
||||||
|
struct stat st;
|
||||||
|
if (fstatat(cs->content_dir_fd, base_file_path, &st, 0) < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((size_t)st.st_size != content_len) {
|
||||||
|
// Extremely unlikely with FNV-1a 64-bit outside of artificial
|
||||||
|
// scenarios, but never hurts to be careful...
|
||||||
|
return -EFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t link_num = (size_t)st.st_nlink + 1;
|
||||||
|
char linkpath[PATH_MAX];
|
||||||
|
snprintf(linkpath, sizeof(linkpath), "%s/%zu", dir_path, link_num);
|
||||||
|
if (linkat(cs->content_dir_fd, base_file_path, cs->content_dir_fd,
|
||||||
|
linkpath, 0) < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a new clip
|
||||||
|
_drop_(close) int fd = openat(cs->content_dir_fd, base_file_path,
|
||||||
|
O_WRONLY | O_CREAT | O_EXCL, 0600);
|
||||||
|
if (fd < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *cur = content;
|
||||||
|
size_t remaining = content_len;
|
||||||
|
|
||||||
|
while (remaining > 0) {
|
||||||
|
ssize_t written = write(fd, cur, remaining);
|
||||||
|
if (written < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
remaining -= (size_t)written;
|
||||||
|
cur += written;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up the backing mapped data and file descriptor for a `struct
|
||||||
|
* cs_content` object.
|
||||||
|
*
|
||||||
|
* @content: The content to unmap
|
||||||
|
*/
|
||||||
|
static int cs_content_unmap(struct cs_content *content) {
|
||||||
|
if (content && content->data) {
|
||||||
|
close(content->fd);
|
||||||
|
if (munmap(content->data, (size_t)content->size)) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* _drop_ function for cs_content_unmap()
|
||||||
|
*
|
||||||
|
* @content: The content to unmap
|
||||||
|
*/
|
||||||
|
void drop_cs_content_unmap(struct cs_content *content) {
|
||||||
|
int ret = cs_content_unmap(content);
|
||||||
|
expect(ret == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the content associated with a given hash from the content directory
|
||||||
|
* and map it into memory.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @hash: The hash of the content to retrieve
|
||||||
|
* @content: A pointer to a `struct cs_content` to populate. The caller must
|
||||||
|
* call cs_content_unmap() when done to free it
|
||||||
|
*/
|
||||||
|
int cs_content_get(struct clip_store *cs, uint64_t hash,
|
||||||
|
struct cs_content *content) {
|
||||||
|
memset(content, '\0', sizeof(struct cs_content));
|
||||||
|
|
||||||
|
char filename[PATH_MAX];
|
||||||
|
snprintf(filename, sizeof(filename), PRI_HASH "/1", hash);
|
||||||
|
|
||||||
|
_drop_(close) int fd = openat(cs->content_dir_fd, filename, O_RDONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (fstat(fd, &st) < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
char *data = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||||
|
if (data == MAP_FAILED) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
content->data = data;
|
||||||
|
content->fd = fd;
|
||||||
|
content->size = st.st_size;
|
||||||
|
fd = -1; // Disarm the _drop_, this will be handled by cs_content_unmap()
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the entry with the specified hash to the newest slot.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @hash: The hash of the entry to move
|
||||||
|
*/
|
||||||
|
int cs_make_newest(struct clip_store *cs, uint64_t hash) {
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
|
||||||
|
if (guard.status < 0) {
|
||||||
|
return guard.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < (int)cs->local_nr_snips; ++i) {
|
||||||
|
if (cs->snips[i].hash == hash) {
|
||||||
|
struct cs_snip tmp = cs->snips[i];
|
||||||
|
memmove(cs->snips + i, cs->snips + i + 1,
|
||||||
|
(cs->local_nr_snips - (i + 1)) * sizeof(*cs->snips));
|
||||||
|
cs->snips[cs->local_nr_snips - 1] = tmp;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _must_use_ _nonnull_ cs_content_remove(struct clip_store *cs,
|
||||||
|
uint64_t hash);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new content entry to the clip store and content directory.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @content: The content to add
|
||||||
|
* @out_hash: Output for the generated hash, or NULL
|
||||||
|
* @dupe_policy: Policy to use for duplicate entries
|
||||||
|
*/
|
||||||
|
int cs_add(struct clip_store *cs, const char *content, uint64_t *out_hash,
|
||||||
|
enum cs_dupe_policy dupe_policy) {
|
||||||
|
uint64_t hash = fnv1a_64_hash(content);
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
size_t nr_lines = first_line(content, line);
|
||||||
|
|
||||||
|
if (out_hash) {
|
||||||
|
*out_hash = hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
|
||||||
|
if (guard.status < 0) {
|
||||||
|
return guard.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = cs_content_add(cs, hash, content, dupe_policy);
|
||||||
|
if (ret == -EEXIST && dupe_policy == CS_DUPE_KEEP_LAST) {
|
||||||
|
return cs_make_newest(cs, hash);
|
||||||
|
}
|
||||||
|
if (ret) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
ret = cs_snip_add(cs, hash, line, nr_lines);
|
||||||
|
if (ret) {
|
||||||
|
int rm_ret = cs_content_remove(cs, hash);
|
||||||
|
if (rm_ret) {
|
||||||
|
warn("Failed to roll back leaked content for hash " PRI_HASH
|
||||||
|
" after cs_snip_add failed (%s): %s\n",
|
||||||
|
hash, strerror(-ret), strerror(-rm_ret));
|
||||||
|
return rm_ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over the snips in the clip store. The function should be initially
|
||||||
|
* called with *snip set to NULL, which will set *snip to point to the newest
|
||||||
|
* snip (excluding the header). On subsequent calls, *snip is updated to point
|
||||||
|
* to the next snip in the snip file. The iteration stops when there are no
|
||||||
|
* more snips to process, indicated by the function returning false.
|
||||||
|
*
|
||||||
|
* @guard: The guard lock
|
||||||
|
* @snip: Pointer to a pointer to the current snip being iterated over
|
||||||
|
* @direction: Whether to iterate from the oldest to newest or vice versa
|
||||||
|
*/
|
||||||
|
bool cs_snip_iter(struct ref_guard *guard, enum cs_iter_direction direction,
|
||||||
|
struct cs_snip **snip) {
|
||||||
|
if (guard->status < 0 || guard->cs->header->nr_snips == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct cs_snip *oldest = guard->cs->snips;
|
||||||
|
struct cs_snip *newest = oldest + (guard->cs->header->nr_snips - 1);
|
||||||
|
const struct cs_snip *stop =
|
||||||
|
direction == CS_ITER_NEWEST_FIRST ? oldest : newest;
|
||||||
|
|
||||||
|
if (!*snip) {
|
||||||
|
*snip = direction == CS_ITER_NEWEST_FIRST ? newest : oldest;
|
||||||
|
return true;
|
||||||
|
} else if (*snip != stop) {
|
||||||
|
*snip = *snip + (direction == CS_ITER_NEWEST_FIRST ? -1 : 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove content from the content directory using the hash as the filename.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @hash: The hash of the content to remove
|
||||||
|
*/
|
||||||
|
static int _must_use_ _nonnull_ cs_content_remove(struct clip_store *cs,
|
||||||
|
uint64_t hash) {
|
||||||
|
|
||||||
|
char hash_dir_name[CS_HASH_STR_MAX];
|
||||||
|
snprintf(hash_dir_name, sizeof(hash_dir_name), PRI_HASH, hash);
|
||||||
|
|
||||||
|
_drop_(close) int hash_dir_fd =
|
||||||
|
openat(cs->content_dir_fd, hash_dir_name, O_RDONLY);
|
||||||
|
if (hash_dir_fd < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (fstatat(hash_dir_fd, "1", &st, 0) < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
char nlink_path[PATH_MAX];
|
||||||
|
snprintf(nlink_path, sizeof(nlink_path), "%u", (unsigned)st.st_nlink);
|
||||||
|
|
||||||
|
if (unlinkat(hash_dir_fd, nlink_path, 0) < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.st_nlink == 1 &&
|
||||||
|
unlinkat(cs->content_dir_fd, hash_dir_name, AT_REMOVEDIR) < 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compacts the clip store by removing doomed snips, finalising their removal
|
||||||
|
* after being marked in cs_remove().
|
||||||
|
*
|
||||||
|
* @guard: The guard lock
|
||||||
|
*/
|
||||||
|
static size_t _nonnull_ cs_snip_remove_doomed(struct ref_guard *guard) {
|
||||||
|
size_t nr_doomed = 0;
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
|
||||||
|
while (cs_snip_iter(guard, CS_ITER_OLDEST_FIRST, &snip)) {
|
||||||
|
if (snip->doomed) {
|
||||||
|
nr_doomed++;
|
||||||
|
} else if (nr_doomed > 0) {
|
||||||
|
*(snip - nr_doomed) = *snip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nr_doomed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over the specified number of snips in the clip store from newest
|
||||||
|
* to oldest and remove those for which the predicate function returns
|
||||||
|
* CS_ACTION_REMOVE (see `enum cs_remove_action` above).
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @should_remove: Function pointer to the predicate used to decide removal
|
||||||
|
* @private: Pointer to user-defined data passed to the predicate function
|
||||||
|
* @direction: Whether to iterate from the oldest to newest or vice versa
|
||||||
|
*/
|
||||||
|
int cs_remove(struct clip_store *cs, enum cs_iter_direction direction,
|
||||||
|
enum cs_remove_action (*should_remove)(uint64_t, const char *,
|
||||||
|
void *),
|
||||||
|
void *private) {
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
|
||||||
|
if (guard.status < 0) {
|
||||||
|
return guard.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
|
||||||
|
while (cs_snip_iter(&guard, direction, &snip)) {
|
||||||
|
enum cs_remove_action action =
|
||||||
|
should_remove(snip->hash, snip->line, private);
|
||||||
|
|
||||||
|
if (action & CS_ACTION_REMOVE) {
|
||||||
|
found = true;
|
||||||
|
int ret = cs_content_remove(cs, snip->hash);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
snip->doomed = true;
|
||||||
|
}
|
||||||
|
if (action & CS_ACTION_STOP) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t nr_doomed = cs_snip_remove_doomed(&guard);
|
||||||
|
int ret = cs_file_resize(cs, cs->header->nr_snips - nr_doomed);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function for deciding which snips to remove during trim operation.
|
||||||
|
*
|
||||||
|
* @hash: Unused
|
||||||
|
* @line: Unused
|
||||||
|
* @private: The count of remaining entries to trim
|
||||||
|
*/
|
||||||
|
static enum cs_remove_action _must_use_ _nonnull_
|
||||||
|
trim_callback(uint64_t hash, const char *line, void *private) {
|
||||||
|
(void)hash;
|
||||||
|
(void)line;
|
||||||
|
|
||||||
|
size_t *count = private;
|
||||||
|
if (*count == 0) {
|
||||||
|
return CS_ACTION_REMOVE;
|
||||||
|
}
|
||||||
|
(*count)--;
|
||||||
|
return CS_ACTION_KEEP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim the clip store to only retain the specified number of snips.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @direction: Whether to remove the N newest or N oldest
|
||||||
|
* @nr_keep: The number of newest snips to retain
|
||||||
|
*/
|
||||||
|
int cs_trim(struct clip_store *cs, enum cs_iter_direction direction,
|
||||||
|
size_t nr_keep) {
|
||||||
|
if (nr_keep >= cs->header->nr_snips) {
|
||||||
|
return 0; // No action needed if we're keeping everything or more
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = cs_remove(cs, direction, trim_callback, &nr_keep);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the content and snip for an entry in the clip store, identified by
|
||||||
|
* its age.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @age: The age of the snip to replace, with 0 being the newest
|
||||||
|
* @direction: Whether to iterate from the oldest to newest or vice versa
|
||||||
|
* @content: The content to replace this entry with
|
||||||
|
* @out_hash: Output for the generated hash, or NULL
|
||||||
|
*/
|
||||||
|
int cs_replace(struct clip_store *cs, enum cs_iter_direction direction,
|
||||||
|
size_t age, const char *content, uint64_t *out_hash) {
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
|
||||||
|
if (guard.status < 0) {
|
||||||
|
return guard.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (age >= cs->header->nr_snips) {
|
||||||
|
return -ERANGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t idx = direction == CS_ITER_NEWEST_FIRST
|
||||||
|
? cs->header->nr_snips - age - 1
|
||||||
|
: age;
|
||||||
|
struct cs_snip *snip = cs->snips + idx;
|
||||||
|
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
size_t nr_lines = first_line(content, line);
|
||||||
|
uint64_t old_hash = snip->hash;
|
||||||
|
uint64_t hash = fnv1a_64_hash(content);
|
||||||
|
|
||||||
|
int ret = cs_content_add(cs, hash, content, CS_DUPE_KEEP_ALL);
|
||||||
|
if (ret) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
ret = cs_content_remove(cs, old_hash);
|
||||||
|
if (ret) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
cs_snip_update(snip, hash, line, nr_lines);
|
||||||
|
if (out_hash) {
|
||||||
|
*out_hash = hash;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current number of entries in the clip store.
|
||||||
|
*
|
||||||
|
* @cs: The clip store to operate on
|
||||||
|
* @out_len: Output for the length of the clip store
|
||||||
|
*/
|
||||||
|
int cs_len(struct clip_store *cs, size_t *out_len) {
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
|
||||||
|
if (guard.status < 0) {
|
||||||
|
return guard.status;
|
||||||
|
}
|
||||||
|
*out_len = cs->header->nr_snips;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
192
src/store.h
Normal file
192
src/store.h
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
#ifndef CM_STORE_H
|
||||||
|
#define CM_STORE_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
#define CS_SNIP_SIZE 256 /* The size of each struct cs_snip */
|
||||||
|
#define CS_SNIP_ALLOC_BATCH 1024 /* How many snips to allocate when growing */
|
||||||
|
#define CS_HASH_STR_MAX 17 /* String length of 64bit hex + \0 */
|
||||||
|
#define PRI_HASH "%016" PRIX64
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single snip within the clip store.
|
||||||
|
*
|
||||||
|
* @hash: A 64-bit hash value associated with the content entry
|
||||||
|
* @doomed: Used during cs_remove to batch mark entries for removal
|
||||||
|
* @nr_lines: The number of lines in the content entry
|
||||||
|
* @line: A character array containing the first salient line, terminated by a
|
||||||
|
* null byte
|
||||||
|
*/
|
||||||
|
#define CS_SNIP_LINE_SIZE CS_SNIP_SIZE - (sizeof(uint64_t) * 2) - sizeof(bool)
|
||||||
|
struct _packed_ cs_snip {
|
||||||
|
uint64_t hash;
|
||||||
|
bool doomed;
|
||||||
|
uint64_t nr_lines;
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The header of the clip store. Must fit within the footprint of a regular
|
||||||
|
* `cs_snip`.
|
||||||
|
*
|
||||||
|
* @nr_snips: The total number of snips in the clip store, excluding the
|
||||||
|
* header
|
||||||
|
* @nr_snips_alloc: The total number of allocated snips in the clip store
|
||||||
|
* that can be used without _cs_file_resize(), excluding the
|
||||||
|
* header
|
||||||
|
* @_unused_padding: Padding to match the size of cs_snip
|
||||||
|
*/
|
||||||
|
#define CS_HEADER_PADDING_SIZE CS_SNIP_SIZE - (sizeof(uint64_t) * 2)
|
||||||
|
struct _packed_ cs_header {
|
||||||
|
uint64_t nr_snips;
|
||||||
|
uint64_t nr_snips_alloc;
|
||||||
|
char _unused_padding[CS_HEADER_PADDING_SIZE];
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(struct cs_snip) == CS_SNIP_SIZE, "cs_snip wrong size");
|
||||||
|
static_assert(sizeof(struct cs_snip) == sizeof(struct cs_header),
|
||||||
|
"cs_header and cs_snip must be the same size");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main interface to the clip store for the user.
|
||||||
|
*
|
||||||
|
* @snip_fd: The file descriptor for the snip file
|
||||||
|
* @content_dir_fd: The file descriptor for the content directory
|
||||||
|
* @header: Pointer to the header in the mmapped file
|
||||||
|
* @snips: Pointer to the beginning of the clip store snips in the mmapped
|
||||||
|
* file, directly after the header
|
||||||
|
* @ready: Indicates if the clip store is ready for operations
|
||||||
|
* @refcount: The reference count for the fd flock
|
||||||
|
* @local_nr_snips: Our last known header->nr_snips
|
||||||
|
* @local_nr_snips_alloc: Our last known header->nr_snips_alloc
|
||||||
|
* @mapped_file_size: The actual size of our current mmap() in bytes
|
||||||
|
*/
|
||||||
|
struct clip_store {
|
||||||
|
/* FDs */
|
||||||
|
int snip_fd;
|
||||||
|
int content_dir_fd;
|
||||||
|
|
||||||
|
/* Pointers inside mmapped snip file */
|
||||||
|
struct cs_header *header;
|
||||||
|
struct cs_snip *snips;
|
||||||
|
|
||||||
|
/* Synchronisation */
|
||||||
|
size_t refcount;
|
||||||
|
size_t local_nr_snips;
|
||||||
|
size_t local_nr_snips_alloc;
|
||||||
|
size_t mapped_file_size;
|
||||||
|
bool ready;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the lifecycle of a single clip store lock reference.
|
||||||
|
*
|
||||||
|
* @status: The result of trying update our mappings to new header values. If
|
||||||
|
* less than 0, the lock should not be used. Set to -EBADF on unref.
|
||||||
|
* @unref: The function to call to unref. This mostly exists to ensure that
|
||||||
|
* the ref_guard is used and is not optimised by the compiler.
|
||||||
|
* @cs: A pointer to the clip store structure on which the lock is held.
|
||||||
|
*
|
||||||
|
* Usage of this structure involves creating a ref guard instance at the
|
||||||
|
* beginning of a scope where a lock is needed. Functions which need a lock
|
||||||
|
* accept a `struct ref_guard *` as their first argument
|
||||||
|
*/
|
||||||
|
struct ref_guard {
|
||||||
|
int status;
|
||||||
|
struct clip_store *cs;
|
||||||
|
void (*unref)(struct clip_store *);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The memory-mapped content associated with a hash in the content directory.
|
||||||
|
*
|
||||||
|
* @data: A pointer to the memory-mapped data
|
||||||
|
* @fd: The file descriptor of the opened file from which the content is mapped
|
||||||
|
* @size: The size of the mapped data
|
||||||
|
*/
|
||||||
|
struct cs_content {
|
||||||
|
char *data;
|
||||||
|
int fd;
|
||||||
|
off_t size;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The direction in which to iterate over snips in the clip store.
|
||||||
|
*
|
||||||
|
* @CS_ITER_NEWEST_FIRST: Iterate over the snips starting from the newest.
|
||||||
|
* @CS_ITER_OLDEST_FIRST: Iterate over the snips starting from the oldest.
|
||||||
|
*/
|
||||||
|
enum cs_iter_direction { CS_ITER_NEWEST_FIRST, CS_ITER_OLDEST_FIRST };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the bit at position n.
|
||||||
|
*
|
||||||
|
* @n: The position of the bit to set
|
||||||
|
*/
|
||||||
|
#define BIT(n) (1UL << (n))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The action to take as returned by `cs_remove`'s `should_remove()` predicate
|
||||||
|
* function.
|
||||||
|
*
|
||||||
|
* @CS_ACTION_REMOVE: Remove this snip from the clip store
|
||||||
|
* @CS_ACTION_KEEP: Keep this snip in the clip store
|
||||||
|
* @CS_ACTION_STOP: Stop iteration after processing this snip
|
||||||
|
*
|
||||||
|
* If neither of CS_ACTION_REMOVE or CS_ACTION_KEEP are specified, the snip is
|
||||||
|
* kept.
|
||||||
|
*/
|
||||||
|
enum cs_remove_action {
|
||||||
|
CS_ACTION_REMOVE = BIT(0),
|
||||||
|
CS_ACTION_KEEP = BIT(1),
|
||||||
|
CS_ACTION_STOP = BIT(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What to do when there's a duplicate entry.
|
||||||
|
*
|
||||||
|
* @CS_DUPE_KEEP_ALL: Keep all duplicate entries.
|
||||||
|
* @CS_DUPE_KEEP_LAST: Only keep the newest, do not insert duplicate entries.
|
||||||
|
*/
|
||||||
|
enum cs_dupe_policy {
|
||||||
|
CS_DUPE_KEEP_ALL,
|
||||||
|
CS_DUPE_KEEP_LAST,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ref_guard _must_use_ _nonnull_ cs_ref(struct clip_store *cs);
|
||||||
|
void _nonnull_ cs_unref(struct clip_store *cs);
|
||||||
|
void _nonnull_ drop_cs_unref(struct ref_guard *guard);
|
||||||
|
int _must_use_ _nonnull_ cs_destroy(struct clip_store *cs);
|
||||||
|
int _must_use_ _nonnull_ cs_init(struct clip_store *cs, int snip_fd,
|
||||||
|
int content_dir_fd);
|
||||||
|
void drop_cs_content_unmap(struct cs_content *content);
|
||||||
|
void drop_cs_destroy(struct clip_store *cs);
|
||||||
|
int _must_use_ _nonnull_ cs_content_get(struct clip_store *cs, uint64_t hash,
|
||||||
|
struct cs_content *content);
|
||||||
|
int _must_use_ _nonnull_n_(1)
|
||||||
|
cs_make_newest(struct clip_store *cs, uint64_t hash);
|
||||||
|
int _must_use_ _nonnull_n_(1)
|
||||||
|
cs_add(struct clip_store *cs, const char *content, uint64_t *out_hash,
|
||||||
|
enum cs_dupe_policy dupe_policy);
|
||||||
|
bool _must_use_ _nonnull_ cs_snip_iter(struct ref_guard *guard,
|
||||||
|
enum cs_iter_direction direction,
|
||||||
|
struct cs_snip **snip);
|
||||||
|
int _must_use_ _nonnull_ cs_remove(
|
||||||
|
struct clip_store *cs, enum cs_iter_direction direction,
|
||||||
|
enum cs_remove_action (*should_remove)(uint64_t, const char *, void *),
|
||||||
|
void *private);
|
||||||
|
int _must_use_ _nonnull_ cs_trim(struct clip_store *cs,
|
||||||
|
enum cs_iter_direction direction,
|
||||||
|
size_t nr_keep);
|
||||||
|
int _must_use_ _nonnull_n_(1, 4)
|
||||||
|
cs_replace(struct clip_store *cs, enum cs_iter_direction direction,
|
||||||
|
size_t age, const char *content, uint64_t *out_hash);
|
||||||
|
int _nonnull_ cs_len(struct clip_store *cs, size_t *out_len);
|
||||||
|
|
||||||
|
size_t _nonnull_ first_line(const char *text, char *out);
|
||||||
|
|
||||||
|
#endif
|
||||||
165
src/util.c
Normal file
165
src/util.c
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "store.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write data to a file descriptor, ensuring all bytes are written.
|
||||||
|
*/
|
||||||
|
void write_safe(int fd, const char *buf, size_t count) {
|
||||||
|
while (count > 0) {
|
||||||
|
ssize_t chunk_size = write(fd, buf, count);
|
||||||
|
expect(chunk_size >= 0);
|
||||||
|
buf += chunk_size;
|
||||||
|
expect(chunk_size <= (ssize_t)count);
|
||||||
|
count -= (size_t)chunk_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read data from a file descriptor into a buffer safely, ensuring correct
|
||||||
|
* handling of partial reads.
|
||||||
|
*/
|
||||||
|
size_t read_safe(int fd, char *buf, size_t count) {
|
||||||
|
size_t count_start = count;
|
||||||
|
while (count > 0) {
|
||||||
|
ssize_t chunk_size = read(fd, buf, count);
|
||||||
|
expect(chunk_size >= 0);
|
||||||
|
if (chunk_size == 0) { // EOF
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buf += chunk_size;
|
||||||
|
count -= (size_t)chunk_size;
|
||||||
|
}
|
||||||
|
expect(count_start >= count);
|
||||||
|
return count_start - count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs safe, bounded string formatting into a buffer. On error or
|
||||||
|
* truncation, expect() aborts.
|
||||||
|
*/
|
||||||
|
size_t snprintf_safe(char *buf, size_t len, const char *fmt, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
int needed = vsnprintf(buf, len, fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
expect(needed >= 0 && (size_t)needed < len);
|
||||||
|
return (size_t)needed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs clipserve to handle selection requests for a hash in the clip store.
|
||||||
|
*/
|
||||||
|
#define CLIPSERVE_MAX_ARGS (CM_SEL_MAX * 2 + 3)
|
||||||
|
|
||||||
|
void run_clipserve(uint64_t hash, const struct selection *selections) {
|
||||||
|
char hash_str[CS_HASH_STR_MAX];
|
||||||
|
snprintf(hash_str, sizeof(hash_str), PRI_HASH, hash);
|
||||||
|
|
||||||
|
const char *cmd[CLIPSERVE_MAX_ARGS];
|
||||||
|
size_t cmd_idx = 0;
|
||||||
|
cmd[cmd_idx++] = "clipserve";
|
||||||
|
if (selections) {
|
||||||
|
for (size_t i = 0; i < CM_SEL_MAX; i++) {
|
||||||
|
if (!selections[i].active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expect(cmd_idx + 2 < CLIPSERVE_MAX_ARGS);
|
||||||
|
cmd[cmd_idx++] = "--selection";
|
||||||
|
cmd[cmd_idx++] = selections[i].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd[cmd_idx++] = hash_str;
|
||||||
|
cmd[cmd_idx++] = NULL;
|
||||||
|
pid_t pid = fork();
|
||||||
|
expect(pid >= 0);
|
||||||
|
|
||||||
|
if (pid > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
execvp(cmd[0], (char *const *)cmd);
|
||||||
|
die("Failed to exec %s: %s\n", cmd[0], strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a positive errno value to a negative error code, ensuring a
|
||||||
|
* non-zero value is returned.
|
||||||
|
*
|
||||||
|
* This is needed because clang-tidy and gcc may complain when doing plain
|
||||||
|
* "return -errno" because the compiler does not know that errno cannot be 0
|
||||||
|
* (and thus that later checks with func() == 0 cannot pass in error
|
||||||
|
* situations).
|
||||||
|
*/
|
||||||
|
int negative_errno(void) { return errno > 0 ? -errno : -EINVAL; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a string to an unsigned 64-bit integer in given base, validating the
|
||||||
|
* format and range of the input.
|
||||||
|
*/
|
||||||
|
static int str_to_uint64_base(const char *input, uint64_t *output, int base) {
|
||||||
|
char *endptr;
|
||||||
|
errno = 0;
|
||||||
|
|
||||||
|
uint64_t val = strtoull(input, &endptr, base);
|
||||||
|
if (errno > 0) {
|
||||||
|
return negative_errno();
|
||||||
|
}
|
||||||
|
if (!endptr || endptr == input || *endptr != 0) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
if (val != 0 && input[0] == '-') {
|
||||||
|
return -ERANGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
*output = val;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a string to an unsigned 64-bit integer, validating the format and
|
||||||
|
* range of the input.
|
||||||
|
*/
|
||||||
|
int str_to_uint64(const char *input, uint64_t *output) {
|
||||||
|
return str_to_uint64_base(input, output, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a hex string to an unsigned 64-bit integer, validating the format
|
||||||
|
* and range of the input.
|
||||||
|
*/
|
||||||
|
int str_to_hex64(const char *input, uint64_t *output) {
|
||||||
|
return str_to_uint64_base(input, output, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether debug mode is enabled and cache the result. Used for dbg().
|
||||||
|
*/
|
||||||
|
bool debug_mode_enabled(void) {
|
||||||
|
static int debug_enabled = -1;
|
||||||
|
if (debug_enabled == -1) {
|
||||||
|
const char *dbg_env = getenv("CM_DEBUG");
|
||||||
|
debug_enabled = dbg_env && streq(dbg_env, "1");
|
||||||
|
}
|
||||||
|
return debug_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a 64-bit FNV-1a hash for a NULL-terminated string.
|
||||||
|
*/
|
||||||
|
uint64_t fnv1a_64_hash(const char *buf) {
|
||||||
|
const uint64_t fnv_offset_basis = 0xcbf29ce484222325ULL;
|
||||||
|
const uint64_t fnv_prime = 0x100000001b3ULL;
|
||||||
|
uint64_t hash = fnv_offset_basis;
|
||||||
|
const uint8_t *src = (const uint8_t *)buf;
|
||||||
|
while (*src) {
|
||||||
|
hash ^= *src++;
|
||||||
|
hash *= fnv_prime;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
111
src/util.h
Normal file
111
src/util.h
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
#ifndef CM_UTIL_H
|
||||||
|
#define CM_UTIL_H
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
struct selection;
|
||||||
|
|
||||||
|
#define _drop_(x) __attribute__((__cleanup__(drop_##x)))
|
||||||
|
#define _must_use_ __attribute__((warn_unused_result))
|
||||||
|
#define _nonnull_ __attribute__((nonnull))
|
||||||
|
#define _nonnull_n_(...) __attribute__((nonnull(__VA_ARGS__)))
|
||||||
|
#define _noreturn_ __attribute__((noreturn))
|
||||||
|
#define _packed_ __attribute__((packed))
|
||||||
|
#define _printf_(a, b) __attribute__((__format__(printf, a, b)))
|
||||||
|
#define _unused_ __attribute__((__unused__))
|
||||||
|
#define likely(x) __builtin_expect(!!(x), 1)
|
||||||
|
#define unlikely(x) __builtin_expect(!!(x), 0)
|
||||||
|
#ifndef __cplusplus
|
||||||
|
#define static_assert _Static_assert
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define UINT64_MAX_STRLEN 20 // (1 << 64) - 1 is 20 digits wide
|
||||||
|
|
||||||
|
#define streq(a, b) (strcmp((a), (b)) == 0)
|
||||||
|
#define strceq(a, b) (strcasecmp((a), (b)) == 0)
|
||||||
|
#define strnull(s) (s) ? (s) : "[null]"
|
||||||
|
|
||||||
|
#define arrlen(x) \
|
||||||
|
(__builtin_choose_expr( \
|
||||||
|
!__builtin_types_compatible_p(typeof(x), typeof(&*(x))), \
|
||||||
|
sizeof(x) / sizeof((x)[0]), (void)0 /* decayed, compile error */))
|
||||||
|
|
||||||
|
#define _msg(level, fmt, ...) fprintf(stderr, level ": " fmt, ##__VA_ARGS__)
|
||||||
|
#define warn(fmt, ...) _msg("WARN", fmt, ##__VA_ARGS__)
|
||||||
|
#define _die(dump, fmt, ...) \
|
||||||
|
do { \
|
||||||
|
_msg("FATAL", fmt, ##__VA_ARGS__); \
|
||||||
|
if (dump) { \
|
||||||
|
abort(); \
|
||||||
|
} \
|
||||||
|
exit(1); \
|
||||||
|
} while (0)
|
||||||
|
#define die(fmt, ...) _die(0, fmt, ##__VA_ARGS__)
|
||||||
|
#define die_on(cond, fmt, ...) \
|
||||||
|
do { \
|
||||||
|
if (unlikely(cond)) { \
|
||||||
|
die(fmt, ##__VA_ARGS__); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
#define expect(x) \
|
||||||
|
do { \
|
||||||
|
if (!likely(x)) { \
|
||||||
|
_die(1, "!(%s) at %s:%s:%d\n", #x, __FILE__, __func__, __LINE__); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define dbg(fmt, ...) \
|
||||||
|
do { \
|
||||||
|
if (debug_mode_enabled()) { \
|
||||||
|
fprintf(stderr, "%s:%ld:%s:%s:%d: " fmt, prog_name, \
|
||||||
|
(long)getpid(), __FILE__, __func__, __LINE__, \
|
||||||
|
##__VA_ARGS__); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
void _nonnull_ write_safe(int fd, const char *buf, size_t count);
|
||||||
|
size_t _nonnull_ read_safe(int fd, char *buf, size_t count);
|
||||||
|
size_t _printf_(3, 4)
|
||||||
|
snprintf_safe(char *buf, size_t len, const char *fmt, ...);
|
||||||
|
|
||||||
|
void run_clipserve(uint64_t hash, const struct selection *selections);
|
||||||
|
uint64_t _nonnull_ fnv1a_64_hash(const char *buf);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __attribute__((cleanup)) functions
|
||||||
|
*/
|
||||||
|
#define DEFINE_DROP_FUNC_PTR(type, func) \
|
||||||
|
static inline void drop_##func(type *p) { func(p); }
|
||||||
|
#define DEFINE_DROP_FUNC(type, func) \
|
||||||
|
static inline void drop_##func(type *p) { \
|
||||||
|
if (*p) \
|
||||||
|
func(*p); \
|
||||||
|
}
|
||||||
|
#define DEFINE_DROP_FUNC_VOID(func) \
|
||||||
|
static inline void drop_##func(void *p) { \
|
||||||
|
void **pp = p; \
|
||||||
|
if (*pp) \
|
||||||
|
func(*pp); \
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void drop_close(int *fd) {
|
||||||
|
if (*fd >= 0) {
|
||||||
|
close(*fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_DROP_FUNC_VOID(free)
|
||||||
|
DEFINE_DROP_FUNC(FILE *, fclose)
|
||||||
|
DEFINE_DROP_FUNC(DIR *, closedir)
|
||||||
|
|
||||||
|
int _must_use_ negative_errno(void);
|
||||||
|
int _nonnull_ str_to_uint64(const char *input, uint64_t *output);
|
||||||
|
int _nonnull_ str_to_hex64(const char *input, uint64_t *output);
|
||||||
|
bool debug_mode_enabled(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
121
src/x.c
Normal file
121
src/x.c
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/Xproto.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "x.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the title of the window with the specified window ID.
|
||||||
|
*/
|
||||||
|
char *get_window_title(Display *dpy, Window owner) {
|
||||||
|
Atom props[] = {XInternAtom(dpy, "_NET_WM_NAME", False), XA_WM_NAME};
|
||||||
|
Atom utf8_string = XInternAtom(dpy, "UTF8_STRING", False);
|
||||||
|
Atom actual_type;
|
||||||
|
int format;
|
||||||
|
unsigned long nr_items, bytes_after;
|
||||||
|
unsigned char *prop = NULL;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < arrlen(props); i++) {
|
||||||
|
if (XGetWindowProperty(dpy, owner, props[i], 0, (~0L), False,
|
||||||
|
(props[i] == XA_WM_NAME) ? AnyPropertyType
|
||||||
|
: utf8_string,
|
||||||
|
&actual_type, &format, &nr_items, &bytes_after,
|
||||||
|
&prop) == Success &&
|
||||||
|
prop) {
|
||||||
|
return (char *)prop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certain X11 operations may fail in expected ways. For example, when
|
||||||
|
* attempting to interact with a window that has been closed. This handler
|
||||||
|
* avoids the application terminating in such cases.
|
||||||
|
*
|
||||||
|
* The cppcheck suppression is for a false positive: this is a callback and
|
||||||
|
* cannot be changed.
|
||||||
|
*/
|
||||||
|
// cppcheck-suppress [constParameterPointer,unmatchedSuppression]
|
||||||
|
int xerror_handler(Display *dpy _unused_, XErrorEvent *ee) {
|
||||||
|
if (ee->error_code == BadWindow ||
|
||||||
|
(ee->request_code == X_SetInputFocus && ee->error_code == BadMatch) ||
|
||||||
|
(ee->request_code == X_PolyText8 && ee->error_code == BadDrawable) ||
|
||||||
|
(ee->request_code == X_PolyFillRectangle &&
|
||||||
|
ee->error_code == BadDrawable) ||
|
||||||
|
(ee->request_code == X_PolySegment && ee->error_code == BadDrawable) ||
|
||||||
|
(ee->request_code == X_ConfigureWindow && ee->error_code == BadMatch) ||
|
||||||
|
(ee->request_code == X_GrabButton && ee->error_code == BadAccess) ||
|
||||||
|
(ee->request_code == X_GrabKey && ee->error_code == BadAccess) ||
|
||||||
|
(ee->request_code == X_CopyArea && ee->error_code == BadDrawable)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
die("X error with request code=%d, error code=%d\n", ee->request_code,
|
||||||
|
ee->error_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define FALLBACK_CHUNK_BYTES 4 * 1024
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a quarter of the maximum request size in bytes.
|
||||||
|
*
|
||||||
|
* XMaxRequestSize and XExtendedMaxRequestSize return units of 4-byte words, so
|
||||||
|
* the word count already equals 1/4 of the maximum request size in bytes.
|
||||||
|
*/
|
||||||
|
size_t get_incr_threshold(Display *dpy) {
|
||||||
|
size_t max_request_words = XExtendedMaxRequestSize(dpy);
|
||||||
|
if (max_request_words == 0) {
|
||||||
|
max_request_words = XMaxRequestSize(dpy);
|
||||||
|
}
|
||||||
|
return max_request_words ? max_request_words : FALLBACK_CHUNK_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate an appropriate INCR payload size.
|
||||||
|
*
|
||||||
|
* The request size word count is a sensible threshold for deciding when to use
|
||||||
|
* INCR at all, but using that same value as the payload of each
|
||||||
|
* XChangeProperty request is too aggressive in practice.
|
||||||
|
*
|
||||||
|
* This is an interoperability constraint, not a protocol-limit one, because:
|
||||||
|
*
|
||||||
|
* - XMaxRequestSize/XExtendedMaxRequestSize only tell us what request size the
|
||||||
|
* X server will accept.
|
||||||
|
* - INCR has no chunk-size negotiation, so if a requestor dislikes large
|
||||||
|
* payloads it can simply stop deleting the property, leaving the sender
|
||||||
|
* waiting forever.
|
||||||
|
*
|
||||||
|
* Other implementations do not agree on a single rule here either: xclip uses
|
||||||
|
* a heuristic, some clipboard managers use a fixed cap, and Emacs couples
|
||||||
|
* large chunks with a timeout.
|
||||||
|
*/
|
||||||
|
size_t get_chunk_size(Display *dpy) {
|
||||||
|
size_t incr_threshold = get_incr_threshold(dpy);
|
||||||
|
return incr_threshold / 4 ? incr_threshold / 4 : FALLBACK_CHUNK_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new INCR transfer to the active list.
|
||||||
|
*/
|
||||||
|
void it_add(struct incr_transfer **it_list, struct incr_transfer *it) {
|
||||||
|
if (*it_list) {
|
||||||
|
(*it_list)->prev = it;
|
||||||
|
}
|
||||||
|
it->next = *it_list;
|
||||||
|
it->prev = NULL;
|
||||||
|
*it_list = it;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Remove an INCR transfer from the active list.
|
||||||
|
*/
|
||||||
|
void it_remove(struct incr_transfer **it_list, struct incr_transfer *it) {
|
||||||
|
if (it->prev) {
|
||||||
|
it->prev->next = it->next;
|
||||||
|
}
|
||||||
|
if (it->next) {
|
||||||
|
it->next->prev = it->prev;
|
||||||
|
}
|
||||||
|
if (*it_list == it) {
|
||||||
|
*it_list = it->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/x.h
Normal file
34
src/x.h
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#ifndef CM_X_H
|
||||||
|
#define CM_X_H
|
||||||
|
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
DEFINE_DROP_FUNC_VOID(XFree)
|
||||||
|
|
||||||
|
size_t _nonnull_ get_incr_threshold(Display *dpy);
|
||||||
|
size_t _nonnull_ get_chunk_size(Display *dpy);
|
||||||
|
char _nonnull_ *get_window_title(Display *dpy, Window owner);
|
||||||
|
int xerror_handler(Display *dpy _unused_, XErrorEvent *ee);
|
||||||
|
|
||||||
|
struct incr_transfer {
|
||||||
|
struct incr_transfer *next;
|
||||||
|
struct incr_transfer *prev;
|
||||||
|
Window requestor;
|
||||||
|
Atom property;
|
||||||
|
Atom target;
|
||||||
|
int format;
|
||||||
|
char *data;
|
||||||
|
size_t data_size;
|
||||||
|
size_t data_capacity;
|
||||||
|
size_t offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define it_dbg(it, fmt, ...) \
|
||||||
|
dbg("[incr 0x%lx] " fmt, (unsigned long)(it)->requestor, ##__VA_ARGS__)
|
||||||
|
void _nonnull_ it_add(struct incr_transfer **it_list, struct incr_transfer *it);
|
||||||
|
void _nonnull_ it_remove(struct incr_transfer **it_list,
|
||||||
|
struct incr_transfer *it);
|
||||||
|
|
||||||
|
#endif
|
||||||
848
tests/test_store.c
Normal file
848
tests/test_store.c
Normal file
|
|
@ -0,0 +1,848 @@
|
||||||
|
#undef NDEBUG
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "../src/store.h"
|
||||||
|
#include "../src/util.h"
|
||||||
|
|
||||||
|
#define COL_NORMAL "\x1B[0m"
|
||||||
|
#define COL_GREEN "\x1B[32m"
|
||||||
|
#define COL_RED "\x1B[31m"
|
||||||
|
|
||||||
|
#define t_assert(test) \
|
||||||
|
do { \
|
||||||
|
if (!(test)) { \
|
||||||
|
printf(" %s[FAIL:%d] %s%s\n", COL_RED, __LINE__, #test, \
|
||||||
|
COL_NORMAL); \
|
||||||
|
return false; \
|
||||||
|
} \
|
||||||
|
printf(" %s[PASS:%d] %s%s\n", COL_GREEN, __LINE__, #test, \
|
||||||
|
COL_NORMAL); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define t_run(test) \
|
||||||
|
do { \
|
||||||
|
printf("%s:\n", #test); \
|
||||||
|
bool ret = test(); \
|
||||||
|
printf("\n"); \
|
||||||
|
if (!ret) { \
|
||||||
|
return ret; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define TEST_SNIP_FILE "/clip_store_snip_test"
|
||||||
|
#define TEST_CONTENT_DIR "/dev/shm/clip_store_content_dir_test"
|
||||||
|
|
||||||
|
static int create_test_snip_fd(void) {
|
||||||
|
shm_unlink(TEST_SNIP_FILE);
|
||||||
|
int snip_fd = shm_open(TEST_SNIP_FILE, O_RDWR | O_CREAT | O_EXCL, 0600);
|
||||||
|
assert(snip_fd >= 0);
|
||||||
|
return snip_fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drop_remove_test_snip_fd(int *snip_fd) {
|
||||||
|
close(*snip_fd);
|
||||||
|
int ret = shm_unlink(TEST_SNIP_FILE);
|
||||||
|
assert(ret == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _drop_remove_test_content_dir_fd(int *dir_fd_ptr) {
|
||||||
|
int dir_fd = *dir_fd_ptr;
|
||||||
|
DIR *dir = fdopendir(dir_fd);
|
||||||
|
if (dir == NULL) {
|
||||||
|
close(dir_fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct dirent *entry;
|
||||||
|
while ((entry = readdir(dir)) != NULL) {
|
||||||
|
if (streq(entry->d_name, ".") || streq(entry->d_name, "..")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry->d_type == DT_REG) {
|
||||||
|
int ret = unlinkat(dir_fd, entry->d_name, 0);
|
||||||
|
assert(ret == 0);
|
||||||
|
} else if (entry->d_type == DT_DIR) {
|
||||||
|
int subdir_fd =
|
||||||
|
openat(dir_fd, entry->d_name, O_RDONLY | O_DIRECTORY);
|
||||||
|
assert(subdir_fd >= 0);
|
||||||
|
_drop_remove_test_content_dir_fd(&subdir_fd);
|
||||||
|
int ret = unlinkat(dir_fd, entry->d_name, AT_REMOVEDIR);
|
||||||
|
assert(ret == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drop_remove_test_content_dir_fd(int *dir_fd_ptr) {
|
||||||
|
_drop_remove_test_content_dir_fd(dir_fd_ptr);
|
||||||
|
int ret = rmdir(TEST_CONTENT_DIR);
|
||||||
|
assert(ret == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove_test_content_dir(const char *path) {
|
||||||
|
int dir_fd = open(path, O_RDONLY);
|
||||||
|
if (dir_fd < 0) {
|
||||||
|
assert(errno == ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop_remove_test_content_dir_fd(&dir_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int create_test_content_dir_fd(void) {
|
||||||
|
remove_test_content_dir(TEST_CONTENT_DIR);
|
||||||
|
int ret = mkdir(TEST_CONTENT_DIR, 0700);
|
||||||
|
assert(ret == 0);
|
||||||
|
int dir_fd = open(TEST_CONTENT_DIR, O_RDONLY);
|
||||||
|
assert(dir_fd >= 0);
|
||||||
|
return dir_fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int count_test_snip_maps(void) {
|
||||||
|
#ifdef __linux__
|
||||||
|
_drop_(fclose) FILE *maps = fopen("/proc/self/maps", "r");
|
||||||
|
assert(maps != NULL);
|
||||||
|
|
||||||
|
char line[4096];
|
||||||
|
int count = 0;
|
||||||
|
while (fgets(line, sizeof(line), maps) != NULL) {
|
||||||
|
if (strstr(line, TEST_SNIP_FILE) != NULL) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
#else
|
||||||
|
return -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test callables */
|
||||||
|
static enum cs_remove_action remove_if_five(uint64_t hash, const char *line,
|
||||||
|
void *private) {
|
||||||
|
(void)hash;
|
||||||
|
size_t *count = private;
|
||||||
|
enum cs_remove_action ret = 0;
|
||||||
|
|
||||||
|
if (*count == 0) {
|
||||||
|
ret |= CS_ACTION_STOP;
|
||||||
|
}
|
||||||
|
(*count)--;
|
||||||
|
|
||||||
|
if (line[0] == '5') {
|
||||||
|
ret |= CS_ACTION_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct clip_store setup_test(void) {
|
||||||
|
struct clip_store cs;
|
||||||
|
int snip_fd = create_test_snip_fd();
|
||||||
|
int content_dir_fd = create_test_content_dir_fd();
|
||||||
|
int ret = cs_init(&cs, snip_fd, content_dir_fd);
|
||||||
|
assert(ret == 0);
|
||||||
|
return cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drop_teardown_test(struct clip_store *cs) {
|
||||||
|
int snip_fd = cs->snip_fd;
|
||||||
|
int content_dir_fd = cs->content_dir_fd;
|
||||||
|
int ret = cs_destroy(cs);
|
||||||
|
assert(ret == 0);
|
||||||
|
close(snip_fd);
|
||||||
|
shm_unlink(TEST_SNIP_FILE);
|
||||||
|
close(content_dir_fd);
|
||||||
|
remove_test_content_dir(TEST_CONTENT_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void add_ten_snips(struct clip_store *cs) {
|
||||||
|
for (char i = 0; i < 10; i++) {
|
||||||
|
char num[8];
|
||||||
|
snprintf(num, sizeof(num), "%d", i);
|
||||||
|
int ret = cs_add(cs, num, NULL, CS_DUPE_KEEP_ALL);
|
||||||
|
assert(ret == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tests */
|
||||||
|
|
||||||
|
static bool test__cs_init(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
t_assert(cs.snip_fd >= 0);
|
||||||
|
t_assert(cs.content_dir_fd >= 0);
|
||||||
|
|
||||||
|
/* Check header fields were set up and are correct */
|
||||||
|
struct stat st;
|
||||||
|
t_assert(fstat(cs.snip_fd, &st) == 0);
|
||||||
|
t_assert(st.st_size == CS_SNIP_SIZE);
|
||||||
|
t_assert(cs.header->nr_snips_alloc == 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_init__bad_size(void) {
|
||||||
|
_drop_(remove_test_snip_fd) int snip_fd = create_test_snip_fd();
|
||||||
|
_drop_(remove_test_content_dir_fd) int content_dir_fd =
|
||||||
|
create_test_content_dir_fd();
|
||||||
|
t_assert(ftruncate(snip_fd, CS_SNIP_SIZE - 1) == 0);
|
||||||
|
struct clip_store cs;
|
||||||
|
t_assert(cs_init(&cs, snip_fd, content_dir_fd) == -EINVAL);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_init__bad_size_aligned(void) {
|
||||||
|
_drop_(remove_test_snip_fd) int snip_fd = create_test_snip_fd();
|
||||||
|
_drop_(remove_test_content_dir_fd) int content_dir_fd =
|
||||||
|
create_test_content_dir_fd();
|
||||||
|
t_assert(ftruncate(snip_fd, CS_SNIP_SIZE * 2) == 0);
|
||||||
|
struct clip_store cs;
|
||||||
|
t_assert(cs_init(&cs, snip_fd, content_dir_fd) == -EINVAL);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_add(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
for (char i = 0; i < 10; i++) {
|
||||||
|
char num[8];
|
||||||
|
snprintf(num, sizeof(num), "%d", i);
|
||||||
|
|
||||||
|
uint64_t hash;
|
||||||
|
int ret = cs_add(&cs, num, &hash, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
_drop_(cs_content_unmap) struct cs_content content;
|
||||||
|
ret = cs_content_get(&cs, hash, &content);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(strncmp(content.data, num, content.size) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
t_assert(cs.header->nr_snips == 10);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_content_get__keeps_fd_open(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
uint64_t hash;
|
||||||
|
int ret = cs_add(&cs, "fd lifecycle", &hash, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
struct cs_content content;
|
||||||
|
ret = cs_content_get(&cs, hash, &content);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(fcntl(content.fd, F_GETFD) == 0);
|
||||||
|
|
||||||
|
int unrelated_fd = open("/dev/null", O_RDONLY);
|
||||||
|
t_assert(unrelated_fd >= 0);
|
||||||
|
|
||||||
|
drop_cs_content_unmap(&content);
|
||||||
|
|
||||||
|
t_assert(fcntl(unrelated_fd, F_GETFD) == 0);
|
||||||
|
close(unrelated_fd);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_snip_iter(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(&cs);
|
||||||
|
|
||||||
|
add_ten_snips(&cs);
|
||||||
|
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
int last_num = 9;
|
||||||
|
while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) {
|
||||||
|
_drop_(cs_content_unmap) struct cs_content content;
|
||||||
|
int ret = cs_content_get(&cs, snip->hash, &content);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
/* add_ten_snips adds from 0-9, we should get the last one first */
|
||||||
|
int num = content.data[0] - '0';
|
||||||
|
t_assert(num == last_num--);
|
||||||
|
}
|
||||||
|
|
||||||
|
snip = NULL;
|
||||||
|
last_num = 0;
|
||||||
|
while (cs_snip_iter(&guard, CS_ITER_OLDEST_FIRST, &snip)) {
|
||||||
|
_drop_(cs_content_unmap) struct cs_content content;
|
||||||
|
int ret = cs_content_get(&cs, snip->hash, &content);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
int num = content.data[0] - '0';
|
||||||
|
t_assert(num == last_num++);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_remove(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(&cs);
|
||||||
|
|
||||||
|
add_ten_snips(&cs);
|
||||||
|
|
||||||
|
size_t nr_iter = 1;
|
||||||
|
t_assert(cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_five, &nr_iter) ==
|
||||||
|
0);
|
||||||
|
t_assert(cs.header->nr_snips == 10);
|
||||||
|
nr_iter = SIZE_MAX;
|
||||||
|
t_assert(cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_five, &nr_iter) ==
|
||||||
|
0);
|
||||||
|
t_assert(cs.header->nr_snips == 9);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_trim(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(&cs);
|
||||||
|
|
||||||
|
add_ten_snips(&cs);
|
||||||
|
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip))
|
||||||
|
;
|
||||||
|
uint64_t oldest_hash = snip->hash;
|
||||||
|
|
||||||
|
t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 3) == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 3);
|
||||||
|
|
||||||
|
/* Check we kept the most recently added ones */
|
||||||
|
int last_num = 9;
|
||||||
|
snip = NULL;
|
||||||
|
while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) {
|
||||||
|
_drop_(cs_content_unmap) struct cs_content content;
|
||||||
|
int ret = cs_content_get(&cs, snip->hash, &content);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
int num = content.data[0] - '0';
|
||||||
|
t_assert(num == last_num--);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check the oldest snip hash is gone */
|
||||||
|
_drop_(cs_content_unmap) struct cs_content content;
|
||||||
|
t_assert(cs_content_get(&cs, oldest_hash, &content) == -ENOENT);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_replace(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(&cs);
|
||||||
|
|
||||||
|
add_ten_snips(&cs);
|
||||||
|
|
||||||
|
const char *new = "new";
|
||||||
|
|
||||||
|
int ret = cs_replace(&cs, CS_ITER_NEWEST_FIRST, 1, new, NULL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
bool i_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip);
|
||||||
|
t_assert(i_ret == true);
|
||||||
|
t_assert(streq(snip->line, "9"));
|
||||||
|
i_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip);
|
||||||
|
t_assert(i_ret == true);
|
||||||
|
t_assert(streq(snip->line, new));
|
||||||
|
|
||||||
|
_drop_(cs_content_unmap) struct cs_content first_content;
|
||||||
|
ret = cs_content_get(&cs, snip->hash, &first_content);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(strncmp(first_content.data, new, first_content.size) == 0);
|
||||||
|
|
||||||
|
/* No other clips should be affected */
|
||||||
|
int last_num = 7;
|
||||||
|
while (cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip)) {
|
||||||
|
_drop_(cs_content_unmap) struct cs_content content;
|
||||||
|
ret = cs_content_get(&cs, snip->hash, &content);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
int num = content.data[0] - '0';
|
||||||
|
t_assert(num == last_num--);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__reuse_cs(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
add_ten_snips(&cs);
|
||||||
|
t_assert(cs_destroy(&cs) == 0);
|
||||||
|
|
||||||
|
t_assert(cs_init(&cs, cs.snip_fd, cs.content_dir_fd) == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 10);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_add__exceeds_snip_line_size(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
/* Construct a string that exceeds CS_SNIP_LINE_SIZE */
|
||||||
|
char long_content[CS_SNIP_LINE_SIZE + 100];
|
||||||
|
memset(long_content, 'A', sizeof(long_content));
|
||||||
|
long_content[sizeof(long_content) - 1] = '\0';
|
||||||
|
|
||||||
|
int ret = cs_add(&cs, long_content, NULL, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(&cs);
|
||||||
|
bool found = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip);
|
||||||
|
t_assert(found);
|
||||||
|
t_assert(strlen(snip->line) == CS_SNIP_LINE_SIZE - 1);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_trim__when_empty(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
int ret = cs_trim(&cs, CS_ITER_NEWEST_FIRST, 0);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_remove___empty(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
/* Attempt to remove any entry, if present */
|
||||||
|
size_t dummy = 0;
|
||||||
|
int ret = cs_remove(&cs, CS_ITER_NEWEST_FIRST, remove_if_five, &dummy);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_add__around_alloc_batch_threshold(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < CS_SNIP_ALLOC_BATCH - 1; i++) {
|
||||||
|
int ret = cs_add(&cs, "test content", NULL, CS_DUPE_KEEP_ALL);
|
||||||
|
assert(ret == 0);
|
||||||
|
}
|
||||||
|
t_assert(cs.header->nr_snips == CS_SNIP_ALLOC_BATCH - 1);
|
||||||
|
|
||||||
|
/* Add one more entry to exceed the batch threshold */
|
||||||
|
t_assert(cs_add(&cs, "test content", NULL, CS_DUPE_KEEP_ALL) == 0);
|
||||||
|
t_assert(cs.header->nr_snips == CS_SNIP_ALLOC_BATCH);
|
||||||
|
t_assert(cs.header->nr_snips_alloc >= CS_SNIP_ALLOC_BATCH);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_trim__no_remove_when_still_referenced(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
uint64_t hash;
|
||||||
|
for (size_t i = 0; i < 2; i++) {
|
||||||
|
int ret = cs_add(&cs, "test content", &hash, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_drop_(cs_content_unmap) struct cs_content content;
|
||||||
|
|
||||||
|
t_assert(cs.header->nr_snips == 2);
|
||||||
|
t_assert(cs_content_get(&cs, hash, &content) == 0);
|
||||||
|
|
||||||
|
t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 1) == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 1);
|
||||||
|
t_assert(cs_content_get(&cs, hash, &content) == 0);
|
||||||
|
|
||||||
|
t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 0) == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 0);
|
||||||
|
t_assert(cs_content_get(&cs, hash, &content) == -ENOENT);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_replace__out_of_bounds(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
add_ten_snips(&cs);
|
||||||
|
|
||||||
|
int ret = cs_replace(&cs, CS_ITER_NEWEST_FIRST, 10, "test content", NULL);
|
||||||
|
t_assert(ret == -ERANGE);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_replace__add_fail_keeps_old_entry(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
const char *old_content = "OLD_CONTENT";
|
||||||
|
uint64_t old_hash;
|
||||||
|
int ret = cs_add(&cs, old_content, &old_hash, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
const char *new_content = "new";
|
||||||
|
uint64_t new_hash = fnv1a_64_hash(new_content);
|
||||||
|
|
||||||
|
char hash_dir[CS_HASH_STR_MAX];
|
||||||
|
snprintf(hash_dir, sizeof(hash_dir), PRI_HASH, new_hash);
|
||||||
|
ret = mkdirat(cs.content_dir_fd, hash_dir, 0700);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
char fake_path[CS_HASH_STR_MAX + 3];
|
||||||
|
snprintf(fake_path, sizeof(fake_path), "%s/1", hash_dir);
|
||||||
|
_drop_(close) int fake_fd = openat(cs.content_dir_fd, fake_path,
|
||||||
|
O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||||
|
t_assert(fake_fd >= 0);
|
||||||
|
t_assert(write(fake_fd, "X", 1) == 1);
|
||||||
|
|
||||||
|
ret = cs_replace(&cs, CS_ITER_NEWEST_FIRST, 0, new_content, NULL);
|
||||||
|
t_assert(ret == -EFAULT);
|
||||||
|
|
||||||
|
_drop_(cs_content_unmap) struct cs_content old_after;
|
||||||
|
ret = cs_content_get(&cs, old_hash, &old_after);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert((size_t)old_after.size == strlen(old_content));
|
||||||
|
t_assert(strncmp(old_after.data, old_content, (size_t)old_after.size) == 0);
|
||||||
|
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(&cs);
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
t_assert(cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip));
|
||||||
|
t_assert(snip->hash == old_hash);
|
||||||
|
t_assert(streq(snip->line, old_content));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_snip__correct_nr_lines(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
add_ten_snips(&cs);
|
||||||
|
|
||||||
|
/* No need to do exhaustive ones, they're done in test__first_line_* */
|
||||||
|
uint64_t hash;
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
int ret =
|
||||||
|
cs_replace(&cs, CS_ITER_NEWEST_FIRST, 0, "one\ntwo\nthree", &hash);
|
||||||
|
|
||||||
|
t_assert(ret == 0);
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(&cs);
|
||||||
|
t_assert(cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip));
|
||||||
|
t_assert(snip->hash == hash);
|
||||||
|
t_assert(snip->nr_lines == 3);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__first_line__empty(void) {
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
size_t num_lines = first_line("", line);
|
||||||
|
printf("%s\n", line);
|
||||||
|
t_assert(streq(line, ""));
|
||||||
|
t_assert(num_lines == 0);
|
||||||
|
num_lines = first_line("\n", line);
|
||||||
|
t_assert(streq(line, ""));
|
||||||
|
t_assert(num_lines == 1);
|
||||||
|
|
||||||
|
num_lines = first_line("\n\n\n", line);
|
||||||
|
t_assert(streq(line, ""));
|
||||||
|
t_assert(num_lines == 3);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__first_line__only_one(void) {
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
size_t num_lines = first_line("Foo bar\n", line);
|
||||||
|
t_assert(streq(line, "Foo bar"));
|
||||||
|
t_assert(num_lines == 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__first_line__multiple(void) {
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
size_t num_lines = first_line("Foo bar\nbaz\nqux\n", line);
|
||||||
|
t_assert(streq(line, "Foo bar"));
|
||||||
|
t_assert(num_lines == 3);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__first_line__no_final_newline(void) {
|
||||||
|
/* If the last line didn't end with a newline, still count it */
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
size_t num_lines = first_line("Foo bar", line);
|
||||||
|
t_assert(streq(line, "Foo bar"));
|
||||||
|
t_assert(num_lines == 1);
|
||||||
|
num_lines = first_line("Foo bar\nbaz", line);
|
||||||
|
t_assert(streq(line, "Foo bar"));
|
||||||
|
t_assert(num_lines == 2);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__first_line__ignore_blank_lines(void) {
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
size_t num_lines = first_line("\n\n\nFoo bar\n\n\n", line);
|
||||||
|
t_assert(streq(line, "Foo bar"));
|
||||||
|
t_assert(num_lines == 6);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__first_line__unicode(void) {
|
||||||
|
char line[CS_SNIP_LINE_SIZE];
|
||||||
|
size_t num_lines = first_line("道", line);
|
||||||
|
t_assert(streq(line, "道"));
|
||||||
|
t_assert(num_lines == 1);
|
||||||
|
num_lines = first_line("道\n", line);
|
||||||
|
t_assert(streq(line, "道"));
|
||||||
|
t_assert(num_lines == 1);
|
||||||
|
num_lines = first_line("道\n非", line);
|
||||||
|
t_assert(streq(line, "道"));
|
||||||
|
t_assert(num_lines == 2);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__synchronisation(void) {
|
||||||
|
_drop_(remove_test_snip_fd) int snip_fd1 = create_test_snip_fd();
|
||||||
|
_drop_(remove_test_content_dir_fd) int content_dir_fd1 =
|
||||||
|
create_test_content_dir_fd();
|
||||||
|
_drop_(close) int snip_fd2 = dup(snip_fd1);
|
||||||
|
_drop_(close) int content_dir_fd2 = dup(content_dir_fd1);
|
||||||
|
|
||||||
|
assert(snip_fd2 >= 0 && content_dir_fd2 >= 0);
|
||||||
|
|
||||||
|
struct clip_store cs1, cs2;
|
||||||
|
int ret = cs_init(&cs1, snip_fd1, content_dir_fd1);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
ret = cs_init(&cs2, snip_fd2, content_dir_fd2);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
uint64_t hash;
|
||||||
|
ret = cs_add(&cs1, "test content", &hash, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
_drop_(cs_unref) struct ref_guard guard_cs2 = cs_ref(&cs2);
|
||||||
|
while (cs_snip_iter(&guard_cs2, CS_ITER_NEWEST_FIRST, &snip)) {
|
||||||
|
if (snip->hash == hash) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t_assert(found);
|
||||||
|
|
||||||
|
ret = cs_trim(&cs2, CS_ITER_NEWEST_FIRST, 0);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
found = false;
|
||||||
|
snip = NULL;
|
||||||
|
_drop_(cs_unref) struct ref_guard guard_cs1 = cs_ref(&cs1);
|
||||||
|
while (cs_snip_iter(&guard_cs1, CS_ITER_NEWEST_FIRST, &snip)) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
t_assert(!found);
|
||||||
|
|
||||||
|
t_assert(cs_destroy(&cs1) == 0);
|
||||||
|
t_assert(cs_destroy(&cs2) == 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_trim__regrow_does_not_leak_mapping(void) {
|
||||||
|
struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
bool added = true;
|
||||||
|
for (int i = 0; i < 1500; i++) {
|
||||||
|
char buf[32];
|
||||||
|
snprintf(buf, sizeof(buf), "clip-%d", i);
|
||||||
|
if (cs_add(&cs, buf, NULL, CS_DUPE_KEEP_ALL) != 0) {
|
||||||
|
added = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t_assert(added);
|
||||||
|
|
||||||
|
int maps_after_growth = count_test_snip_maps();
|
||||||
|
if (maps_after_growth < 0) {
|
||||||
|
drop_teardown_test(&cs);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
t_assert(maps_after_growth == 1);
|
||||||
|
|
||||||
|
t_assert(cs_trim(&cs, CS_ITER_NEWEST_FIRST, 0) == 0);
|
||||||
|
t_assert(count_test_snip_maps() == 1);
|
||||||
|
|
||||||
|
t_assert(cs_add(&cs, "regrow", NULL, CS_DUPE_KEEP_ALL) == 0);
|
||||||
|
t_assert(count_test_snip_maps() == 1);
|
||||||
|
|
||||||
|
int snip_fd = cs.snip_fd;
|
||||||
|
int content_dir_fd = cs.content_dir_fd;
|
||||||
|
t_assert(cs_destroy(&cs) == 0);
|
||||||
|
t_assert(count_test_snip_maps() == 0);
|
||||||
|
|
||||||
|
close(snip_fd);
|
||||||
|
shm_unlink(TEST_SNIP_FILE);
|
||||||
|
close(content_dir_fd);
|
||||||
|
remove_test_content_dir(TEST_CONTENT_DIR);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_add__dupe_keep_all(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
uint64_t hash1, hash2;
|
||||||
|
int ret = cs_add(&cs, "duplicate", &hash1, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
ret = cs_add(&cs, "duplicate", &hash2, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(hash1 == hash2);
|
||||||
|
t_assert(cs.header->nr_snips == 2);
|
||||||
|
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(&cs);
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
bool iter_ret = cs_snip_iter(&guard, CS_ITER_OLDEST_FIRST, &snip);
|
||||||
|
t_assert(iter_ret == true);
|
||||||
|
t_assert(snip->hash == hash1);
|
||||||
|
iter_ret = cs_snip_iter(&guard, CS_ITER_OLDEST_FIRST, &snip);
|
||||||
|
t_assert(iter_ret == true);
|
||||||
|
t_assert(snip->hash == hash2);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool test__cs_add__dupe_keep_last(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
uint64_t hash1, hash2, hash3;
|
||||||
|
int ret = cs_add(&cs, "duplicate", &hash1, CS_DUPE_KEEP_LAST);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 1);
|
||||||
|
ret = cs_add(&cs, "duplicate", &hash2, CS_DUPE_KEEP_LAST);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 1);
|
||||||
|
ret = cs_add(&cs, "duplicate", &hash3, CS_DUPE_KEEP_LAST);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 1);
|
||||||
|
t_assert(hash1 == hash2);
|
||||||
|
t_assert(hash1 == hash3);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* After adding a duplicate entry, ensure the duplicate is moved to the newest
|
||||||
|
* slot while other entries remain in order. */
|
||||||
|
static bool test__cs_add__dupe_keep_last_with_multiple_entries(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
uint64_t hash_a, hash_dup;
|
||||||
|
int ret = cs_add(&cs, "A", &hash_a, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
ret = cs_add(&cs, "duplicate", &hash_dup, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
ret = cs_add(&cs, "B", NULL, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 3);
|
||||||
|
/* Now add a duplicate entry with KEEP_LAST which should move the duplicate
|
||||||
|
* to the newest slot */
|
||||||
|
ret = cs_add(&cs, "duplicate", NULL, CS_DUPE_KEEP_LAST);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 3);
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(&cs);
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
bool iter_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip);
|
||||||
|
t_assert(iter_ret == true);
|
||||||
|
t_assert(snip->hash == hash_dup);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool check_order(struct clip_store *cs, uint64_t *order, size_t len) {
|
||||||
|
_drop_(cs_unref) struct ref_guard guard = cs_ref(cs);
|
||||||
|
struct cs_snip *snip = NULL;
|
||||||
|
for (size_t i = 0; i < len; ++i) {
|
||||||
|
bool iter_ret = cs_snip_iter(&guard, CS_ITER_NEWEST_FIRST, &snip);
|
||||||
|
t_assert(iter_ret == true);
|
||||||
|
t_assert(snip->hash == order[i]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* After calling cs_make_newest make sure the entry is at the newest slot while
|
||||||
|
* other entries remain in order. */
|
||||||
|
static bool test__cs_make_newest(void) {
|
||||||
|
_drop_(teardown_test) struct clip_store cs = setup_test();
|
||||||
|
|
||||||
|
uint64_t hash_a, hash_b, hash_c;
|
||||||
|
int ret = cs_add(&cs, "A", &hash_a, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
ret = cs_add(&cs, "B", &hash_b, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
ret = cs_add(&cs, "C", &hash_c, CS_DUPE_KEEP_ALL);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
t_assert(cs.header->nr_snips == 3);
|
||||||
|
|
||||||
|
uint64_t order_before[3] = {hash_c, hash_b, hash_a};
|
||||||
|
ret = check_order(&cs, order_before, 3);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
/* Now the order should change to ["A", "C", "B"] */
|
||||||
|
ret = cs_make_newest(&cs, hash_a);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
uint64_t order_after[3] = {hash_a, hash_c, hash_b};
|
||||||
|
ret = check_order(&cs, order_after, 3);
|
||||||
|
t_assert(ret == 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
t_run(test__cs_init);
|
||||||
|
t_run(test__cs_init__bad_size);
|
||||||
|
t_run(test__cs_init__bad_size_aligned);
|
||||||
|
t_run(test__cs_add);
|
||||||
|
t_run(test__cs_content_get__keeps_fd_open);
|
||||||
|
t_run(test__cs_snip_iter);
|
||||||
|
t_run(test__cs_remove);
|
||||||
|
t_run(test__cs_trim);
|
||||||
|
t_run(test__cs_replace);
|
||||||
|
t_run(test__reuse_cs);
|
||||||
|
t_run(test__cs_add__exceeds_snip_line_size);
|
||||||
|
t_run(test__cs_trim__when_empty);
|
||||||
|
t_run(test__cs_remove___empty);
|
||||||
|
t_run(test__cs_add__around_alloc_batch_threshold);
|
||||||
|
t_run(test__cs_replace__out_of_bounds);
|
||||||
|
t_run(test__cs_replace__add_fail_keeps_old_entry);
|
||||||
|
t_run(test__synchronisation);
|
||||||
|
t_run(test__cs_trim__regrow_does_not_leak_mapping);
|
||||||
|
t_run(test__cs_trim__no_remove_when_still_referenced);
|
||||||
|
t_run(test__cs_snip__correct_nr_lines);
|
||||||
|
t_run(test__first_line__empty);
|
||||||
|
t_run(test__first_line__only_one);
|
||||||
|
t_run(test__first_line__multiple);
|
||||||
|
t_run(test__first_line__no_final_newline);
|
||||||
|
t_run(test__first_line__ignore_blank_lines);
|
||||||
|
t_run(test__first_line__unicode);
|
||||||
|
t_run(test__cs_add__dupe_keep_all);
|
||||||
|
t_run(test__cs_add__dupe_keep_last);
|
||||||
|
t_run(test__cs_add__dupe_keep_last_with_multiple_entries);
|
||||||
|
t_run(test__cs_make_newest);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
350
tests/x_integration_tests
Executable file
350
tests/x_integration_tests
Executable file
|
|
@ -0,0 +1,350 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -exE -o errtrace
|
||||||
|
|
||||||
|
dump_cache() {
|
||||||
|
printf '\nTest failure: contents of cache:\n\n' >&2
|
||||||
|
cat "$l_out" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_fail() {
|
||||||
|
set +e
|
||||||
|
"$@"
|
||||||
|
local status=$?
|
||||||
|
set -e
|
||||||
|
(( status != 0 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! (( NO_PID_NAMESPACE )) && (( EUID )); then
|
||||||
|
export _UNSHARED=1
|
||||||
|
exec unshare --user --map-root-user --pid --mount --fork --mount-proc "$0" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( _UNSHARED )); then
|
||||||
|
# Get our own tmp
|
||||||
|
mount -t tmpfs unshared_tmp /tmp
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${0%/*}"/..
|
||||||
|
make clean debug
|
||||||
|
cd src
|
||||||
|
|
||||||
|
export PATH=$PWD:$PATH
|
||||||
|
|
||||||
|
expect_fail env CM_CONFIG=relative CM_DIR="$(mktemp -d)" clipmenu
|
||||||
|
expect_fail env CM_DIR=relative CM_CONFIG="$(mktemp)" clipmenu
|
||||||
|
|
||||||
|
export CM_CONFIG=$(mktemp)
|
||||||
|
export CM_DIR=$(mktemp -d)
|
||||||
|
export CM_DEBUG=1
|
||||||
|
export ASAN_OPTIONS='halt_on_error=1:abort_on_error=1'
|
||||||
|
export UBSAN_OPTIONS='halt_on_error=1:abort_on_error=1'
|
||||||
|
uid=$(id -u)
|
||||||
|
[[ "$(clipctl version)" == 7 ]]
|
||||||
|
[[ "$(clipctl cache-dir)" == "$CM_DIR/clipmenu.7.$uid" ]]
|
||||||
|
|
||||||
|
launcher=$(mktemp)
|
||||||
|
l_out=$(mktemp)
|
||||||
|
|
||||||
|
trap dump_cache ERR
|
||||||
|
|
||||||
|
cat > "$launcher" << EOF
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cat > "$l_out"
|
||||||
|
|
||||||
|
if [[ \$SELECT ]]; then
|
||||||
|
printf '%s\\n' "\$SELECT"
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
chmod a+x "$launcher"
|
||||||
|
export CM_LAUNCHER="$launcher"
|
||||||
|
|
||||||
|
tilde_home=$(mktemp -d)
|
||||||
|
tilde_dir="$tilde_home/clipmenu_tilde"
|
||||||
|
mkdir -p "$tilde_dir"
|
||||||
|
HOME="$tilde_home" CM_DIR="~/clipmenu_tilde" CM_CONFIG="$(mktemp)" \
|
||||||
|
clipmenu || true
|
||||||
|
[[ -d "$tilde_dir/clipmenu.7.$uid" ]]
|
||||||
|
rm -rf "$tilde_home"
|
||||||
|
|
||||||
|
tilde_conf_home=$(mktemp -d)
|
||||||
|
printf 'max_clips nope\n' > "$tilde_conf_home/bad.conf"
|
||||||
|
err=$(HOME="$tilde_conf_home" CM_CONFIG="~/bad.conf" CM_DIR="$(mktemp -d)" \
|
||||||
|
clipmenu 2>&1 || true)
|
||||||
|
[[ "$err" == *"for max_clips"* ]]
|
||||||
|
rm -rf "$tilde_conf_home"
|
||||||
|
|
||||||
|
kill_background_jobs() {
|
||||||
|
local -a bg
|
||||||
|
readarray -t bg < <(jobs -p)
|
||||||
|
(( ${#bg[@]} )) && kill -- "${bg[@]}" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
primary() {
|
||||||
|
printf '%s' "${1?}" | xsel -p
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'kill_background_jobs' EXIT
|
||||||
|
|
||||||
|
settle() {
|
||||||
|
sleep 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
long_settle() {
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
check_nr_clips() {
|
||||||
|
if [[ $SELECT ]]; then
|
||||||
|
clipmenu
|
||||||
|
else
|
||||||
|
# On no selection, we exit 1. Avoid masking real problems by only doing
|
||||||
|
# it when no selection is specified.
|
||||||
|
clipmenu || true
|
||||||
|
fi
|
||||||
|
(( $(wc -l < "$l_out") == "${1?}" ))
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_nr_clips() {
|
||||||
|
local want=${1?}
|
||||||
|
local deadline=$((SECONDS + 5))
|
||||||
|
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
if check_nr_clips "$want"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
check_nr_clips "$want"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! (( USE_CURRENT_DISPLAY )); then
|
||||||
|
export DISPLAY=:1911
|
||||||
|
Xvfb "$DISPLAY" &
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clear selections
|
||||||
|
xsel -bc
|
||||||
|
xsel -pc
|
||||||
|
xsel -sc
|
||||||
|
|
||||||
|
clipmenud &
|
||||||
|
clipmenud_pid=$!
|
||||||
|
settle
|
||||||
|
|
||||||
|
# Should be empty
|
||||||
|
check_nr_clips 0
|
||||||
|
|
||||||
|
primary foo
|
||||||
|
settle
|
||||||
|
|
||||||
|
check_nr_clips 1
|
||||||
|
|
||||||
|
# Put the same one repeatedly, should ignore it
|
||||||
|
primary foo
|
||||||
|
primary foo
|
||||||
|
primary foo
|
||||||
|
settle
|
||||||
|
|
||||||
|
settle
|
||||||
|
check_nr_clips 1
|
||||||
|
[[ "$(< "$l_out")" == "[1] foo" ]]
|
||||||
|
|
||||||
|
# Put possible partials, should update it
|
||||||
|
primary fooa
|
||||||
|
primary fooab
|
||||||
|
primary fooabc
|
||||||
|
|
||||||
|
# Should have replaced the old one
|
||||||
|
settle
|
||||||
|
check_nr_clips 1
|
||||||
|
[[ "$(< "$l_out")" == "[1] fooabc" ]]
|
||||||
|
|
||||||
|
primary fooab
|
||||||
|
primary fooa
|
||||||
|
primary foo
|
||||||
|
|
||||||
|
# Should have replaced the old one
|
||||||
|
settle
|
||||||
|
check_nr_clips 1
|
||||||
|
[[ "$(< "$l_out")" == "[1] foo" ]]
|
||||||
|
|
||||||
|
# Put some more content on the clipboard for testing
|
||||||
|
primary bar
|
||||||
|
primary baz
|
||||||
|
settle
|
||||||
|
|
||||||
|
check_nr_clips 3
|
||||||
|
|
||||||
|
# Nothing gets deleted, but we recognise the right clips
|
||||||
|
[[ $(clipdel a) == $'bar\nbaz' ]]
|
||||||
|
check_nr_clips 3
|
||||||
|
|
||||||
|
# Likewise but inversion
|
||||||
|
[[ $(clipdel -v a) == foo ]]
|
||||||
|
check_nr_clips 3
|
||||||
|
|
||||||
|
# Real delete with inversion
|
||||||
|
[[ $(clipdel -dv a) == foo ]]
|
||||||
|
check_nr_clips 2
|
||||||
|
|
||||||
|
# Test literal match
|
||||||
|
primary '*foo'
|
||||||
|
settle
|
||||||
|
check_nr_clips 3
|
||||||
|
|
||||||
|
[[ $(clipdel -dF '*') == '*foo' ]]
|
||||||
|
check_nr_clips 2
|
||||||
|
|
||||||
|
# Test -n
|
||||||
|
primary latest
|
||||||
|
settle
|
||||||
|
check_nr_clips 3
|
||||||
|
|
||||||
|
# Should delete the newest entry
|
||||||
|
[[ $(clipdel -dn 1) == 'latest' ]]
|
||||||
|
check_nr_clips 2
|
||||||
|
|
||||||
|
# Test -N
|
||||||
|
primary wibble
|
||||||
|
primary wobble
|
||||||
|
settle
|
||||||
|
check_nr_clips 4
|
||||||
|
|
||||||
|
# Should delete everything other than the oldest 2 entries
|
||||||
|
[[ $(clipdel -dvN 2) == $'wibble\nwobble' ]]
|
||||||
|
check_nr_clips 2
|
||||||
|
|
||||||
|
# Test clipdelmenu
|
||||||
|
primary test1
|
||||||
|
primary test2
|
||||||
|
primary test3
|
||||||
|
settle
|
||||||
|
check_nr_clips 5
|
||||||
|
|
||||||
|
# Should delete the selected entry
|
||||||
|
SELECT='[4] test2' clipdelmenu
|
||||||
|
settle
|
||||||
|
check_nr_clips 4
|
||||||
|
|
||||||
|
# Verify test2 is gone and test1, test3 remain
|
||||||
|
clipmenu || true
|
||||||
|
[[ "$(< "$l_out")" == *"test1"* ]]
|
||||||
|
[[ "$(< "$l_out")" == *"test3"* ]]
|
||||||
|
[[ "$(< "$l_out")" != *"test2"* ]]
|
||||||
|
|
||||||
|
# Delete the others
|
||||||
|
SELECT='[3] test1' clipdelmenu
|
||||||
|
SELECT='[3] test3' clipdelmenu
|
||||||
|
|
||||||
|
# Test multi-select deletion
|
||||||
|
primary multi1
|
||||||
|
primary multi2
|
||||||
|
settle
|
||||||
|
check_nr_clips 4
|
||||||
|
|
||||||
|
SELECT=$'[3] multi1\n[4] multi2' clipdelmenu
|
||||||
|
check_nr_clips 2
|
||||||
|
|
||||||
|
# Check selecting starts serving
|
||||||
|
xsel -pc
|
||||||
|
|
||||||
|
SELECT='[1] bar' clipmenu
|
||||||
|
settle
|
||||||
|
[[ "$(xsel -po)" == bar ]]
|
||||||
|
|
||||||
|
SELECT='[2] baz' clipmenu
|
||||||
|
settle
|
||||||
|
[[ "$(xsel -po)" == baz ]]
|
||||||
|
|
||||||
|
# Disable populates no new clips
|
||||||
|
clipctl disable
|
||||||
|
[[ "$(clipctl status)" == disabled ]]
|
||||||
|
primary wibble
|
||||||
|
primary wobble
|
||||||
|
settle
|
||||||
|
check_nr_clips 2
|
||||||
|
|
||||||
|
# Enable enables again
|
||||||
|
clipctl enable
|
||||||
|
[[ "$(clipctl status)" == enabled ]]
|
||||||
|
primary wibble
|
||||||
|
primary wobble
|
||||||
|
settle
|
||||||
|
check_nr_clips 4
|
||||||
|
|
||||||
|
# Toggle works both ways
|
||||||
|
clipctl toggle
|
||||||
|
[[ "$(clipctl status)" == disabled ]]
|
||||||
|
clipctl toggle
|
||||||
|
[[ "$(clipctl status)" == enabled ]]
|
||||||
|
|
||||||
|
# Test INCR support
|
||||||
|
set +x
|
||||||
|
printf '%.0sa' {1..9999999} | xsel -p
|
||||||
|
set -x
|
||||||
|
long_settle # This is a big one, give it some time
|
||||||
|
len_before=$(xsel -po | wc -c)
|
||||||
|
check_nr_clips 5
|
||||||
|
SELECT='[5] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' \
|
||||||
|
clipmenu
|
||||||
|
long_settle
|
||||||
|
len_after=$(xsel -po | wc -c)
|
||||||
|
|
||||||
|
# Make sure we got the whole thing
|
||||||
|
(( len_before == len_after ))
|
||||||
|
|
||||||
|
# Issue #241. Put a large clipboard selection via INCR, then put something else
|
||||||
|
# small immediately after.
|
||||||
|
xsel -bc
|
||||||
|
settle
|
||||||
|
check_nr_clips 5
|
||||||
|
|
||||||
|
# Large selection (INCR)
|
||||||
|
printf '%.0sa' {1..4001} | xsel -b
|
||||||
|
wait_nr_clips 6
|
||||||
|
|
||||||
|
# After large selection is stored, put something small that's a possible #
|
||||||
|
# partial (non-INCR)
|
||||||
|
printf a | xsel -b
|
||||||
|
settle
|
||||||
|
|
||||||
|
check_nr_clips 6
|
||||||
|
|
||||||
|
# own_selections: default does not own PRIMARY
|
||||||
|
kill "$clipmenud_pid"
|
||||||
|
wait "$clipmenud_pid" || true
|
||||||
|
pkill -x clipserve 2>/dev/null || true
|
||||||
|
CM_OWN_CLIPBOARD=1 CM_OWN_SELECTIONS=clipboard clipmenud &
|
||||||
|
clipmenud_pid=$!
|
||||||
|
settle
|
||||||
|
xsel -bc
|
||||||
|
xsel -pc
|
||||||
|
printf 'primary_stale' | xsel -p
|
||||||
|
printf 'clip_default' | xsel -b
|
||||||
|
settle
|
||||||
|
[[ "$(xsel -bo)" == clip_default ]]
|
||||||
|
primary_out=$(xsel -po 2>/dev/null || true)
|
||||||
|
[[ "$primary_out" != clip_default ]]
|
||||||
|
|
||||||
|
# own_selections: owns all listed selections
|
||||||
|
kill "$clipmenud_pid"
|
||||||
|
wait "$clipmenud_pid" || true
|
||||||
|
pkill -x clipserve 2>/dev/null || true
|
||||||
|
CM_OWN_CLIPBOARD=1 CM_OWN_SELECTIONS='clipboard primary' clipmenud &
|
||||||
|
clipmenud_pid=$!
|
||||||
|
settle
|
||||||
|
xsel -bc
|
||||||
|
xsel -pc
|
||||||
|
printf 'primary_stale' | xsel -p
|
||||||
|
printf 'own_both' | xsel -b
|
||||||
|
settle
|
||||||
|
[[ "$(xsel -bo)" == own_both ]]
|
||||||
|
[[ "$(xsel -po)" == own_both ]]
|
||||||
|
|
||||||
|
if (( _UNSHARED )); then
|
||||||
|
umount -l /tmp
|
||||||
|
fi
|
||||||
Loading…
Add table
Add a link
Reference in a new issue