Compare commits
No commits in common. "433e9d2eb4d649ceccd4b5fd85d215eea57343d8" and "993d1b1fefe2e818f95f9993e67fe97431494223" have entirely different histories.
433e9d2eb4
...
993d1b1fef
58 changed files with 433 additions and 7957 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -10,12 +10,11 @@ dmenu/dmenu
|
|||
dmenu/stest
|
||||
slstatus/slstatus
|
||||
slock/slock
|
||||
clipmenu/src/clipmenu
|
||||
clipmenu/src/clipmenud
|
||||
clipmenu/src/clipdel
|
||||
clipmenu/src/clipdelmenu
|
||||
clipmenu/src/clipctl
|
||||
clipmenu/src/clipserve
|
||||
clipmenu/clipmenu
|
||||
clipmenu/clipmenud
|
||||
clipmenu/clipdel
|
||||
clipmenu/clipctl
|
||||
clipmenu/clipserve
|
||||
|
||||
# Restos de parches
|
||||
*.orig
|
||||
|
|
|
|||
63
README.md
63
README.md
|
|
@ -1,63 +0,0 @@
|
|||
# suckless
|
||||
|
||||
Mi configuración de dwm, st, dmenu, slstatus, slock, scroll y clipmenu.
|
||||
Tema Everforest y fuente Fira Code en todos.
|
||||
|
||||
Cada programa se importó con `git subtree --squash`, así que es un único repo
|
||||
pero se puede seguir actualizando desde el proyecto original.
|
||||
|
||||
## Compilar e instalar
|
||||
|
||||
cd dwm && make && sudo make install
|
||||
|
||||
(Igual para `st`, `dmenu`, `slstatus`, `slock`, `scroll` y `clipmenu`.)
|
||||
|
||||
## Actualizar desde el proyecto original
|
||||
|
||||
git subtree pull --prefix=dwm https://git.suckless.org/dwm master --squash
|
||||
git subtree pull --prefix=clipmenu https://github.com/cdown/clipmenu develop --squash
|
||||
|
||||
Los demás funcionan igual que dwm, cambiando el nombre en `--prefix` y en la URL.
|
||||
|
||||
## Atajos de dwm
|
||||
|
||||
`Mod` es `Super` (en el dwm original es `Alt`). Los atajos de ratón también usan
|
||||
`Super`. Solo aparecen los atajos que cambian respecto al original o que son
|
||||
nuevos; el resto están igual. Los atajos de st, scroll, dmenu y slock no se han
|
||||
tocado.
|
||||
|
||||
### Cambiados
|
||||
|
||||
| Acción | Original | Ahora |
|
||||
|---|---|---|
|
||||
| Abrir terminal (`st`) | Mod+Shift+Return | Mod+Return |
|
||||
| zoom (mover la ventana al área principal) | Mod+Return | Mod+Shift+Return |
|
||||
| Cerrar ventana | Mod+Shift+c | Mod+q |
|
||||
| Salir de dwm | Mod+Shift+q | Mod+Shift+m |
|
||||
|
||||
### Nuevos
|
||||
|
||||
| Atajo | Acción |
|
||||
|---|---|
|
||||
| Mod+v | clipmenu (misma paleta y fuente que dmenu) |
|
||||
| Mod+Shift+l | Bloquear la pantalla con `slock` |
|
||||
| Mod+`-` / Mod+`+` | Reducir / aumentar los gaps |
|
||||
| Mod+Shift+`+` | Gaps a 0 |
|
||||
| Tecla de subir / bajar volumen | Volumen ±5% con `wpctl` (máximo 100%) |
|
||||
| Tecla de silenciar | Silenciar o activar el audio |
|
||||
| Tecla de silenciar micro o Mod+ñ | Silenciar o activar el micrófono |
|
||||
| Impr Pant | Captura de pantalla completa con `maim` |
|
||||
| Shift+Impr Pant | Captura de una región |
|
||||
|
||||
Las capturas se guardan en `~/Imágenes/Capturas`, se copian al portapapeles y
|
||||
muestran una notificación.
|
||||
|
||||
## Notas
|
||||
|
||||
- La configuración personal está en `config.h`. `config.def.h` es la versión
|
||||
original más los parches. Si un parche cambia `config.def.h`, hay que pasar
|
||||
el cambio a mano a `config.h`: `make` solo lo copia si `config.h` no existe.
|
||||
- Los parches aplicados están en `<programa>/patches/`.
|
||||
- dwm: fullgaps
|
||||
- st: ligatures, alpha y anysize (en ese orden; anysize necesita dos
|
||||
arreglos a mano, ver el historial de git)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
BinPackArguments: true
|
||||
BreakStringLiterals: false
|
||||
ColumnLimit: 80
|
||||
IndentCaseLabels: true
|
||||
IndentPPDirectives: BeforeHash
|
||||
IndentWidth: 4
|
||||
InsertBraces: true
|
||||
PenaltyBreakComment: 0
|
||||
SortIncludes: true
|
||||
SpaceBeforeParens: ControlStatementsExceptForEachMacros
|
||||
UseTab: Never
|
||||
28
clipmenu/.github/workflows/ci.yml
vendored
28
clipmenu/.github/workflows/ci.yml
vendored
|
|
@ -1,28 +0,0 @@
|
|||
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
clipmenu/.gitignore
vendored
9
clipmenu/.gitignore
vendored
|
|
@ -1,9 +0,0 @@
|
|||
src/clipmenud
|
||||
src/clipctl
|
||||
src/clipdel
|
||||
src/clipdelmenu
|
||||
src/clipmenu
|
||||
src/clipserve
|
||||
tests/test
|
||||
tests/test_store
|
||||
*.o
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
[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
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
.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 .
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
.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 .
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
.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 .
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
.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 .
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
.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 .
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
.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 .
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
.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 .
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
#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);
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
#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;
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
#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);
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
#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);
|
||||
}
|
||||
|
|
@ -1,765 +0,0 @@
|
|||
#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
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
#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;
|
||||
}
|
||||
|
|
@ -1,500 +0,0 @@
|
|||
#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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
#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
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
#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;
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
#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
|
||||
|
|
@ -1,847 +0,0 @@
|
|||
#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;
|
||||
}
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
#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
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
#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;
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
#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
clipmenu/src/x.c
121
clipmenu/src/x.c
|
|
@ -1,121 +0,0 @@
|
|||
#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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#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
|
||||
|
|
@ -1,848 +0,0 @@
|
|||
#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;
|
||||
}
|
||||
|
|
@ -1,350 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -65,7 +65,7 @@ static const Layout layouts[] = {
|
|||
static char dmenumon[2] = "0"; /* component of dmenucmd, manipulated in spawn() */
|
||||
static const char *dmenucmd[] = { "dmenu_run", "-m", dmenumon, "-fn", dmenufont, "-nb", col_bg, "-nf", col_fg, "-sb", col_accent, "-sf", col_accent_fg, NULL };
|
||||
static const char *termcmd[] = { "st", NULL };
|
||||
static const char *clipcmd[] = { "clipmenu", "-fn", dmenufont, "-nb", col_bg, "-nf", col_fg, "-sb", col_accent, "-sf", col_accent_fg, NULL };
|
||||
static const char *clipcmd[] = { "clipmenu", NULL };
|
||||
static const char *lockcmd[] = { "slock", NULL };
|
||||
|
||||
/* Audio: tras cada cambio se avisa a slstatus (SIGUSR1) para que refresque la barra al momento */
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
MIT/X Consortium License
|
||||
|
||||
© 2016-2026 Hiltjo Posthuma <hiltjo@codemadness.org>
|
||||
© 2015-2016 Markus Teich <markus.teich@stusta.mhn.de>
|
||||
© 2014 Dimitris Papastamos <sin@2f30.org>
|
||||
© 2006-2014 Anselm R Garbe <anselm@garbe.us>
|
||||
© 2014-2016 Laslo Hunhold <dev@frign.de>
|
||||
|
||||
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.
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# slock - simple screen locker
|
||||
# See LICENSE file for copyright and license details.
|
||||
|
||||
include config.mk
|
||||
|
||||
SRC = slock.c ${COMPATSRC}
|
||||
OBJ = ${SRC:.c=.o}
|
||||
|
||||
all: slock
|
||||
|
||||
.c.o:
|
||||
${CC} -c ${CFLAGS} $<
|
||||
|
||||
${OBJ}: config.h config.mk util.h
|
||||
|
||||
config.h:
|
||||
cp config.def.h $@
|
||||
|
||||
slock: ${OBJ}
|
||||
${CC} -o $@ ${OBJ} ${LDFLAGS}
|
||||
|
||||
clean:
|
||||
rm -f slock ${OBJ} slock-${VERSION}.tar.gz
|
||||
|
||||
dist: clean
|
||||
mkdir -p slock-${VERSION}
|
||||
cp -R LICENSE Makefile README slock.1 config.mk \
|
||||
${SRC} config.def.h util.h slock-${VERSION}
|
||||
tar -cf slock-${VERSION}.tar slock-${VERSION}
|
||||
gzip slock-${VERSION}.tar
|
||||
rm -rf slock-${VERSION}
|
||||
|
||||
install: all
|
||||
mkdir -p ${DESTDIR}${PREFIX}/bin
|
||||
cp -f slock ${DESTDIR}${PREFIX}/bin
|
||||
chmod 755 ${DESTDIR}${PREFIX}/bin/slock
|
||||
chmod u+s ${DESTDIR}${PREFIX}/bin/slock
|
||||
mkdir -p ${DESTDIR}${MANPREFIX}/man1
|
||||
sed "s/VERSION/${VERSION}/g" <slock.1 >${DESTDIR}${MANPREFIX}/man1/slock.1
|
||||
chmod 644 ${DESTDIR}${MANPREFIX}/man1/slock.1
|
||||
|
||||
uninstall:
|
||||
rm -f ${DESTDIR}${PREFIX}/bin/slock
|
||||
rm -f ${DESTDIR}${MANPREFIX}/man1/slock.1
|
||||
|
||||
.PHONY: all clean dist install uninstall
|
||||
24
slock/README
24
slock/README
|
|
@ -1,24 +0,0 @@
|
|||
slock - simple screen locker
|
||||
============================
|
||||
simple screen locker utility for X.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
In order to build slock you need the Xlib header files.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
Edit config.mk to match your local setup (slock is installed into
|
||||
the /usr/local namespace by default).
|
||||
|
||||
Afterwards enter the following command to build and install slock
|
||||
(if necessary as root):
|
||||
|
||||
make clean install
|
||||
|
||||
|
||||
Running slock
|
||||
-------------
|
||||
Simply invoke the 'slock' command. To get out of it, enter your password.
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/* user and group to drop privileges to */
|
||||
static const char *user = "nobody";
|
||||
static const char *group = "nogroup";
|
||||
|
||||
static const char *colorname[NUMCOLS] = {
|
||||
[INIT] = "black", /* after initialization */
|
||||
[INPUT] = "#005577", /* during input */
|
||||
[FAILED] = "#CC3333", /* wrong password */
|
||||
};
|
||||
|
||||
/* treat a cleared input like a wrong password (color) */
|
||||
static const int failonclear = 1;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/* user and group to drop privileges to */
|
||||
static const char *user = "nobody";
|
||||
static const char *group = "nogroup";
|
||||
|
||||
static const char *colorname[NUMCOLS] = {
|
||||
[INIT] = "#2d353b", /* after initialization (fondo Everforest) */
|
||||
[INPUT] = "#a7c080", /* during input (verde acento) */
|
||||
[FAILED] = "#e67e80", /* wrong password (rojo Everforest) */
|
||||
};
|
||||
|
||||
/* treat a cleared input like a wrong password (color) */
|
||||
static const int failonclear = 1;
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# slock version
|
||||
VERSION = 1.7
|
||||
|
||||
# Customize below to fit your system
|
||||
|
||||
# paths
|
||||
PREFIX = /usr/local
|
||||
MANPREFIX = ${PREFIX}/share/man
|
||||
|
||||
X11INC = /usr/X11R6/include
|
||||
X11LIB = /usr/X11R6/lib
|
||||
|
||||
# includes and libs
|
||||
INCS = -I. -I/usr/include -I${X11INC}
|
||||
LIBS = -L/usr/lib -lc -lcrypt -L${X11LIB} -lX11 -lXext -lXrandr
|
||||
|
||||
# flags
|
||||
CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE -DHAVE_SHADOW_H
|
||||
CFLAGS = -std=c99 -pedantic -Wall -Os ${INCS} ${CPPFLAGS}
|
||||
LDFLAGS = -s ${LIBS}
|
||||
COMPATSRC = explicit_bzero.c
|
||||
|
||||
# On OpenBSD and Darwin remove -lcrypt from LIBS
|
||||
#LIBS = -L/usr/lib -lc -L${X11LIB} -lX11 -lXext -lXrandr
|
||||
# On *BSD remove -DHAVE_SHADOW_H from CPPFLAGS
|
||||
# On NetBSD add -D_NETBSD_SOURCE to CPPFLAGS
|
||||
#CPPFLAGS = -DVERSION=\"${VERSION}\" -D_BSD_SOURCE -D_NETBSD_SOURCE
|
||||
# On OpenBSD set COMPATSRC to empty
|
||||
#COMPATSRC =
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
/* $OpenBSD: explicit_bzero.c,v 1.3 2014/06/21 02:34:26 matthew Exp $ */
|
||||
/*
|
||||
* Public domain.
|
||||
* Written by Matthew Dempsky.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
__attribute__((weak)) void
|
||||
__explicit_bzero_hook(void *buf, size_t len)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
explicit_bzero(void *buf, size_t len)
|
||||
{
|
||||
memset(buf, 0, len);
|
||||
__explicit_bzero_hook(buf, len);
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
.Dd September 5, 2026
|
||||
.Dt SLOCK 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm slock
|
||||
.Nd simple X screen locker
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op Fl v
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
is a simple X screen locker.
|
||||
The options are as follows:
|
||||
.Bl -tag -width Ds
|
||||
.It Fl v
|
||||
Print version information to stdout and exit.
|
||||
.El
|
||||
.Sh EXIT STATUS
|
||||
.Ex -std
|
||||
.Sh SECURITY CONSIDERATIONS
|
||||
To make sure a locked screen can not be bypassed by switching VTs
|
||||
or killing the X server with Ctrl+Alt+Backspace, it is recommended
|
||||
to disable both in
|
||||
.Xr xorg.conf 5
|
||||
for maximum security:
|
||||
.Bd -literal
|
||||
Section "ServerFlags"
|
||||
Option "DontVTSwitch" "True"
|
||||
Option "DontZap" "True"
|
||||
EndSection
|
||||
.Ed
|
||||
.Sh CUSTOMIZATION
|
||||
.Nm
|
||||
can be customized by creating a custom config.h from config.def.h and
|
||||
(re)compiling the source code.
|
||||
This keeps it fast, secure and simple.
|
||||
382
slock/slock.c
382
slock/slock.c
|
|
@ -1,382 +0,0 @@
|
|||
/* See LICENSE file for license details. */
|
||||
#define _XOPEN_SOURCE 500
|
||||
#if HAVE_SHADOW_H
|
||||
#include <shadow.h>
|
||||
#endif
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
|
||||
#include "util.h"
|
||||
|
||||
char *argv0;
|
||||
|
||||
enum {
|
||||
INIT,
|
||||
INPUT,
|
||||
FAILED,
|
||||
NUMCOLS
|
||||
};
|
||||
|
||||
struct lock {
|
||||
int screen;
|
||||
Window root, win;
|
||||
Pixmap pmap;
|
||||
unsigned long colors[NUMCOLS];
|
||||
};
|
||||
|
||||
struct xrandr {
|
||||
int active;
|
||||
int evbase;
|
||||
int errbase;
|
||||
};
|
||||
|
||||
#include "config.h"
|
||||
|
||||
static void
|
||||
die(const char *errstr, ...)
|
||||
{
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, errstr);
|
||||
vfprintf(stderr, errstr, ap);
|
||||
va_end(ap);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
#include <fcntl.h>
|
||||
#include <linux/oom.h>
|
||||
|
||||
static void
|
||||
dontkillme(void)
|
||||
{
|
||||
FILE *f;
|
||||
const char oomfile[] = "/proc/self/oom_score_adj";
|
||||
|
||||
if (!(f = fopen(oomfile, "w"))) {
|
||||
if (errno == ENOENT)
|
||||
return;
|
||||
die("slock: fopen %s: %s\n", oomfile, strerror(errno));
|
||||
}
|
||||
fprintf(f, "%d", OOM_SCORE_ADJ_MIN);
|
||||
if (fclose(f)) {
|
||||
if (errno == EACCES)
|
||||
die("slock: unable to disable OOM killer. "
|
||||
"Make sure to suid or sgid slock.\n");
|
||||
else
|
||||
die("slock: fclose %s: %s\n", oomfile, strerror(errno));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static const char *
|
||||
gethash(void)
|
||||
{
|
||||
const char *hash;
|
||||
struct passwd *pw;
|
||||
|
||||
/* Check if the current user has a password entry */
|
||||
errno = 0;
|
||||
if (!(pw = getpwuid(getuid()))) {
|
||||
if (errno)
|
||||
die("slock: getpwuid: %s\n", strerror(errno));
|
||||
else
|
||||
die("slock: cannot retrieve password entry\n");
|
||||
}
|
||||
hash = pw->pw_passwd;
|
||||
|
||||
#if HAVE_SHADOW_H
|
||||
if (!strcmp(hash, "x")) {
|
||||
struct spwd *sp;
|
||||
if (!(sp = getspnam(pw->pw_name)))
|
||||
die("slock: getspnam: cannot retrieve shadow entry. "
|
||||
"Make sure to suid or sgid slock.\n");
|
||||
hash = sp->sp_pwdp;
|
||||
}
|
||||
#else
|
||||
if (!strcmp(hash, "*")) {
|
||||
#ifdef __OpenBSD__
|
||||
if (!(pw = getpwuid_shadow(getuid())))
|
||||
die("slock: getpwnam_shadow: cannot retrieve shadow entry. "
|
||||
"Make sure to suid or sgid slock.\n");
|
||||
hash = pw->pw_passwd;
|
||||
#else
|
||||
die("slock: getpwuid: cannot retrieve shadow entry. "
|
||||
"Make sure to suid or sgid slock.\n");
|
||||
#endif /* __OpenBSD__ */
|
||||
}
|
||||
#endif /* HAVE_SHADOW_H */
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
static void
|
||||
readpw(Display *dpy, struct xrandr *rr, struct lock **locks, int nscreens,
|
||||
const char *hash)
|
||||
{
|
||||
XRRScreenChangeNotifyEvent *rre;
|
||||
char buf[32], passwd[256], *inputhash;
|
||||
int num, screen, running, failure, oldc;
|
||||
unsigned int len, color;
|
||||
KeySym ksym;
|
||||
XEvent ev;
|
||||
|
||||
len = 0;
|
||||
running = 1;
|
||||
failure = 0;
|
||||
oldc = INIT;
|
||||
|
||||
while (running && !XNextEvent(dpy, &ev)) {
|
||||
if (ev.type == KeyPress) {
|
||||
explicit_bzero(&buf, sizeof(buf));
|
||||
num = XLookupString(&ev.xkey, buf, sizeof(buf), &ksym, 0);
|
||||
if (IsKeypadKey(ksym)) {
|
||||
if (ksym == XK_KP_Enter)
|
||||
ksym = XK_Return;
|
||||
else if (ksym >= XK_KP_0 && ksym <= XK_KP_9)
|
||||
ksym = (ksym - XK_KP_0) + XK_0;
|
||||
}
|
||||
if (IsFunctionKey(ksym) ||
|
||||
IsKeypadKey(ksym) ||
|
||||
IsMiscFunctionKey(ksym) ||
|
||||
IsPFKey(ksym) ||
|
||||
IsPrivateKeypadKey(ksym))
|
||||
continue;
|
||||
switch (ksym) {
|
||||
case XK_Return:
|
||||
passwd[len] = '\0';
|
||||
errno = 0;
|
||||
if (!(inputhash = crypt(passwd, hash)))
|
||||
fprintf(stderr, "slock: crypt: %s\n", strerror(errno));
|
||||
else
|
||||
running = !!strcmp(inputhash, hash);
|
||||
if (running) {
|
||||
XBell(dpy, 100);
|
||||
failure = 1;
|
||||
}
|
||||
explicit_bzero(&passwd, sizeof(passwd));
|
||||
len = 0;
|
||||
break;
|
||||
case XK_Escape:
|
||||
explicit_bzero(&passwd, sizeof(passwd));
|
||||
len = 0;
|
||||
break;
|
||||
case XK_BackSpace:
|
||||
if (len)
|
||||
passwd[--len] = '\0';
|
||||
break;
|
||||
default:
|
||||
if (num && !iscntrl((unsigned char)buf[0]) &&
|
||||
(len + num < sizeof(passwd))) {
|
||||
memcpy(passwd + len, buf, num);
|
||||
len += num;
|
||||
} else if (buf[0] == '\025') { /* ctrl-u clears input */
|
||||
explicit_bzero(&passwd, sizeof(passwd));
|
||||
len = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
color = len ? INPUT : ((failure || failonclear) ? FAILED : INIT);
|
||||
if (running && oldc != color) {
|
||||
for (screen = 0; screen < nscreens; screen++) {
|
||||
XSetWindowBackground(dpy,
|
||||
locks[screen]->win,
|
||||
locks[screen]->colors[color]);
|
||||
XClearWindow(dpy, locks[screen]->win);
|
||||
}
|
||||
oldc = color;
|
||||
}
|
||||
} else if (rr->active && ev.type == rr->evbase + RRScreenChangeNotify) {
|
||||
rre = (XRRScreenChangeNotifyEvent*)&ev;
|
||||
for (screen = 0; screen < nscreens; screen++) {
|
||||
if (locks[screen]->win == rre->window) {
|
||||
if (rre->rotation == RR_Rotate_90 ||
|
||||
rre->rotation == RR_Rotate_270)
|
||||
XResizeWindow(dpy, locks[screen]->win,
|
||||
rre->height, rre->width);
|
||||
else
|
||||
XResizeWindow(dpy, locks[screen]->win,
|
||||
rre->width, rre->height);
|
||||
XClearWindow(dpy, locks[screen]->win);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (screen = 0; screen < nscreens; screen++)
|
||||
XRaiseWindow(dpy, locks[screen]->win);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static struct lock *
|
||||
lockscreen(Display *dpy, struct xrandr *rr, int screen)
|
||||
{
|
||||
char curs[] = {0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int i, ptgrab, kbgrab;
|
||||
struct lock *lock;
|
||||
XColor color, dummy;
|
||||
XSetWindowAttributes wa;
|
||||
Cursor invisible;
|
||||
|
||||
if (dpy == NULL || screen < 0 || !(lock = malloc(sizeof(struct lock))))
|
||||
return NULL;
|
||||
|
||||
lock->screen = screen;
|
||||
lock->root = RootWindow(dpy, lock->screen);
|
||||
|
||||
for (i = 0; i < NUMCOLS; i++) {
|
||||
XAllocNamedColor(dpy, DefaultColormap(dpy, lock->screen),
|
||||
colorname[i], &color, &dummy);
|
||||
lock->colors[i] = color.pixel;
|
||||
}
|
||||
|
||||
/* init */
|
||||
wa.override_redirect = 1;
|
||||
wa.background_pixel = lock->colors[INIT];
|
||||
lock->win = XCreateWindow(dpy, lock->root, 0, 0,
|
||||
DisplayWidth(dpy, lock->screen),
|
||||
DisplayHeight(dpy, lock->screen),
|
||||
0, DefaultDepth(dpy, lock->screen),
|
||||
CopyFromParent,
|
||||
DefaultVisual(dpy, lock->screen),
|
||||
CWOverrideRedirect | CWBackPixel, &wa);
|
||||
lock->pmap = XCreateBitmapFromData(dpy, lock->win, curs, 8, 8);
|
||||
invisible = XCreatePixmapCursor(dpy, lock->pmap, lock->pmap,
|
||||
&color, &color, 0, 0);
|
||||
XDefineCursor(dpy, lock->win, invisible);
|
||||
|
||||
/* Try to grab mouse pointer *and* keyboard for 600ms, else fail the lock */
|
||||
for (i = 0, ptgrab = kbgrab = -1; i < 6; i++) {
|
||||
if (ptgrab != GrabSuccess) {
|
||||
ptgrab = XGrabPointer(dpy, lock->root, False,
|
||||
ButtonPressMask | ButtonReleaseMask |
|
||||
PointerMotionMask, GrabModeAsync,
|
||||
GrabModeAsync, None, invisible, CurrentTime);
|
||||
}
|
||||
if (kbgrab != GrabSuccess) {
|
||||
kbgrab = XGrabKeyboard(dpy, lock->root, True,
|
||||
GrabModeAsync, GrabModeAsync, CurrentTime);
|
||||
}
|
||||
|
||||
/* input is grabbed: we can lock the screen */
|
||||
if (ptgrab == GrabSuccess && kbgrab == GrabSuccess) {
|
||||
XMapRaised(dpy, lock->win);
|
||||
if (rr->active)
|
||||
XRRSelectInput(dpy, lock->win, RRScreenChangeNotifyMask);
|
||||
|
||||
XSelectInput(dpy, lock->root, SubstructureNotifyMask);
|
||||
return lock;
|
||||
}
|
||||
|
||||
/* retry on AlreadyGrabbed but fail on other errors */
|
||||
if ((ptgrab != AlreadyGrabbed && ptgrab != GrabSuccess) ||
|
||||
(kbgrab != AlreadyGrabbed && kbgrab != GrabSuccess))
|
||||
break;
|
||||
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
/* we couldn't grab all input: fail out */
|
||||
if (ptgrab != GrabSuccess)
|
||||
fprintf(stderr, "slock: unable to grab mouse pointer for screen %d\n",
|
||||
screen);
|
||||
if (kbgrab != GrabSuccess)
|
||||
fprintf(stderr, "slock: unable to grab keyboard for screen %d\n",
|
||||
screen);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
usage(void)
|
||||
{
|
||||
die("usage: slock [-v]\n");
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv) {
|
||||
struct xrandr rr;
|
||||
struct lock **locks;
|
||||
struct passwd *pwd;
|
||||
struct group *grp;
|
||||
uid_t duid;
|
||||
gid_t dgid;
|
||||
const char *hash;
|
||||
Display *dpy;
|
||||
int s, nlocks, nscreens;
|
||||
|
||||
if (argc > 1 && !strcmp(argv[1], "-v")) {
|
||||
puts("slock-"VERSION);
|
||||
return 0;
|
||||
} else if (argc > 1) {
|
||||
usage();
|
||||
}
|
||||
|
||||
/* validate drop-user and -group */
|
||||
errno = 0;
|
||||
if (!(pwd = getpwnam(user)))
|
||||
die("slock: getpwnam %s: %s\n", user,
|
||||
errno ? strerror(errno) : "user entry not found");
|
||||
duid = pwd->pw_uid;
|
||||
errno = 0;
|
||||
if (!(grp = getgrnam(group)))
|
||||
die("slock: getgrnam %s: %s\n", group,
|
||||
errno ? strerror(errno) : "group entry not found");
|
||||
dgid = grp->gr_gid;
|
||||
|
||||
#ifdef __linux__
|
||||
dontkillme();
|
||||
#endif
|
||||
|
||||
hash = gethash();
|
||||
errno = 0;
|
||||
if (!crypt("", hash))
|
||||
die("slock: crypt: %s\n", strerror(errno));
|
||||
|
||||
if (!(dpy = XOpenDisplay(NULL)))
|
||||
die("slock: cannot open display\n");
|
||||
|
||||
/* drop privileges */
|
||||
if (setgroups(0, NULL) < 0)
|
||||
die("slock: setgroups: %s\n", strerror(errno));
|
||||
if (setgid(dgid) < 0)
|
||||
die("slock: setgid: %s\n", strerror(errno));
|
||||
if (setuid(duid) < 0)
|
||||
die("slock: setuid: %s\n", strerror(errno));
|
||||
|
||||
/* check for Xrandr support */
|
||||
rr.active = XRRQueryExtension(dpy, &rr.evbase, &rr.errbase);
|
||||
|
||||
/* get number of screens in display "dpy" and blank them */
|
||||
nscreens = ScreenCount(dpy);
|
||||
if (!(locks = calloc(nscreens, sizeof(struct lock *))))
|
||||
die("slock: out of memory\n");
|
||||
for (nlocks = 0, s = 0; s < nscreens; s++) {
|
||||
if ((locks[s] = lockscreen(dpy, &rr, s)) != NULL)
|
||||
nlocks++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
XSync(dpy, 0);
|
||||
|
||||
/* did we manage to lock everything? */
|
||||
if (nlocks != nscreens)
|
||||
return 1;
|
||||
|
||||
/* everything is now blank. Wait for the correct password */
|
||||
readpw(dpy, &rr, locks, nscreens, hash);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
#undef explicit_bzero
|
||||
void explicit_bzero(void *, size_t);
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
include config.mk
|
||||
|
||||
SRC = st.c x.c hb.c
|
||||
SRC = st.c x.c
|
||||
OBJ = $(SRC:.c=.o)
|
||||
|
||||
all: st
|
||||
|
|
@ -16,8 +16,7 @@ config.h:
|
|||
$(CC) $(STCFLAGS) -c $<
|
||||
|
||||
st.o: config.h st.h win.h
|
||||
x.o: arg.h config.h st.h win.h hb.h
|
||||
hb.o: st.h
|
||||
x.o: arg.h config.h st.h win.h
|
||||
|
||||
$(OBJ): config.h config.mk
|
||||
|
||||
|
|
|
|||
|
|
@ -93,9 +93,6 @@ char *termname = "st-256color";
|
|||
*/
|
||||
unsigned int tabspaces = 8;
|
||||
|
||||
/* bg opacity */
|
||||
float alpha = 0.8;
|
||||
|
||||
/* Terminal colors (16 first used in escape sequence) */
|
||||
static const char *colorname[] = {
|
||||
/* 8 normal colors */
|
||||
|
|
|
|||
526
st/config.h
526
st/config.h
|
|
@ -83,49 +83,45 @@ char *termname = "st-256color";
|
|||
* the st.info and appropriately install the st.info in the environment where
|
||||
* you use this st version.
|
||||
*
|
||||
* it#$tabspaces,
|
||||
* it#$tabspaces,
|
||||
*
|
||||
* Secondly make sure your kernel is not expanding tabs. When running `stty
|
||||
* -a` »tab0« should appear. You can tell the terminal to not expand tabs by
|
||||
* running following command:
|
||||
* running following command:
|
||||
*
|
||||
* stty tabs
|
||||
* stty tabs
|
||||
*/
|
||||
unsigned int tabspaces = 8;
|
||||
|
||||
/* bg opacity */
|
||||
float alpha = 0.9;
|
||||
|
||||
/* Terminal colors (16 first used in escape sequence) */
|
||||
static const char *colorname[] = {
|
||||
/* Paleta Everforest (dark medium) */
|
||||
/* 8 normal colors */
|
||||
"#343f44", /* 0: black */
|
||||
"#e67e80", /* 1: red */
|
||||
"#a7c080", /* 2: green */
|
||||
"#dbbc7f", /* 3: yellow */
|
||||
"#7fbbb3", /* 4: blue */
|
||||
"#d699b6", /* 5: magenta */
|
||||
"#83c092", /* 6: cyan */
|
||||
"#d3c6aa", /* 7: white */
|
||||
/* 8 normal colors */
|
||||
"#343f44", /* 0: black */
|
||||
"#e67e80", /* 1: red */
|
||||
"#a7c080", /* 2: green */
|
||||
"#dbbc7f", /* 3: yellow */
|
||||
"#7fbbb3", /* 4: blue */
|
||||
"#d699b6", /* 5: magenta */
|
||||
"#83c092", /* 6: cyan */
|
||||
"#d3c6aa", /* 7: white */
|
||||
|
||||
/* 8 bright colors (un 18% más claros; el negro brillante es el gris de comentarios) */
|
||||
"#859289", /* 8: bright black */
|
||||
"#ea9597", /* 9: bright red */
|
||||
"#b7cb97", /* 10: bright green */
|
||||
"#e1c896", /* 11: bright yellow */
|
||||
"#96c7c1", /* 12: bright blue */
|
||||
"#ddabc3", /* 13: bright magenta */
|
||||
"#99cba6", /* 14: bright cyan */
|
||||
"#dbd0b9", /* 15: bright white */
|
||||
/* 8 bright colors */
|
||||
"#475258", /* 8: bright black */
|
||||
"#e67e80", /* 9: bright red */
|
||||
"#a7c080", /* 10: bright green */
|
||||
"#dbbc7f", /* 11: bright yellow */
|
||||
"#7fbbb3", /* 12: bright blue */
|
||||
"#d699b6", /* 13: bright magenta */
|
||||
"#83c092", /* 14: bright cyan */
|
||||
"#d3c6aa", /* 15: bright white */
|
||||
|
||||
[255] = 0,
|
||||
[255] = 0,
|
||||
|
||||
/* more colors can be added after 255 to use with DefaultXX */
|
||||
"#a7c080", /* 256: default cursor colour (verde acento) */
|
||||
"#3d484d", /* 257: default reverse cursor colour */
|
||||
"#d3c6aa", /* 258: default foreground colour */
|
||||
"#2d353b", /* 259: default background colour */
|
||||
/* more colors can be added after 255 to use with DefaultXX */
|
||||
"#a7c080", /* 256: default cursor colour (Everforest Green Accent) */
|
||||
"#3d484d", /* 257: default reverse cursor colour */
|
||||
"#d3c6aa", /* 258: default foreground colour (Everforest FG) */
|
||||
"#2d353b", /* 259: default background colour (Everforest Dark Medium) */
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -179,12 +175,12 @@ static uint forcemousemod = ShiftMask;
|
|||
* Beware that overloading Button1 will disable the selection.
|
||||
*/
|
||||
static MouseShortcut mshortcuts[] = {
|
||||
/* mask button function argument release */
|
||||
{ XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 },
|
||||
{ ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} },
|
||||
{ XK_ANY_MOD, Button4, ttysend, {.s = "\031"} },
|
||||
{ ShiftMask, Button5, ttysend, {.s = "\033[6;2~"} },
|
||||
{ XK_ANY_MOD, Button5, ttysend, {.s = "\005"} },
|
||||
/* mask button function argument release */
|
||||
{ XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 },
|
||||
{ ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} },
|
||||
{ XK_ANY_MOD, Button4, ttysend, {.s = "\031"} },
|
||||
{ ShiftMask, Button5, ttysend, {.s = "\033[6;2~"} },
|
||||
{ XK_ANY_MOD, Button5, ttysend, {.s = "\005"} },
|
||||
};
|
||||
|
||||
/* Internal keyboard shortcuts. */
|
||||
|
|
@ -192,19 +188,19 @@ static MouseShortcut mshortcuts[] = {
|
|||
#define TERMMOD (ControlMask|ShiftMask)
|
||||
|
||||
static Shortcut shortcuts[] = {
|
||||
/* mask keysym function argument */
|
||||
{ XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} },
|
||||
{ ControlMask, XK_Print, toggleprinter, {.i = 0} },
|
||||
{ ShiftMask, XK_Print, printscreen, {.i = 0} },
|
||||
{ XK_ANY_MOD, XK_Print, printsel, {.i = 0} },
|
||||
{ TERMMOD, XK_Prior, zoom, {.f = +1} },
|
||||
{ TERMMOD, XK_Next, zoom, {.f = -1} },
|
||||
{ TERMMOD, XK_Home, zoomreset, {.f = 0} },
|
||||
{ TERMMOD, XK_C, clipcopy, {.i = 0} },
|
||||
{ TERMMOD, XK_V, clippaste, {.i = 0} },
|
||||
{ TERMMOD, XK_Y, selpaste, {.i = 0} },
|
||||
{ ShiftMask, XK_Insert, selpaste, {.i = 0} },
|
||||
{ TERMMOD, XK_Num_Lock, numlock, {.i = 0} },
|
||||
/* mask keysym function argument */
|
||||
{ XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} },
|
||||
{ ControlMask, XK_Print, toggleprinter, {.i = 0} },
|
||||
{ ShiftMask, XK_Print, printscreen, {.i = 0} },
|
||||
{ XK_ANY_MOD, XK_Print, printsel, {.i = 0} },
|
||||
{ TERMMOD, XK_Prior, zoom, {.f = +1} },
|
||||
{ TERMMOD, XK_Next, zoom, {.f = -1} },
|
||||
{ TERMMOD, XK_Home, zoomreset, {.f = 0} },
|
||||
{ TERMMOD, XK_C, clipcopy, {.i = 0} },
|
||||
{ TERMMOD, XK_V, clippaste, {.i = 0} },
|
||||
{ TERMMOD, XK_Y, selpaste, {.i = 0} },
|
||||
{ ShiftMask, XK_Insert, selpaste, {.i = 0} },
|
||||
{ TERMMOD, XK_Num_Lock, numlock, {.i = 0} },
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
@ -216,7 +212,7 @@ static Shortcut shortcuts[] = {
|
|||
* appkey value:
|
||||
* * 0: no value
|
||||
* * > 0: keypad application mode enabled
|
||||
* * = 2: term.numlock = 1
|
||||
* * = 2: term.numlock = 1
|
||||
* * < 0: keypad application mode disabled
|
||||
* appcursor value:
|
||||
* * 0: no value
|
||||
|
|
@ -245,216 +241,216 @@ static uint ignoremod = Mod2Mask|XK_SWITCH_MOD;
|
|||
* world. Please decide about changes wisely.
|
||||
*/
|
||||
static Key key[] = {
|
||||
/* keysym mask string appkey appcursor */
|
||||
{ XK_KP_Home, ShiftMask, "\033[2J", 0, -1},
|
||||
{ XK_KP_Home, ShiftMask, "\033[1;2H", 0, +1},
|
||||
{ XK_KP_Home, XK_ANY_MOD, "\033[H", 0, -1},
|
||||
{ XK_KP_Home, XK_ANY_MOD, "\033[1~", 0, +1},
|
||||
{ XK_KP_Up, XK_ANY_MOD, "\033Ox", +1, 0},
|
||||
{ XK_KP_Up, XK_ANY_MOD, "\033[A", 0, -1},
|
||||
{ XK_KP_Up, XK_ANY_MOD, "\033OA", 0, +1},
|
||||
{ XK_KP_Down, XK_ANY_MOD, "\033Or", +1, 0},
|
||||
{ XK_KP_Down, XK_ANY_MOD, "\033[B", 0, -1},
|
||||
{ XK_KP_Down, XK_ANY_MOD, "\033OB", 0, +1},
|
||||
{ XK_KP_Left, XK_ANY_MOD, "\033Ot", +1, 0},
|
||||
{ XK_KP_Left, XK_ANY_MOD, "\033[D", 0, -1},
|
||||
{ XK_KP_Left, XK_ANY_MOD, "\033OD", 0, +1},
|
||||
{ XK_KP_Right, XK_ANY_MOD, "\033Ov", +1, 0},
|
||||
{ XK_KP_Right, XK_ANY_MOD, "\033[C", 0, -1},
|
||||
{ XK_KP_Right, XK_ANY_MOD, "\033OC", 0, +1},
|
||||
{ XK_KP_Prior, ShiftMask, "\033[5;2~", 0, 0},
|
||||
{ XK_KP_Prior, XK_ANY_MOD, "\033[5~", 0, 0},
|
||||
{ XK_KP_Begin, XK_ANY_MOD, "\033[E", 0, 0},
|
||||
{ XK_KP_End, ControlMask, "\033[J", -1, 0},
|
||||
{ XK_KP_End, ControlMask, "\033[1;5F", +1, 0},
|
||||
{ XK_KP_End, ShiftMask, "\033[K", -1, 0},
|
||||
{ XK_KP_End, ShiftMask, "\033[1;2F", +1, 0},
|
||||
{ XK_KP_End, XK_ANY_MOD, "\033[4~", 0, 0},
|
||||
{ XK_KP_Next, ShiftMask, "\033[6;2~", 0, 0},
|
||||
{ XK_KP_Next, XK_ANY_MOD, "\033[6~", 0, 0},
|
||||
{ XK_KP_Insert, ShiftMask, "\033[2;2~", +1, 0},
|
||||
{ XK_KP_Insert, ShiftMask, "\033[4l", -1, 0},
|
||||
{ XK_KP_Insert, ControlMask, "\033[L", -1, 0},
|
||||
{ XK_KP_Insert, ControlMask, "\033[2;5~", +1, 0},
|
||||
{ XK_KP_Insert, XK_ANY_MOD, "\033[4h", -1, 0},
|
||||
{ XK_KP_Insert, XK_ANY_MOD, "\033[2~", +1, 0},
|
||||
{ XK_KP_Delete, ControlMask, "\033[M", -1, 0},
|
||||
{ XK_KP_Delete, ControlMask, "\033[3;5~", +1, 0},
|
||||
{ XK_KP_Delete, ShiftMask, "\033[2K", -1, 0},
|
||||
{ XK_KP_Delete, ShiftMask, "\033[3;2~", +1, 0},
|
||||
{ XK_KP_Delete, XK_ANY_MOD, "\033[P", -1, 0},
|
||||
{ XK_KP_Delete, XK_ANY_MOD, "\033[3~", +1, 0},
|
||||
{ XK_KP_Multiply, XK_ANY_MOD, "\033Oj", +2, 0},
|
||||
{ XK_KP_Add, XK_ANY_MOD, "\033Ok", +2, 0},
|
||||
{ XK_KP_Enter, XK_ANY_MOD, "\033OM", +2, 0},
|
||||
{ XK_KP_Enter, XK_ANY_MOD, "\r", -1, 0},
|
||||
{ XK_KP_Subtract, XK_ANY_MOD, "\033Om", +2, 0},
|
||||
{ XK_KP_Decimal, XK_ANY_MOD, "\033On", +2, 0},
|
||||
{ XK_KP_Divide, XK_ANY_MOD, "\033Oo", +2, 0},
|
||||
{ XK_KP_0, XK_ANY_MOD, "\033Op", +2, 0},
|
||||
{ XK_KP_1, XK_ANY_MOD, "\033Oq", +2, 0},
|
||||
{ XK_KP_2, XK_ANY_MOD, "\033Or", +2, 0},
|
||||
{ XK_KP_3, XK_ANY_MOD, "\033Os", +2, 0},
|
||||
{ XK_KP_4, XK_ANY_MOD, "\033Ot", +2, 0},
|
||||
{ XK_KP_5, XK_ANY_MOD, "\033Ou", +2, 0},
|
||||
{ XK_KP_6, XK_ANY_MOD, "\033Ov", +2, 0},
|
||||
{ XK_KP_7, XK_ANY_MOD, "\033Ow", +2, 0},
|
||||
{ XK_KP_8, XK_ANY_MOD, "\033Ox", +2, 0},
|
||||
{ XK_KP_9, XK_ANY_MOD, "\033Oy", +2, 0},
|
||||
{ XK_Up, ShiftMask, "\033[1;2A", 0, 0},
|
||||
{ XK_Up, Mod1Mask, "\033[1;3A", 0, 0},
|
||||
{ XK_Up, ShiftMask|Mod1Mask,"\033[1;4A", 0, 0},
|
||||
{ XK_Up, ControlMask, "\033[1;5A", 0, 0},
|
||||
{ XK_Up, ShiftMask|ControlMask,"\033[1;6A", 0, 0},
|
||||
{ XK_Up, ControlMask|Mod1Mask,"\033[1;7A", 0, 0},
|
||||
{ XK_Up,ShiftMask|ControlMask|Mod1Mask,"\033[1;8A", 0, 0},
|
||||
{ XK_Up, XK_ANY_MOD, "\033[A", 0, -1},
|
||||
{ XK_Up, XK_ANY_MOD, "\033OA", 0, +1},
|
||||
{ XK_Down, ShiftMask, "\033[1;2B", 0, 0},
|
||||
{ XK_Down, Mod1Mask, "\033[1;3B", 0, 0},
|
||||
{ XK_Down, ShiftMask|Mod1Mask,"\033[1;4B", 0, 0},
|
||||
{ XK_Down, ControlMask, "\033[1;5B", 0, 0},
|
||||
{ XK_Down, ShiftMask|ControlMask,"\033[1;6B", 0, 0},
|
||||
{ XK_Down, ControlMask|Mod1Mask,"\033[1;7B", 0, 0},
|
||||
{ XK_Down,ShiftMask|ControlMask|Mod1Mask,"\033[1;8B",0, 0},
|
||||
{ XK_Down, XK_ANY_MOD, "\033[B", 0, -1},
|
||||
{ XK_Down, XK_ANY_MOD, "\033OB", 0, +1},
|
||||
{ XK_Left, ShiftMask, "\033[1;2D", 0, 0},
|
||||
{ XK_Left, Mod1Mask, "\033[1;3D", 0, 0},
|
||||
{ XK_Left, ShiftMask|Mod1Mask,"\033[1;4D", 0, 0},
|
||||
{ XK_Left, ControlMask, "\033[1;5D", 0, 0},
|
||||
{ XK_Left, ShiftMask|ControlMask,"\033[1;6D", 0, 0},
|
||||
{ XK_Left, ControlMask|Mod1Mask,"\033[1;7D", 0, 0},
|
||||
{ XK_Left,ShiftMask|ControlMask|Mod1Mask,"\033[1;8D",0, 0},
|
||||
{ XK_Left, XK_ANY_MOD, "\033[D", 0, -1},
|
||||
{ XK_Left, XK_ANY_MOD, "\033OD", 0, +1},
|
||||
{ XK_Right, ShiftMask, "\033[1;2C", 0, 0},
|
||||
{ XK_Right, Mod1Mask, "\033[1;3C", 0, 0},
|
||||
{ XK_Right, ShiftMask|Mod1Mask,"\033[1;4C", 0, 0},
|
||||
{ XK_Right, ControlMask, "\033[1;5C", 0, 0},
|
||||
{ XK_Right, ShiftMask|ControlMask,"\033[1;6C", 0, 0},
|
||||
{ XK_Right, ControlMask|Mod1Mask,"\033[1;7C", 0, 0},
|
||||
{ XK_Right,ShiftMask|ControlMask|Mod1Mask,"\033[1;8C",0, 0},
|
||||
{ XK_Right, XK_ANY_MOD, "\033[C", 0, -1},
|
||||
{ XK_Right, XK_ANY_MOD, "\033OC", 0, +1},
|
||||
{ XK_ISO_Left_Tab, ShiftMask, "\033[Z", 0, 0},
|
||||
{ XK_Return, Mod1Mask, "\033\r", 0, 0},
|
||||
{ XK_Return, XK_ANY_MOD, "\r", 0, 0},
|
||||
{ XK_Insert, ShiftMask, "\033[4l", -1, 0},
|
||||
{ XK_Insert, ShiftMask, "\033[2;2~", +1, 0},
|
||||
{ XK_Insert, ControlMask, "\033[L", -1, 0},
|
||||
{ XK_Insert, ControlMask, "\033[2;5~", +1, 0},
|
||||
{ XK_Insert, XK_ANY_MOD, "\033[4h", -1, 0},
|
||||
{ XK_Insert, XK_ANY_MOD, "\033[2~", +1, 0},
|
||||
{ XK_Delete, ControlMask, "\033[M", -1, 0},
|
||||
{ XK_Delete, ControlMask, "\033[3;5~", +1, 0},
|
||||
{ XK_Delete, ShiftMask, "\033[2K", -1, 0},
|
||||
{ XK_Delete, ShiftMask, "\033[3;2~", +1, 0},
|
||||
{ XK_Delete, XK_ANY_MOD, "\033[P", -1, 0},
|
||||
{ XK_Delete, XK_ANY_MOD, "\033[3~", +1, 0},
|
||||
{ XK_BackSpace, XK_NO_MOD, "\177", 0, 0},
|
||||
{ XK_BackSpace, Mod1Mask, "\033\177", 0, 0},
|
||||
{ XK_Home, ShiftMask, "\033[2J", 0, -1},
|
||||
{ XK_Home, ShiftMask, "\033[1;2H", 0, +1},
|
||||
{ XK_Home, XK_ANY_MOD, "\033[H", 0, -1},
|
||||
{ XK_Home, XK_ANY_MOD, "\033[1~", 0, +1},
|
||||
{ XK_End, ControlMask, "\033[J", -1, 0},
|
||||
{ XK_End, ControlMask, "\033[1;5F", +1, 0},
|
||||
{ XK_End, ShiftMask, "\033[K", -1, 0},
|
||||
{ XK_End, ShiftMask, "\033[1;2F", +1, 0},
|
||||
{ XK_End, XK_ANY_MOD, "\033[4~", 0, 0},
|
||||
{ XK_Prior, ControlMask, "\033[5;5~", 0, 0},
|
||||
{ XK_Prior, ShiftMask, "\033[5;2~", 0, 0},
|
||||
{ XK_Prior, XK_ANY_MOD, "\033[5~", 0, 0},
|
||||
{ XK_Next, ControlMask, "\033[6;5~", 0, 0},
|
||||
{ XK_Next, ShiftMask, "\033[6;2~", 0, 0},
|
||||
{ XK_Next, XK_ANY_MOD, "\033[6~", 0, 0},
|
||||
{ XK_F1, XK_NO_MOD, "\033OP" , 0, 0},
|
||||
{ XK_F1, /* F13 */ ShiftMask, "\033[1;2P", 0, 0},
|
||||
{ XK_F1, /* F25 */ ControlMask, "\033[1;5P", 0, 0},
|
||||
{ XK_F1, /* F37 */ Mod4Mask, "\033[1;6P", 0, 0},
|
||||
{ XK_F1, /* F49 */ Mod1Mask, "\033[1;3P", 0, 0},
|
||||
{ XK_F1, /* F61 */ Mod3Mask, "\033[1;4P", 0, 0},
|
||||
{ XK_F2, XK_NO_MOD, "\033OQ" , 0, 0},
|
||||
{ XK_F2, /* F14 */ ShiftMask, "\033[1;2Q", 0, 0},
|
||||
{ XK_F2, /* F26 */ ControlMask, "\033[1;5Q", 0, 0},
|
||||
{ XK_F2, /* F38 */ Mod4Mask, "\033[1;6Q", 0, 0},
|
||||
{ XK_F2, /* F50 */ Mod1Mask, "\033[1;3Q", 0, 0},
|
||||
{ XK_F2, /* F62 */ Mod3Mask, "\033[1;4Q", 0, 0},
|
||||
{ XK_F3, XK_NO_MOD, "\033OR" , 0, 0},
|
||||
{ XK_F3, /* F15 */ ShiftMask, "\033[1;2R", 0, 0},
|
||||
{ XK_F3, /* F27 */ ControlMask, "\033[1;5R", 0, 0},
|
||||
{ XK_F3, /* F39 */ Mod4Mask, "\033[1;6R", 0, 0},
|
||||
{ XK_F3, /* F51 */ Mod1Mask, "\033[1;3R", 0, 0},
|
||||
{ XK_F3, /* F63 */ Mod3Mask, "\033[1;4R", 0, 0},
|
||||
{ XK_F4, XK_NO_MOD, "\033OS" , 0, 0},
|
||||
{ XK_F4, /* F16 */ ShiftMask, "\033[1;2S", 0, 0},
|
||||
{ XK_F4, /* F28 */ ControlMask, "\033[1;5S", 0, 0},
|
||||
{ XK_F4, /* F40 */ Mod4Mask, "\033[1;6S", 0, 0},
|
||||
{ XK_F4, /* F52 */ Mod1Mask, "\033[1;3S", 0, 0},
|
||||
{ XK_F5, XK_NO_MOD, "\033[15~", 0, 0},
|
||||
{ XK_F5, /* F17 */ ShiftMask, "\033[15;2~", 0, 0},
|
||||
{ XK_F5, /* F29 */ ControlMask, "\033[15;5~", 0, 0},
|
||||
{ XK_F5, /* F41 */ Mod4Mask, "\033[15;6~", 0, 0},
|
||||
{ XK_F5, /* F53 */ Mod1Mask, "\033[15;3~", 0, 0},
|
||||
{ XK_F6, XK_NO_MOD, "\033[17~", 0, 0},
|
||||
{ XK_F6, /* F18 */ ShiftMask, "\033[17;2~", 0, 0},
|
||||
{ XK_F6, /* F30 */ ControlMask, "\033[17;5~", 0, 0},
|
||||
{ XK_F6, /* F42 */ Mod4Mask, "\033[17;6~", 0, 0},
|
||||
{ XK_F6, /* F54 */ Mod1Mask, "\033[17;3~", 0, 0},
|
||||
{ XK_F7, XK_NO_MOD, "\033[18~", 0, 0},
|
||||
{ XK_F7, /* F19 */ ShiftMask, "\033[18;2~", 0, 0},
|
||||
{ XK_F7, /* F31 */ ControlMask, "\033[18;5~", 0, 0},
|
||||
{ XK_F7, /* F43 */ Mod4Mask, "\033[18;6~", 0, 0},
|
||||
{ XK_F7, /* F55 */ Mod1Mask, "\033[18;3~", 0, 0},
|
||||
{ XK_F8, XK_NO_MOD, "\033[19~", 0, 0},
|
||||
{ XK_F8, /* F20 */ ShiftMask, "\033[19;2~", 0, 0},
|
||||
{ XK_F8, /* F32 */ ControlMask, "\033[19;5~", 0, 0},
|
||||
{ XK_F8, /* F44 */ Mod4Mask, "\033[19;6~", 0, 0},
|
||||
{ XK_F8, /* F56 */ Mod1Mask, "\033[19;3~", 0, 0},
|
||||
{ XK_F9, XK_NO_MOD, "\033[20~", 0, 0},
|
||||
{ XK_F9, /* F21 */ ShiftMask, "\033[20;2~", 0, 0},
|
||||
{ XK_F9, /* F33 */ ControlMask, "\033[20;5~", 0, 0},
|
||||
{ XK_F9, /* F45 */ Mod4Mask, "\033[20;6~", 0, 0},
|
||||
{ XK_F9, /* F57 */ Mod1Mask, "\033[20;3~", 0, 0},
|
||||
{ XK_F10, XK_NO_MOD, "\033[21~", 0, 0},
|
||||
{ XK_F10, /* F22 */ ShiftMask, "\033[21;2~", 0, 0},
|
||||
{ XK_F10, /* F34 */ ControlMask, "\033[21;5~", 0, 0},
|
||||
{ XK_F10, /* F46 */ Mod4Mask, "\033[21;6~", 0, 0},
|
||||
{ XK_F10, /* F58 */ Mod1Mask, "\033[21;3~", 0, 0},
|
||||
{ XK_F11, XK_NO_MOD, "\033[23~", 0, 0},
|
||||
{ XK_F11, /* F23 */ ShiftMask, "\033[23;2~", 0, 0},
|
||||
{ XK_F11, /* F35 */ ControlMask, "\033[23;5~", 0, 0},
|
||||
{ XK_F11, /* F47 */ Mod4Mask, "\033[23;6~", 0, 0},
|
||||
{ XK_F11, /* F59 */ Mod1Mask, "\033[23;3~", 0, 0},
|
||||
{ XK_F12, XK_NO_MOD, "\033[24~", 0, 0},
|
||||
{ XK_F12, /* F24 */ ShiftMask, "\033[24;2~", 0, 0},
|
||||
{ XK_F12, /* F36 */ ControlMask, "\033[24;5~", 0, 0},
|
||||
{ XK_F12, /* F48 */ Mod4Mask, "\033[24;6~", 0, 0},
|
||||
{ XK_F12, /* F60 */ Mod1Mask, "\033[24;3~", 0, 0},
|
||||
{ XK_F13, XK_NO_MOD, "\033[1;2P", 0, 0},
|
||||
{ XK_F14, XK_NO_MOD, "\033[1;2Q", 0, 0},
|
||||
{ XK_F15, XK_NO_MOD, "\033[1;2R", 0, 0},
|
||||
{ XK_F16, XK_NO_MOD, "\033[1;2S", 0, 0},
|
||||
{ XK_F17, XK_NO_MOD, "\033[15;2~", 0, 0},
|
||||
{ XK_F18, XK_NO_MOD, "\033[17;2~", 0, 0},
|
||||
{ XK_F19, XK_NO_MOD, "\033[18;2~", 0, 0},
|
||||
{ XK_F20, XK_NO_MOD, "\033[19;2~", 0, 0},
|
||||
{ XK_F21, XK_NO_MOD, "\033[20;2~", 0, 0},
|
||||
{ XK_F22, XK_NO_MOD, "\033[21;2~", 0, 0},
|
||||
{ XK_F23, XK_NO_MOD, "\033[23;2~", 0, 0},
|
||||
{ XK_F24, XK_NO_MOD, "\033[24;2~", 0, 0},
|
||||
{ XK_F25, XK_NO_MOD, "\033[1;5P", 0, 0},
|
||||
{ XK_F26, XK_NO_MOD, "\033[1;5Q", 0, 0},
|
||||
{ XK_F27, XK_NO_MOD, "\033[1;5R", 0, 0},
|
||||
{ XK_F28, XK_NO_MOD, "\033[1;5S", 0, 0},
|
||||
{ XK_F29, XK_NO_MOD, "\033[15;5~", 0, 0},
|
||||
{ XK_F30, XK_NO_MOD, "\033[17;5~", 0, 0},
|
||||
{ XK_F31, XK_NO_MOD, "\033[18;5~", 0, 0},
|
||||
{ XK_F32, XK_NO_MOD, "\033[19;5~", 0, 0},
|
||||
{ XK_F33, XK_NO_MOD, "\033[20;5~", 0, 0},
|
||||
{ XK_F34, XK_NO_MOD, "\033[21;5~", 0, 0},
|
||||
{ XK_F35, XK_NO_MOD, "\033[23;5~", 0, 0},
|
||||
/* keysym mask string appkey appcursor */
|
||||
{ XK_KP_Home, ShiftMask, "\033[2J", 0, -1},
|
||||
{ XK_KP_Home, ShiftMask, "\033[1;2H", 0, +1},
|
||||
{ XK_KP_Home, XK_ANY_MOD, "\033[H", 0, -1},
|
||||
{ XK_KP_Home, XK_ANY_MOD, "\033[1~", 0, +1},
|
||||
{ XK_KP_Up, XK_ANY_MOD, "\033Ox", +1, 0},
|
||||
{ XK_KP_Up, XK_ANY_MOD, "\033[A", 0, -1},
|
||||
{ XK_KP_Up, XK_ANY_MOD, "\033OA", 0, +1},
|
||||
{ XK_KP_Down, XK_ANY_MOD, "\033Or", +1, 0},
|
||||
{ XK_KP_Down, XK_ANY_MOD, "\033[B", 0, -1},
|
||||
{ XK_KP_Down, XK_ANY_MOD, "\033OB", 0, +1},
|
||||
{ XK_KP_Left, XK_ANY_MOD, "\033Ot", +1, 0},
|
||||
{ XK_KP_Left, XK_ANY_MOD, "\033[D", 0, -1},
|
||||
{ XK_KP_Left, XK_ANY_MOD, "\033OD", 0, +1},
|
||||
{ XK_KP_Right, XK_ANY_MOD, "\033Ov", +1, 0},
|
||||
{ XK_KP_Right, XK_ANY_MOD, "\033[C", 0, -1},
|
||||
{ XK_KP_Right, XK_ANY_MOD, "\033OC", 0, +1},
|
||||
{ XK_KP_Prior, ShiftMask, "\033[5;2~", 0, 0},
|
||||
{ XK_KP_Prior, XK_ANY_MOD, "\033[5~", 0, 0},
|
||||
{ XK_KP_Begin, XK_ANY_MOD, "\033[E", 0, 0},
|
||||
{ XK_KP_End, ControlMask, "\033[J", -1, 0},
|
||||
{ XK_KP_End, ControlMask, "\033[1;5F", +1, 0},
|
||||
{ XK_KP_End, ShiftMask, "\033[K", -1, 0},
|
||||
{ XK_KP_End, ShiftMask, "\033[1;2F", +1, 0},
|
||||
{ XK_KP_End, XK_ANY_MOD, "\033[4~", 0, 0},
|
||||
{ XK_KP_Next, ShiftMask, "\033[6;2~", 0, 0},
|
||||
{ XK_KP_Next, XK_ANY_MOD, "\033[6~", 0, 0},
|
||||
{ XK_KP_Insert, ShiftMask, "\033[2;2~", +1, 0},
|
||||
{ XK_KP_Insert, ShiftMask, "\033[4l", -1, 0},
|
||||
{ XK_KP_Insert, ControlMask, "\033[L", -1, 0},
|
||||
{ XK_KP_Insert, ControlMask, "\033[2;5~", +1, 0},
|
||||
{ XK_KP_Insert, XK_ANY_MOD, "\033[4h", -1, 0},
|
||||
{ XK_KP_Insert, XK_ANY_MOD, "\033[2~", +1, 0},
|
||||
{ XK_KP_Delete, ControlMask, "\033[M", -1, 0},
|
||||
{ XK_KP_Delete, ControlMask, "\033[3;5~", +1, 0},
|
||||
{ XK_KP_Delete, ShiftMask, "\033[2K", -1, 0},
|
||||
{ XK_KP_Delete, ShiftMask, "\033[3;2~", +1, 0},
|
||||
{ XK_KP_Delete, XK_ANY_MOD, "\033[P", -1, 0},
|
||||
{ XK_KP_Delete, XK_ANY_MOD, "\033[3~", +1, 0},
|
||||
{ XK_KP_Multiply, XK_ANY_MOD, "\033Oj", +2, 0},
|
||||
{ XK_KP_Add, XK_ANY_MOD, "\033Ok", +2, 0},
|
||||
{ XK_KP_Enter, XK_ANY_MOD, "\033OM", +2, 0},
|
||||
{ XK_KP_Enter, XK_ANY_MOD, "\r", -1, 0},
|
||||
{ XK_KP_Subtract, XK_ANY_MOD, "\033Om", +2, 0},
|
||||
{ XK_KP_Decimal, XK_ANY_MOD, "\033On", +2, 0},
|
||||
{ XK_KP_Divide, XK_ANY_MOD, "\033Oo", +2, 0},
|
||||
{ XK_KP_0, XK_ANY_MOD, "\033Op", +2, 0},
|
||||
{ XK_KP_1, XK_ANY_MOD, "\033Oq", +2, 0},
|
||||
{ XK_KP_2, XK_ANY_MOD, "\033Or", +2, 0},
|
||||
{ XK_KP_3, XK_ANY_MOD, "\033Os", +2, 0},
|
||||
{ XK_KP_4, XK_ANY_MOD, "\033Ot", +2, 0},
|
||||
{ XK_KP_5, XK_ANY_MOD, "\033Ou", +2, 0},
|
||||
{ XK_KP_6, XK_ANY_MOD, "\033Ov", +2, 0},
|
||||
{ XK_KP_7, XK_ANY_MOD, "\033Ow", +2, 0},
|
||||
{ XK_KP_8, XK_ANY_MOD, "\033Ox", +2, 0},
|
||||
{ XK_KP_9, XK_ANY_MOD, "\033Oy", +2, 0},
|
||||
{ XK_Up, ShiftMask, "\033[1;2A", 0, 0},
|
||||
{ XK_Up, Mod1Mask, "\033[1;3A", 0, 0},
|
||||
{ XK_Up, ShiftMask|Mod1Mask,"\033[1;4A", 0, 0},
|
||||
{ XK_Up, ControlMask, "\033[1;5A", 0, 0},
|
||||
{ XK_Up, ShiftMask|ControlMask,"\033[1;6A", 0, 0},
|
||||
{ XK_Up, ControlMask|Mod1Mask,"\033[1;7A", 0, 0},
|
||||
{ XK_Up,ShiftMask|ControlMask|Mod1Mask,"\033[1;8A", 0, 0},
|
||||
{ XK_Up, XK_ANY_MOD, "\033[A", 0, -1},
|
||||
{ XK_Up, XK_ANY_MOD, "\033OA", 0, +1},
|
||||
{ XK_Down, ShiftMask, "\033[1;2B", 0, 0},
|
||||
{ XK_Down, Mod1Mask, "\033[1;3B", 0, 0},
|
||||
{ XK_Down, ShiftMask|Mod1Mask,"\033[1;4B", 0, 0},
|
||||
{ XK_Down, ControlMask, "\033[1;5B", 0, 0},
|
||||
{ XK_Down, ShiftMask|ControlMask,"\033[1;6B", 0, 0},
|
||||
{ XK_Down, ControlMask|Mod1Mask,"\033[1;7B", 0, 0},
|
||||
{ XK_Down,ShiftMask|ControlMask|Mod1Mask,"\033[1;8B",0, 0},
|
||||
{ XK_Down, XK_ANY_MOD, "\033[B", 0, -1},
|
||||
{ XK_Down, XK_ANY_MOD, "\033OB", 0, +1},
|
||||
{ XK_Left, ShiftMask, "\033[1;2D", 0, 0},
|
||||
{ XK_Left, Mod1Mask, "\033[1;3D", 0, 0},
|
||||
{ XK_Left, ShiftMask|Mod1Mask,"\033[1;4D", 0, 0},
|
||||
{ XK_Left, ControlMask, "\033[1;5D", 0, 0},
|
||||
{ XK_Left, ShiftMask|ControlMask,"\033[1;6D", 0, 0},
|
||||
{ XK_Left, ControlMask|Mod1Mask,"\033[1;7D", 0, 0},
|
||||
{ XK_Left,ShiftMask|ControlMask|Mod1Mask,"\033[1;8D",0, 0},
|
||||
{ XK_Left, XK_ANY_MOD, "\033[D", 0, -1},
|
||||
{ XK_Left, XK_ANY_MOD, "\033OD", 0, +1},
|
||||
{ XK_Right, ShiftMask, "\033[1;2C", 0, 0},
|
||||
{ XK_Right, Mod1Mask, "\033[1;3C", 0, 0},
|
||||
{ XK_Right, ShiftMask|Mod1Mask,"\033[1;4C", 0, 0},
|
||||
{ XK_Right, ControlMask, "\033[1;5C", 0, 0},
|
||||
{ XK_Right, ShiftMask|ControlMask,"\033[1;6C", 0, 0},
|
||||
{ XK_Right, ControlMask|Mod1Mask,"\033[1;7C", 0, 0},
|
||||
{ XK_Right,ShiftMask|ControlMask|Mod1Mask,"\033[1;8C",0, 0},
|
||||
{ XK_Right, XK_ANY_MOD, "\033[C", 0, -1},
|
||||
{ XK_Right, XK_ANY_MOD, "\033OC", 0, +1},
|
||||
{ XK_ISO_Left_Tab, ShiftMask, "\033[Z", 0, 0},
|
||||
{ XK_Return, Mod1Mask, "\033\r", 0, 0},
|
||||
{ XK_Return, XK_ANY_MOD, "\r", 0, 0},
|
||||
{ XK_Insert, ShiftMask, "\033[4l", -1, 0},
|
||||
{ XK_Insert, ShiftMask, "\033[2;2~", +1, 0},
|
||||
{ XK_Insert, ControlMask, "\033[L", -1, 0},
|
||||
{ XK_Insert, ControlMask, "\033[2;5~", +1, 0},
|
||||
{ XK_Insert, XK_ANY_MOD, "\033[4h", -1, 0},
|
||||
{ XK_Insert, XK_ANY_MOD, "\033[2~", +1, 0},
|
||||
{ XK_Delete, ControlMask, "\033[M", -1, 0},
|
||||
{ XK_Delete, ControlMask, "\033[3;5~", +1, 0},
|
||||
{ XK_Delete, ShiftMask, "\033[2K", -1, 0},
|
||||
{ XK_Delete, ShiftMask, "\033[3;2~", +1, 0},
|
||||
{ XK_Delete, XK_ANY_MOD, "\033[P", -1, 0},
|
||||
{ XK_Delete, XK_ANY_MOD, "\033[3~", +1, 0},
|
||||
{ XK_BackSpace, XK_NO_MOD, "\177", 0, 0},
|
||||
{ XK_BackSpace, Mod1Mask, "\033\177", 0, 0},
|
||||
{ XK_Home, ShiftMask, "\033[2J", 0, -1},
|
||||
{ XK_Home, ShiftMask, "\033[1;2H", 0, +1},
|
||||
{ XK_Home, XK_ANY_MOD, "\033[H", 0, -1},
|
||||
{ XK_Home, XK_ANY_MOD, "\033[1~", 0, +1},
|
||||
{ XK_End, ControlMask, "\033[J", -1, 0},
|
||||
{ XK_End, ControlMask, "\033[1;5F", +1, 0},
|
||||
{ XK_End, ShiftMask, "\033[K", -1, 0},
|
||||
{ XK_End, ShiftMask, "\033[1;2F", +1, 0},
|
||||
{ XK_End, XK_ANY_MOD, "\033[4~", 0, 0},
|
||||
{ XK_Prior, ControlMask, "\033[5;5~", 0, 0},
|
||||
{ XK_Prior, ShiftMask, "\033[5;2~", 0, 0},
|
||||
{ XK_Prior, XK_ANY_MOD, "\033[5~", 0, 0},
|
||||
{ XK_Next, ControlMask, "\033[6;5~", 0, 0},
|
||||
{ XK_Next, ShiftMask, "\033[6;2~", 0, 0},
|
||||
{ XK_Next, XK_ANY_MOD, "\033[6~", 0, 0},
|
||||
{ XK_F1, XK_NO_MOD, "\033OP" , 0, 0},
|
||||
{ XK_F1, /* F13 */ ShiftMask, "\033[1;2P", 0, 0},
|
||||
{ XK_F1, /* F25 */ ControlMask, "\033[1;5P", 0, 0},
|
||||
{ XK_F1, /* F37 */ Mod4Mask, "\033[1;6P", 0, 0},
|
||||
{ XK_F1, /* F49 */ Mod1Mask, "\033[1;3P", 0, 0},
|
||||
{ XK_F1, /* F61 */ Mod3Mask, "\033[1;4P", 0, 0},
|
||||
{ XK_F2, XK_NO_MOD, "\033OQ" , 0, 0},
|
||||
{ XK_F2, /* F14 */ ShiftMask, "\033[1;2Q", 0, 0},
|
||||
{ XK_F2, /* F26 */ ControlMask, "\033[1;5Q", 0, 0},
|
||||
{ XK_F2, /* F38 */ Mod4Mask, "\033[1;6Q", 0, 0},
|
||||
{ XK_F2, /* F50 */ Mod1Mask, "\033[1;3Q", 0, 0},
|
||||
{ XK_F2, /* F62 */ Mod3Mask, "\033[1;4Q", 0, 0},
|
||||
{ XK_F3, XK_NO_MOD, "\033OR" , 0, 0},
|
||||
{ XK_F3, /* F15 */ ShiftMask, "\033[1;2R", 0, 0},
|
||||
{ XK_F3, /* F27 */ ControlMask, "\033[1;5R", 0, 0},
|
||||
{ XK_F3, /* F39 */ Mod4Mask, "\033[1;6R", 0, 0},
|
||||
{ XK_F3, /* F51 */ Mod1Mask, "\033[1;3R", 0, 0},
|
||||
{ XK_F3, /* F63 */ Mod3Mask, "\033[1;4R", 0, 0},
|
||||
{ XK_F4, XK_NO_MOD, "\033OS" , 0, 0},
|
||||
{ XK_F4, /* F16 */ ShiftMask, "\033[1;2S", 0, 0},
|
||||
{ XK_F4, /* F28 */ ControlMask, "\033[1;5S", 0, 0},
|
||||
{ XK_F4, /* F40 */ Mod4Mask, "\033[1;6S", 0, 0},
|
||||
{ XK_F4, /* F52 */ Mod1Mask, "\033[1;3S", 0, 0},
|
||||
{ XK_F5, XK_NO_MOD, "\033[15~", 0, 0},
|
||||
{ XK_F5, /* F17 */ ShiftMask, "\033[15;2~", 0, 0},
|
||||
{ XK_F5, /* F29 */ ControlMask, "\033[15;5~", 0, 0},
|
||||
{ XK_F5, /* F41 */ Mod4Mask, "\033[15;6~", 0, 0},
|
||||
{ XK_F5, /* F53 */ Mod1Mask, "\033[15;3~", 0, 0},
|
||||
{ XK_F6, XK_NO_MOD, "\033[17~", 0, 0},
|
||||
{ XK_F6, /* F18 */ ShiftMask, "\033[17;2~", 0, 0},
|
||||
{ XK_F6, /* F30 */ ControlMask, "\033[17;5~", 0, 0},
|
||||
{ XK_F6, /* F42 */ Mod4Mask, "\033[17;6~", 0, 0},
|
||||
{ XK_F6, /* F54 */ Mod1Mask, "\033[17;3~", 0, 0},
|
||||
{ XK_F7, XK_NO_MOD, "\033[18~", 0, 0},
|
||||
{ XK_F7, /* F19 */ ShiftMask, "\033[18;2~", 0, 0},
|
||||
{ XK_F7, /* F31 */ ControlMask, "\033[18;5~", 0, 0},
|
||||
{ XK_F7, /* F43 */ Mod4Mask, "\033[18;6~", 0, 0},
|
||||
{ XK_F7, /* F55 */ Mod1Mask, "\033[18;3~", 0, 0},
|
||||
{ XK_F8, XK_NO_MOD, "\033[19~", 0, 0},
|
||||
{ XK_F8, /* F20 */ ShiftMask, "\033[19;2~", 0, 0},
|
||||
{ XK_F8, /* F32 */ ControlMask, "\033[19;5~", 0, 0},
|
||||
{ XK_F8, /* F44 */ Mod4Mask, "\033[19;6~", 0, 0},
|
||||
{ XK_F8, /* F56 */ Mod1Mask, "\033[19;3~", 0, 0},
|
||||
{ XK_F9, XK_NO_MOD, "\033[20~", 0, 0},
|
||||
{ XK_F9, /* F21 */ ShiftMask, "\033[20;2~", 0, 0},
|
||||
{ XK_F9, /* F33 */ ControlMask, "\033[20;5~", 0, 0},
|
||||
{ XK_F9, /* F45 */ Mod4Mask, "\033[20;6~", 0, 0},
|
||||
{ XK_F9, /* F57 */ Mod1Mask, "\033[20;3~", 0, 0},
|
||||
{ XK_F10, XK_NO_MOD, "\033[21~", 0, 0},
|
||||
{ XK_F10, /* F22 */ ShiftMask, "\033[21;2~", 0, 0},
|
||||
{ XK_F10, /* F34 */ ControlMask, "\033[21;5~", 0, 0},
|
||||
{ XK_F10, /* F46 */ Mod4Mask, "\033[21;6~", 0, 0},
|
||||
{ XK_F10, /* F58 */ Mod1Mask, "\033[21;3~", 0, 0},
|
||||
{ XK_F11, XK_NO_MOD, "\033[23~", 0, 0},
|
||||
{ XK_F11, /* F23 */ ShiftMask, "\033[23;2~", 0, 0},
|
||||
{ XK_F11, /* F35 */ ControlMask, "\033[23;5~", 0, 0},
|
||||
{ XK_F11, /* F47 */ Mod4Mask, "\033[23;6~", 0, 0},
|
||||
{ XK_F11, /* F59 */ Mod1Mask, "\033[23;3~", 0, 0},
|
||||
{ XK_F12, XK_NO_MOD, "\033[24~", 0, 0},
|
||||
{ XK_F12, /* F24 */ ShiftMask, "\033[24;2~", 0, 0},
|
||||
{ XK_F12, /* F36 */ ControlMask, "\033[24;5~", 0, 0},
|
||||
{ XK_F12, /* F48 */ Mod4Mask, "\033[24;6~", 0, 0},
|
||||
{ XK_F12, /* F60 */ Mod1Mask, "\033[24;3~", 0, 0},
|
||||
{ XK_F13, XK_NO_MOD, "\033[1;2P", 0, 0},
|
||||
{ XK_F14, XK_NO_MOD, "\033[1;2Q", 0, 0},
|
||||
{ XK_F15, XK_NO_MOD, "\033[1;2R", 0, 0},
|
||||
{ XK_F16, XK_NO_MOD, "\033[1;2S", 0, 0},
|
||||
{ XK_F17, XK_NO_MOD, "\033[15;2~", 0, 0},
|
||||
{ XK_F18, XK_NO_MOD, "\033[17;2~", 0, 0},
|
||||
{ XK_F19, XK_NO_MOD, "\033[18;2~", 0, 0},
|
||||
{ XK_F20, XK_NO_MOD, "\033[19;2~", 0, 0},
|
||||
{ XK_F21, XK_NO_MOD, "\033[20;2~", 0, 0},
|
||||
{ XK_F22, XK_NO_MOD, "\033[21;2~", 0, 0},
|
||||
{ XK_F23, XK_NO_MOD, "\033[23;2~", 0, 0},
|
||||
{ XK_F24, XK_NO_MOD, "\033[24;2~", 0, 0},
|
||||
{ XK_F25, XK_NO_MOD, "\033[1;5P", 0, 0},
|
||||
{ XK_F26, XK_NO_MOD, "\033[1;5Q", 0, 0},
|
||||
{ XK_F27, XK_NO_MOD, "\033[1;5R", 0, 0},
|
||||
{ XK_F28, XK_NO_MOD, "\033[1;5S", 0, 0},
|
||||
{ XK_F29, XK_NO_MOD, "\033[15;5~", 0, 0},
|
||||
{ XK_F30, XK_NO_MOD, "\033[17;5~", 0, 0},
|
||||
{ XK_F31, XK_NO_MOD, "\033[18;5~", 0, 0},
|
||||
{ XK_F32, XK_NO_MOD, "\033[19;5~", 0, 0},
|
||||
{ XK_F33, XK_NO_MOD, "\033[20;5~", 0, 0},
|
||||
{ XK_F34, XK_NO_MOD, "\033[21;5~", 0, 0},
|
||||
{ XK_F35, XK_NO_MOD, "\033[23;5~", 0, 0},
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
@ -465,7 +461,7 @@ static Key key[] = {
|
|||
* If no match is found, regular selection is used.
|
||||
*/
|
||||
static uint selmasks[] = {
|
||||
[SEL_RECTANGULAR] = Mod1Mask,
|
||||
[SEL_RECTANGULAR] = Mod1Mask,
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
@ -473,6 +469,6 @@ static uint selmasks[] = {
|
|||
* of single wide characters.
|
||||
*/
|
||||
static char ascii_printable[] =
|
||||
" !\"#$%&'()*+,-./0123456789:;<=>?"
|
||||
"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
|
||||
"`abcdefghijklmnopqrstuvwxyz{|}~";
|
||||
" !\"#$%&'()*+,-./0123456789:;<=>?"
|
||||
"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
|
||||
"`abcdefghijklmnopqrstuvwxyz{|}~";
|
||||
|
|
|
|||
|
|
@ -15,12 +15,10 @@ PKG_CONFIG = pkg-config
|
|||
# includes and libs
|
||||
INCS = -I$(X11INC) \
|
||||
`$(PKG_CONFIG) --cflags fontconfig` \
|
||||
`$(PKG_CONFIG) --cflags freetype2` \
|
||||
`$(PKG_CONFIG) --cflags harfbuzz`
|
||||
`$(PKG_CONFIG) --cflags freetype2`
|
||||
LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \
|
||||
`$(PKG_CONFIG) --libs fontconfig` \
|
||||
`$(PKG_CONFIG) --libs freetype2` \
|
||||
`$(PKG_CONFIG) --libs harfbuzz`
|
||||
`$(PKG_CONFIG) --libs freetype2`
|
||||
|
||||
# flags
|
||||
STCPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600
|
||||
|
|
@ -31,8 +29,7 @@ STLDFLAGS = $(LIBS) $(LDFLAGS)
|
|||
#CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600 -D_BSD_SOURCE
|
||||
#LIBS = -L$(X11LIB) -lm -lX11 -lutil -lXft \
|
||||
# `$(PKG_CONFIG) --libs fontconfig` \
|
||||
# `$(PKG_CONFIG) --libs freetype2` \
|
||||
# `$(PKG_CONFIG) --libs harfbuzz`
|
||||
# `$(PKG_CONFIG) --libs freetype2`
|
||||
#MANPREFIX = ${PREFIX}/man
|
||||
|
||||
# compiler and linker
|
||||
|
|
|
|||
125
st/hb.c
125
st/hb.c
|
|
@ -1,125 +0,0 @@
|
|||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <X11/Xft/Xft.h>
|
||||
#include <X11/cursorfont.h>
|
||||
#include <hb.h>
|
||||
#include <hb-ft.h>
|
||||
|
||||
#include "st.h"
|
||||
#include "hb.h"
|
||||
|
||||
#define FEATURE(c1,c2,c3,c4) { .tag = HB_TAG(c1,c2,c3,c4), .value = 1, .start = HB_FEATURE_GLOBAL_START, .end = HB_FEATURE_GLOBAL_END }
|
||||
#define BUFFER_STEP 256
|
||||
|
||||
hb_font_t *hbfindfont(XftFont *match);
|
||||
|
||||
typedef struct {
|
||||
XftFont *match;
|
||||
hb_font_t *font;
|
||||
} HbFontMatch;
|
||||
|
||||
typedef struct {
|
||||
size_t capacity;
|
||||
HbFontMatch *fonts;
|
||||
} HbFontCache;
|
||||
|
||||
static HbFontCache hbfontcache = { 0, NULL };
|
||||
|
||||
typedef struct {
|
||||
size_t capacity;
|
||||
Rune *runes;
|
||||
} RuneBuffer;
|
||||
|
||||
static RuneBuffer hbrunebuffer = { 0, NULL };
|
||||
|
||||
/*
|
||||
* Poplulate the array with a list of font features, wrapped in FEATURE macro,
|
||||
* e. g.
|
||||
* FEATURE('c', 'a', 'l', 't'), FEATURE('d', 'l', 'i', 'g')
|
||||
*/
|
||||
hb_feature_t features[] = { };
|
||||
|
||||
void
|
||||
hbunloadfonts()
|
||||
{
|
||||
for (int i = 0; i < hbfontcache.capacity; i++) {
|
||||
hb_font_destroy(hbfontcache.fonts[i].font);
|
||||
XftUnlockFace(hbfontcache.fonts[i].match);
|
||||
}
|
||||
|
||||
if (hbfontcache.fonts != NULL) {
|
||||
free(hbfontcache.fonts);
|
||||
hbfontcache.fonts = NULL;
|
||||
}
|
||||
hbfontcache.capacity = 0;
|
||||
}
|
||||
|
||||
hb_font_t *
|
||||
hbfindfont(XftFont *match)
|
||||
{
|
||||
for (int i = 0; i < hbfontcache.capacity; i++) {
|
||||
if (hbfontcache.fonts[i].match == match)
|
||||
return hbfontcache.fonts[i].font;
|
||||
}
|
||||
|
||||
/* Font not found in cache, caching it now. */
|
||||
hbfontcache.fonts = realloc(hbfontcache.fonts, sizeof(HbFontMatch) * (hbfontcache.capacity + 1));
|
||||
FT_Face face = XftLockFace(match);
|
||||
hb_font_t *font = hb_ft_font_create(face, NULL);
|
||||
if (font == NULL)
|
||||
die("Failed to load Harfbuzz font.");
|
||||
|
||||
hbfontcache.fonts[hbfontcache.capacity].match = match;
|
||||
hbfontcache.fonts[hbfontcache.capacity].font = font;
|
||||
hbfontcache.capacity += 1;
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
void hbtransform(HbTransformData *data, XftFont *xfont, const Glyph *glyphs, int start, int length) {
|
||||
ushort mode = USHRT_MAX;
|
||||
unsigned int glyph_count;
|
||||
int rune_idx, glyph_idx, end = start + length;
|
||||
|
||||
hb_font_t *font = hbfindfont(xfont);
|
||||
if (font == NULL)
|
||||
return;
|
||||
|
||||
hb_buffer_t *buffer = hb_buffer_create();
|
||||
hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);
|
||||
hb_buffer_set_cluster_level(buffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS);
|
||||
|
||||
/* Resize the buffer if required length is larger. */
|
||||
if (hbrunebuffer.capacity < length) {
|
||||
hbrunebuffer.capacity = (length / BUFFER_STEP + 1) * BUFFER_STEP;
|
||||
hbrunebuffer.runes = realloc(hbrunebuffer.runes, hbrunebuffer.capacity * sizeof(Rune));
|
||||
}
|
||||
|
||||
/* Fill buffer with codepoints. */
|
||||
for (rune_idx = 0, glyph_idx = start; glyph_idx < end; glyph_idx++, rune_idx++) {
|
||||
hbrunebuffer.runes[rune_idx] = glyphs[glyph_idx].u;
|
||||
mode = glyphs[glyph_idx].mode;
|
||||
if (mode & ATTR_WDUMMY)
|
||||
hbrunebuffer.runes[rune_idx] = 0x0020;
|
||||
}
|
||||
hb_buffer_add_codepoints(buffer, hbrunebuffer.runes, length, 0, length);
|
||||
|
||||
/* Shape the segment. */
|
||||
hb_shape(font, buffer, features, sizeof(features)/sizeof(hb_feature_t));
|
||||
|
||||
/* Get new glyph info. */
|
||||
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(buffer, &glyph_count);
|
||||
hb_glyph_position_t *pos = hb_buffer_get_glyph_positions(buffer, &glyph_count);
|
||||
|
||||
/* Fill the output. */
|
||||
data->buffer = buffer;
|
||||
data->glyphs = info;
|
||||
data->positions = pos;
|
||||
data->count = glyph_count;
|
||||
}
|
||||
|
||||
void hbcleanup(HbTransformData *data) {
|
||||
hb_buffer_destroy(data->buffer);
|
||||
memset(data, 0, sizeof(HbTransformData));
|
||||
}
|
||||
14
st/hb.h
14
st/hb.h
|
|
@ -1,14 +0,0 @@
|
|||
#include <X11/Xft/Xft.h>
|
||||
#include <hb.h>
|
||||
#include <hb-ft.h>
|
||||
|
||||
typedef struct {
|
||||
hb_buffer_t *buffer;
|
||||
hb_glyph_info_t *glyphs;
|
||||
hb_glyph_position_t *positions;
|
||||
unsigned int count;
|
||||
} HbTransformData;
|
||||
|
||||
void hbunloadfonts();
|
||||
void hbtransform(HbTransformData *, XftFont *, const Glyph *, int, int);
|
||||
void hbcleanup(HbTransformData *);
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
diff --git a/config.def.h b/config.def.h
|
||||
index 2cd740a..019a4e1 100644
|
||||
--- a/config.def.h
|
||||
+++ b/config.def.h
|
||||
@@ -93,6 +93,9 @@ char *termname = "st-256color";
|
||||
*/
|
||||
unsigned int tabspaces = 8;
|
||||
|
||||
+/* bg opacity */
|
||||
+float alpha = 0.8;
|
||||
+
|
||||
/* Terminal colors (16 first used in escape sequence) */
|
||||
static const char *colorname[] = {
|
||||
/* 8 normal colors */
|
||||
diff --git a/x.c b/x.c
|
||||
index d73152b..f32fd6c 100644
|
||||
--- a/x.c
|
||||
+++ b/x.c
|
||||
@@ -105,6 +105,7 @@ typedef struct {
|
||||
XSetWindowAttributes attrs;
|
||||
int scr;
|
||||
int isfixed; /* is fixed geometry? */
|
||||
+ int depth; /* bit depth */
|
||||
int l, t; /* left and top offset */
|
||||
int gm; /* geometry mask */
|
||||
} XWindow;
|
||||
@@ -752,7 +753,7 @@ xresize(int col, int row)
|
||||
|
||||
XFreePixmap(xw.dpy, xw.buf);
|
||||
xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h,
|
||||
- DefaultDepth(xw.dpy, xw.scr));
|
||||
+ xw.depth);
|
||||
XftDrawChange(xw.draw, xw.buf);
|
||||
xclear(0, 0, win.w, win.h);
|
||||
|
||||
@@ -812,6 +813,10 @@ xloadcols(void)
|
||||
else
|
||||
die("could not allocate color %d\n", i);
|
||||
}
|
||||
+
|
||||
+ dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * alpha);
|
||||
+ dc.col[defaultbg].pixel &= 0x00FFFFFF;
|
||||
+ dc.col[defaultbg].pixel |= (unsigned char)(0xff * alpha) << 24;
|
||||
loaded = 1;
|
||||
}
|
||||
|
||||
@@ -842,6 +847,12 @@ xsetcolorname(int x, const char *name)
|
||||
XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]);
|
||||
dc.col[x] = ncolor;
|
||||
|
||||
+ if (x == defaultbg) {
|
||||
+ dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * alpha);
|
||||
+ dc.col[defaultbg].pixel &= 0x00FFFFFF;
|
||||
+ dc.col[defaultbg].pixel |= (unsigned char)(0xff * alpha) << 24;
|
||||
+ }
|
||||
+
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1134,11 +1145,25 @@ xinit(int cols, int rows)
|
||||
Window parent, root;
|
||||
pid_t thispid = getpid();
|
||||
XColor xmousefg, xmousebg;
|
||||
+ XWindowAttributes attr;
|
||||
+ XVisualInfo vis;
|
||||
|
||||
if (!(xw.dpy = XOpenDisplay(NULL)))
|
||||
die("can't open display\n");
|
||||
xw.scr = XDefaultScreen(xw.dpy);
|
||||
- xw.vis = XDefaultVisual(xw.dpy, xw.scr);
|
||||
+
|
||||
+ root = XRootWindow(xw.dpy, xw.scr);
|
||||
+ if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0))))
|
||||
+ parent = root;
|
||||
+
|
||||
+ if (XMatchVisualInfo(xw.dpy, xw.scr, 32, TrueColor, &vis) != 0) {
|
||||
+ xw.vis = vis.visual;
|
||||
+ xw.depth = vis.depth;
|
||||
+ } else {
|
||||
+ XGetWindowAttributes(xw.dpy, parent, &attr);
|
||||
+ xw.vis = attr.visual;
|
||||
+ xw.depth = attr.depth;
|
||||
+ }
|
||||
|
||||
/* font */
|
||||
if (!FcInit())
|
||||
@@ -1148,7 +1173,7 @@ xinit(int cols, int rows)
|
||||
xloadfonts(usedfont, 0);
|
||||
|
||||
/* colors */
|
||||
- xw.cmap = XDefaultColormap(xw.dpy, xw.scr);
|
||||
+ xw.cmap = XCreateColormap(xw.dpy, parent, xw.vis, None);
|
||||
xloadcols();
|
||||
|
||||
/* adjust fixed window geometry */
|
||||
@@ -1168,11 +1193,8 @@ xinit(int cols, int rows)
|
||||
| ButtonMotionMask | ButtonPressMask | ButtonReleaseMask;
|
||||
xw.attrs.colormap = xw.cmap;
|
||||
|
||||
- root = XRootWindow(xw.dpy, xw.scr);
|
||||
- if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0))))
|
||||
- parent = root;
|
||||
- xw.win = XCreateWindow(xw.dpy, root, xw.l, xw.t,
|
||||
- win.w, win.h, 0, XDefaultDepth(xw.dpy, xw.scr), InputOutput,
|
||||
+ xw.win = XCreateWindow(xw.dpy, parent, xw.l, xw.t,
|
||||
+ win.w, win.h, 0, xw.depth, InputOutput,
|
||||
xw.vis, CWBackPixel | CWBorderPixel | CWBitGravity
|
||||
| CWEventMask | CWColormap, &xw.attrs);
|
||||
if (parent != root)
|
||||
@@ -1183,7 +1205,7 @@ xinit(int cols, int rows)
|
||||
dc.gc = XCreateGC(xw.dpy, xw.win, GCGraphicsExposures,
|
||||
&gcvalues);
|
||||
xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h,
|
||||
- DefaultDepth(xw.dpy, xw.scr));
|
||||
+ xw.depth);
|
||||
XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel);
|
||||
XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h);
|
||||
|
||||
@@ -2047,6 +2069,10 @@ main(int argc, char *argv[])
|
||||
case 'a':
|
||||
allowaltscreen = 0;
|
||||
break;
|
||||
+ case 'A':
|
||||
+ alpha = strtof(EARGF(usage()), NULL);
|
||||
+ LIMIT(alpha, 0.0, 1.0);
|
||||
+ break;
|
||||
case 'c':
|
||||
opt_class = EARGF(usage());
|
||||
break;
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
From 8dcdc4b21a73268e167d98aa30f24315c7f3b7ff Mon Sep 17 00:00:00 2001
|
||||
From: Bakkeby <bakkeby@gmail.com>
|
||||
Date: Mon, 18 Jul 2022 16:52:03 +0200
|
||||
Subject: [PATCH] Adding anysize patch
|
||||
|
||||
---
|
||||
x.c | 56 ++++++++++++++++++++++++++++++--------------------------
|
||||
1 file changed, 30 insertions(+), 26 deletions(-)
|
||||
|
||||
diff --git a/x.c b/x.c
|
||||
index 2a3bd38..f534347 100644
|
||||
--- a/x.c
|
||||
+++ b/x.c
|
||||
@@ -81,6 +81,7 @@ typedef XftGlyphFontSpec GlyphFontSpec;
|
||||
typedef struct {
|
||||
int tw, th; /* tty width and height */
|
||||
int w, h; /* window width and height */
|
||||
+ int hborderpx, vborderpx;
|
||||
int ch; /* char height */
|
||||
int cw; /* char width */
|
||||
int mode; /* window state/mode flags */
|
||||
@@ -331,7 +332,7 @@ ttysend(const Arg *arg)
|
||||
int
|
||||
evcol(XEvent *e)
|
||||
{
|
||||
- int x = e->xbutton.x - borderpx;
|
||||
+ int x = e->xbutton.x - win.hborderpx;
|
||||
LIMIT(x, 0, win.tw - 1);
|
||||
return x / win.cw;
|
||||
}
|
||||
@@ -339,7 +340,7 @@ evcol(XEvent *e)
|
||||
int
|
||||
evrow(XEvent *e)
|
||||
{
|
||||
- int y = e->xbutton.y - borderpx;
|
||||
+ int y = e->xbutton.y - win.vborderpx;
|
||||
LIMIT(y, 0, win.th - 1);
|
||||
return y / win.ch;
|
||||
}
|
||||
@@ -739,6 +740,9 @@ cresize(int width, int height)
|
||||
col = MAX(1, col);
|
||||
row = MAX(1, row);
|
||||
|
||||
+ win.hborderpx = (win.w - col * win.cw) / 2;
|
||||
+ win.vborderpx = (win.h - row * win.ch) / 2;
|
||||
+
|
||||
tresize(col, row);
|
||||
xresize(col, row);
|
||||
ttyresize(win.tw, win.th);
|
||||
@@ -869,8 +873,8 @@ xhints(void)
|
||||
sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize;
|
||||
sizeh->height = win.h;
|
||||
sizeh->width = win.w;
|
||||
- sizeh->height_inc = win.ch;
|
||||
- sizeh->width_inc = win.cw;
|
||||
+ sizeh->height_inc = 1;
|
||||
+ sizeh->width_inc = 1;
|
||||
sizeh->base_height = 2 * borderpx;
|
||||
sizeh->base_width = 2 * borderpx;
|
||||
sizeh->min_height = win.ch + 2 * borderpx;
|
||||
@@ -1152,8 +1156,8 @@ xinit(int cols, int rows)
|
||||
xloadcols();
|
||||
|
||||
/* adjust fixed window geometry */
|
||||
- win.w = 2 * borderpx + cols * win.cw;
|
||||
- win.h = 2 * borderpx + rows * win.ch;
|
||||
+ win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw;
|
||||
+ win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch;
|
||||
if (xw.gm & XNegative)
|
||||
xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2;
|
||||
if (xw.gm & YNegative)
|
||||
@@ -1242,7 +1246,7 @@ xinit(int cols, int rows)
|
||||
int
|
||||
xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
|
||||
{
|
||||
- float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
|
||||
+ float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp;
|
||||
ushort mode, prevmode = USHRT_MAX;
|
||||
Font *font = &dc.font;
|
||||
int frcflags = FRC_NORMAL;
|
||||
@@ -1375,7 +1379,7 @@ void
|
||||
xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y)
|
||||
{
|
||||
int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1);
|
||||
- int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch,
|
||||
+ int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch,
|
||||
width = charlen * win.cw;
|
||||
Color *fg, *bg, *temp, revfg, revbg, truefg, truebg;
|
||||
XRenderColor colfg, colbg;
|
||||
@@ -1465,17 +1469,17 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
|
||||
|
||||
/* Intelligent cleaning up of the borders. */
|
||||
if (x == 0) {
|
||||
- xclear(0, (y == 0)? 0 : winy, borderpx,
|
||||
+ xclear(0, (y == 0)? 0 : winy, win.hborderpx,
|
||||
winy + win.ch +
|
||||
- ((winy + win.ch >= borderpx + win.th)? win.h : 0));
|
||||
+ ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0));
|
||||
}
|
||||
- if (winx + width >= borderpx + win.tw) {
|
||||
+ if (winx + width >= win.hborderpx + win.tw) {
|
||||
xclear(winx + width, (y == 0)? 0 : winy, win.w,
|
||||
- ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch)));
|
||||
+ ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch)));
|
||||
}
|
||||
if (y == 0)
|
||||
- xclear(winx, 0, winx + width, borderpx);
|
||||
- if (winy + win.ch >= borderpx + win.th)
|
||||
+ xclear(winx, 0, winx + width, win.vborderpx);
|
||||
+ if (winy + win.ch >= win.vborderpx + win.th)
|
||||
xclear(winx, winy + win.ch, winx + width, win.h);
|
||||
|
||||
/* Clean up the region we want to draw to. */
|
||||
@@ -1569,35 +1573,35 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
|
||||
case 3: /* Blinking Underline */
|
||||
case 4: /* Steady Underline */
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
- borderpx + cx * win.cw,
|
||||
- borderpx + (cy + 1) * win.ch - \
|
||||
+ win.hborderpx + cx * win.cw,
|
||||
+ win.vborderpx + (cy + 1) * win.ch - \
|
||||
cursorthickness,
|
||||
win.cw, cursorthickness);
|
||||
break;
|
||||
case 5: /* Blinking bar */
|
||||
case 6: /* Steady bar */
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
- borderpx + cx * win.cw,
|
||||
- borderpx + cy * win.ch,
|
||||
+ win.hborderpx + cx * win.cw,
|
||||
+ win.vborderpx + cy * win.ch,
|
||||
cursorthickness, win.ch);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
- borderpx + cx * win.cw,
|
||||
- borderpx + cy * win.ch,
|
||||
+ win.hborderpx + cx * win.cw,
|
||||
+ win.vborderpx + cy * win.ch,
|
||||
win.cw - 1, 1);
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
- borderpx + cx * win.cw,
|
||||
- borderpx + cy * win.ch,
|
||||
+ win.hborderpx + cx * win.cw,
|
||||
+ win.vborderpx + cy * win.ch,
|
||||
1, win.ch - 1);
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
- borderpx + (cx + 1) * win.cw - 1,
|
||||
- borderpx + cy * win.ch,
|
||||
+ win.hborderpx + (cx + 1) * win.cw - 1,
|
||||
+ win.vborderpx + cy * win.ch,
|
||||
1, win.ch - 1);
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
- borderpx + cx * win.cw,
|
||||
- borderpx + (cy + 1) * win.ch - 1,
|
||||
+ win.hborderpx + cx * win.cw,
|
||||
+ win.vborderpx + (cy + 1) * win.ch - 1,
|
||||
win.cw, 1);
|
||||
}
|
||||
}
|
||||
--
|
||||
2.37.1
|
||||
|
||||
|
|
@ -1,619 +0,0 @@
|
|||
diff --git a/Makefile b/Makefile
|
||||
index 15db421..dfcea0f 100644
|
||||
--- a/Makefile
|
||||
+++ b/Makefile
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
include config.mk
|
||||
|
||||
-SRC = st.c x.c
|
||||
+SRC = st.c x.c hb.c
|
||||
OBJ = $(SRC:.c=.o)
|
||||
|
||||
all: st
|
||||
@@ -16,7 +16,8 @@ config.h:
|
||||
$(CC) $(STCFLAGS) -c $<
|
||||
|
||||
st.o: config.h st.h win.h
|
||||
-x.o: arg.h config.h st.h win.h
|
||||
+x.o: arg.h config.h st.h win.h hb.h
|
||||
+hb.o: st.h
|
||||
|
||||
$(OBJ): config.h config.mk
|
||||
|
||||
diff --git a/config.mk b/config.mk
|
||||
index 2fc854e..ec69d1a 100644
|
||||
--- a/config.mk
|
||||
+++ b/config.mk
|
||||
@@ -15,10 +15,12 @@ PKG_CONFIG = pkg-config
|
||||
# includes and libs
|
||||
INCS = -I$(X11INC) \
|
||||
`$(PKG_CONFIG) --cflags fontconfig` \
|
||||
- `$(PKG_CONFIG) --cflags freetype2`
|
||||
+ `$(PKG_CONFIG) --cflags freetype2` \
|
||||
+ `$(PKG_CONFIG) --cflags harfbuzz`
|
||||
LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \
|
||||
`$(PKG_CONFIG) --libs fontconfig` \
|
||||
- `$(PKG_CONFIG) --libs freetype2`
|
||||
+ `$(PKG_CONFIG) --libs freetype2` \
|
||||
+ `$(PKG_CONFIG) --libs harfbuzz`
|
||||
|
||||
# flags
|
||||
STCPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600
|
||||
@@ -29,7 +31,8 @@ STLDFLAGS = $(LIBS) $(LDFLAGS)
|
||||
#CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600 -D_BSD_SOURCE
|
||||
#LIBS = -L$(X11LIB) -lm -lX11 -lutil -lXft \
|
||||
# `$(PKG_CONFIG) --libs fontconfig` \
|
||||
-# `$(PKG_CONFIG) --libs freetype2`
|
||||
+# `$(PKG_CONFIG) --libs freetype2` \
|
||||
+# `$(PKG_CONFIG) --libs harfbuzz`
|
||||
#MANPREFIX = ${PREFIX}/man
|
||||
|
||||
# compiler and linker
|
||||
diff --git a/hb.c b/hb.c
|
||||
new file mode 100644
|
||||
index 0000000..99412c8
|
||||
--- /dev/null
|
||||
+++ b/hb.c
|
||||
@@ -0,0 +1,125 @@
|
||||
+#include <stdlib.h>
|
||||
+#include <stdio.h>
|
||||
+#include <math.h>
|
||||
+#include <X11/Xft/Xft.h>
|
||||
+#include <X11/cursorfont.h>
|
||||
+#include <hb.h>
|
||||
+#include <hb-ft.h>
|
||||
+
|
||||
+#include "st.h"
|
||||
+#include "hb.h"
|
||||
+
|
||||
+#define FEATURE(c1,c2,c3,c4) { .tag = HB_TAG(c1,c2,c3,c4), .value = 1, .start = HB_FEATURE_GLOBAL_START, .end = HB_FEATURE_GLOBAL_END }
|
||||
+#define BUFFER_STEP 256
|
||||
+
|
||||
+hb_font_t *hbfindfont(XftFont *match);
|
||||
+
|
||||
+typedef struct {
|
||||
+ XftFont *match;
|
||||
+ hb_font_t *font;
|
||||
+} HbFontMatch;
|
||||
+
|
||||
+typedef struct {
|
||||
+ size_t capacity;
|
||||
+ HbFontMatch *fonts;
|
||||
+} HbFontCache;
|
||||
+
|
||||
+static HbFontCache hbfontcache = { 0, NULL };
|
||||
+
|
||||
+typedef struct {
|
||||
+ size_t capacity;
|
||||
+ Rune *runes;
|
||||
+} RuneBuffer;
|
||||
+
|
||||
+static RuneBuffer hbrunebuffer = { 0, NULL };
|
||||
+
|
||||
+/*
|
||||
+ * Poplulate the array with a list of font features, wrapped in FEATURE macro,
|
||||
+ * e. g.
|
||||
+ * FEATURE('c', 'a', 'l', 't'), FEATURE('d', 'l', 'i', 'g')
|
||||
+ */
|
||||
+hb_feature_t features[] = { };
|
||||
+
|
||||
+void
|
||||
+hbunloadfonts()
|
||||
+{
|
||||
+ for (int i = 0; i < hbfontcache.capacity; i++) {
|
||||
+ hb_font_destroy(hbfontcache.fonts[i].font);
|
||||
+ XftUnlockFace(hbfontcache.fonts[i].match);
|
||||
+ }
|
||||
+
|
||||
+ if (hbfontcache.fonts != NULL) {
|
||||
+ free(hbfontcache.fonts);
|
||||
+ hbfontcache.fonts = NULL;
|
||||
+ }
|
||||
+ hbfontcache.capacity = 0;
|
||||
+}
|
||||
+
|
||||
+hb_font_t *
|
||||
+hbfindfont(XftFont *match)
|
||||
+{
|
||||
+ for (int i = 0; i < hbfontcache.capacity; i++) {
|
||||
+ if (hbfontcache.fonts[i].match == match)
|
||||
+ return hbfontcache.fonts[i].font;
|
||||
+ }
|
||||
+
|
||||
+ /* Font not found in cache, caching it now. */
|
||||
+ hbfontcache.fonts = realloc(hbfontcache.fonts, sizeof(HbFontMatch) * (hbfontcache.capacity + 1));
|
||||
+ FT_Face face = XftLockFace(match);
|
||||
+ hb_font_t *font = hb_ft_font_create(face, NULL);
|
||||
+ if (font == NULL)
|
||||
+ die("Failed to load Harfbuzz font.");
|
||||
+
|
||||
+ hbfontcache.fonts[hbfontcache.capacity].match = match;
|
||||
+ hbfontcache.fonts[hbfontcache.capacity].font = font;
|
||||
+ hbfontcache.capacity += 1;
|
||||
+
|
||||
+ return font;
|
||||
+}
|
||||
+
|
||||
+void hbtransform(HbTransformData *data, XftFont *xfont, const Glyph *glyphs, int start, int length) {
|
||||
+ ushort mode = USHRT_MAX;
|
||||
+ unsigned int glyph_count;
|
||||
+ int rune_idx, glyph_idx, end = start + length;
|
||||
+
|
||||
+ hb_font_t *font = hbfindfont(xfont);
|
||||
+ if (font == NULL)
|
||||
+ return;
|
||||
+
|
||||
+ hb_buffer_t *buffer = hb_buffer_create();
|
||||
+ hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);
|
||||
+ hb_buffer_set_cluster_level(buffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS);
|
||||
+
|
||||
+ /* Resize the buffer if required length is larger. */
|
||||
+ if (hbrunebuffer.capacity < length) {
|
||||
+ hbrunebuffer.capacity = (length / BUFFER_STEP + 1) * BUFFER_STEP;
|
||||
+ hbrunebuffer.runes = realloc(hbrunebuffer.runes, hbrunebuffer.capacity * sizeof(Rune));
|
||||
+ }
|
||||
+
|
||||
+ /* Fill buffer with codepoints. */
|
||||
+ for (rune_idx = 0, glyph_idx = start; glyph_idx < end; glyph_idx++, rune_idx++) {
|
||||
+ hbrunebuffer.runes[rune_idx] = glyphs[glyph_idx].u;
|
||||
+ mode = glyphs[glyph_idx].mode;
|
||||
+ if (mode & ATTR_WDUMMY)
|
||||
+ hbrunebuffer.runes[rune_idx] = 0x0020;
|
||||
+ }
|
||||
+ hb_buffer_add_codepoints(buffer, hbrunebuffer.runes, length, 0, length);
|
||||
+
|
||||
+ /* Shape the segment. */
|
||||
+ hb_shape(font, buffer, features, sizeof(features)/sizeof(hb_feature_t));
|
||||
+
|
||||
+ /* Get new glyph info. */
|
||||
+ hb_glyph_info_t *info = hb_buffer_get_glyph_infos(buffer, &glyph_count);
|
||||
+ hb_glyph_position_t *pos = hb_buffer_get_glyph_positions(buffer, &glyph_count);
|
||||
+
|
||||
+ /* Fill the output. */
|
||||
+ data->buffer = buffer;
|
||||
+ data->glyphs = info;
|
||||
+ data->positions = pos;
|
||||
+ data->count = glyph_count;
|
||||
+}
|
||||
+
|
||||
+void hbcleanup(HbTransformData *data) {
|
||||
+ hb_buffer_destroy(data->buffer);
|
||||
+ memset(data, 0, sizeof(HbTransformData));
|
||||
+}
|
||||
diff --git a/hb.h b/hb.h
|
||||
new file mode 100644
|
||||
index 0000000..3b0ef44
|
||||
--- /dev/null
|
||||
+++ b/hb.h
|
||||
@@ -0,0 +1,14 @@
|
||||
+#include <X11/Xft/Xft.h>
|
||||
+#include <hb.h>
|
||||
+#include <hb-ft.h>
|
||||
+
|
||||
+typedef struct {
|
||||
+ hb_buffer_t *buffer;
|
||||
+ hb_glyph_info_t *glyphs;
|
||||
+ hb_glyph_position_t *positions;
|
||||
+ unsigned int count;
|
||||
+} HbTransformData;
|
||||
+
|
||||
+void hbunloadfonts();
|
||||
+void hbtransform(HbTransformData *, XftFont *, const Glyph *, int, int);
|
||||
+void hbcleanup(HbTransformData *);
|
||||
diff --git a/st.c b/st.c
|
||||
index 8e57991..9641c85 100644
|
||||
--- a/st.c
|
||||
+++ b/st.c
|
||||
@@ -2685,7 +2685,8 @@ draw(void)
|
||||
|
||||
drawregion(0, 0, term.col, term.row);
|
||||
xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
|
||||
- term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
|
||||
+ term.ocx, term.ocy, term.line[term.ocy][term.ocx],
|
||||
+ term.line[term.ocy], term.col);
|
||||
term.ocx = cx;
|
||||
term.ocy = term.c.y;
|
||||
xfinishdraw();
|
||||
diff --git a/st.h b/st.h
|
||||
index fd3b0d8..142fdfe 100644
|
||||
--- a/st.h
|
||||
+++ b/st.h
|
||||
@@ -11,7 +11,8 @@
|
||||
#define DIVCEIL(n, d) (((n) + ((d) - 1)) / (d))
|
||||
#define DEFAULT(a, b) (a) = (a) ? (a) : (b)
|
||||
#define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)
|
||||
-#define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \
|
||||
+#define ATTRCMP(a, b) (((a).mode & (~ATTR_WRAP)) != ((b).mode & (~ATTR_WRAP)) || \
|
||||
+ (a).fg != (b).fg || \
|
||||
(a).bg != (b).bg)
|
||||
#define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \
|
||||
(t1.tv_nsec-t2.tv_nsec)/1E6)
|
||||
diff --git a/win.h b/win.h
|
||||
index 6de960d..94679e4 100644
|
||||
--- a/win.h
|
||||
+++ b/win.h
|
||||
@@ -25,7 +25,7 @@ enum win_mode {
|
||||
|
||||
void xbell(void);
|
||||
void xclipcopy(void);
|
||||
-void xdrawcursor(int, int, Glyph, int, int, Glyph);
|
||||
+void xdrawcursor(int, int, Glyph, int, int, Glyph, Line, int);
|
||||
void xdrawline(Line, int, int, int);
|
||||
void xfinishdraw(void);
|
||||
void xloadcols(void);
|
||||
diff --git a/x.c b/x.c
|
||||
index d73152b..4d05b9f 100644
|
||||
--- a/x.c
|
||||
+++ b/x.c
|
||||
@@ -19,6 +19,7 @@ char *argv0;
|
||||
#include "arg.h"
|
||||
#include "st.h"
|
||||
#include "win.h"
|
||||
+#include "hb.h"
|
||||
|
||||
/* types used in config.h */
|
||||
typedef struct {
|
||||
@@ -141,8 +142,9 @@ typedef struct {
|
||||
} DC;
|
||||
|
||||
static inline ushort sixd_to_16bit(int);
|
||||
+static void xresetfontsettings(ushort mode, Font **font, int *frcflags);
|
||||
static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int);
|
||||
-static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int);
|
||||
+static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int, int);
|
||||
static void xdrawglyph(Glyph, int, int);
|
||||
static void xclear(int, int, int, int);
|
||||
static int xgeommasktogravity(int);
|
||||
@@ -757,7 +759,7 @@ xresize(int col, int row)
|
||||
xclear(0, 0, win.w, win.h);
|
||||
|
||||
/* resize to new width */
|
||||
- xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec));
|
||||
+ xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec) * 4);
|
||||
}
|
||||
|
||||
ushort
|
||||
@@ -1062,6 +1064,9 @@ xunloadfont(Font *f)
|
||||
void
|
||||
xunloadfonts(void)
|
||||
{
|
||||
+ /* Clear Harfbuzz font cache. */
|
||||
+ hbunloadfonts();
|
||||
+
|
||||
/* Free the loaded fonts in the font cache. */
|
||||
while (frclen > 0)
|
||||
XftFontClose(xw.dpy, frc[--frclen].font);
|
||||
@@ -1188,7 +1193,7 @@ xinit(int cols, int rows)
|
||||
XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h);
|
||||
|
||||
/* font spec buffer */
|
||||
- xw.specbuf = xmalloc(cols * sizeof(GlyphFontSpec));
|
||||
+ xw.specbuf = xmalloc(cols * sizeof(GlyphFontSpec) * 4);
|
||||
|
||||
/* Xft rendering context */
|
||||
xw.draw = XftDrawCreate(xw.dpy, xw.buf, xw.vis, xw.cmap);
|
||||
@@ -1242,142 +1247,153 @@ xinit(int cols, int rows)
|
||||
xsel.xtarget = XA_STRING;
|
||||
}
|
||||
|
||||
+void
|
||||
+xresetfontsettings(ushort mode, Font **font, int *frcflags)
|
||||
+{
|
||||
+ *font = &dc.font;
|
||||
+ if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
|
||||
+ *font = &dc.ibfont;
|
||||
+ *frcflags = FRC_ITALICBOLD;
|
||||
+ } else if (mode & ATTR_ITALIC) {
|
||||
+ *font = &dc.ifont;
|
||||
+ *frcflags = FRC_ITALIC;
|
||||
+ } else if (mode & ATTR_BOLD) {
|
||||
+ *font = &dc.bfont;
|
||||
+ *frcflags = FRC_BOLD;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
int
|
||||
xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
|
||||
{
|
||||
float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
|
||||
- ushort mode, prevmode = USHRT_MAX;
|
||||
+ ushort mode = glyphs[0].mode & ~ATTR_WRAP;
|
||||
Font *font = &dc.font;
|
||||
int frcflags = FRC_NORMAL;
|
||||
- float runewidth = win.cw;
|
||||
+ float runewidth = win.cw * ((glyphs[0].mode & ATTR_WIDE) ? 2.0f : 1.0f);
|
||||
Rune rune;
|
||||
FT_UInt glyphidx;
|
||||
FcResult fcres;
|
||||
FcPattern *fcpattern, *fontpattern;
|
||||
FcFontSet *fcsets[] = { NULL };
|
||||
FcCharSet *fccharset;
|
||||
- int i, f, numspecs = 0;
|
||||
+ int f, code_idx, numspecs = 0;
|
||||
+ float cluster_xp = xp, cluster_yp = yp;
|
||||
+ HbTransformData shaped = { 0 };
|
||||
|
||||
- for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) {
|
||||
- /* Fetch rune and mode for current glyph. */
|
||||
- rune = glyphs[i].u;
|
||||
- mode = glyphs[i].mode;
|
||||
+ /* Initial values. */
|
||||
+ xresetfontsettings(mode, &font, &frcflags);
|
||||
|
||||
- /* Skip dummy wide-character spacing. */
|
||||
- if (mode == ATTR_WDUMMY)
|
||||
+ /* Shape the segment. */
|
||||
+ hbtransform(&shaped, font->match, glyphs, 0, len);
|
||||
+ xp = winx; yp = winy + font->ascent;
|
||||
+ cluster_xp = xp; cluster_yp = yp;
|
||||
+
|
||||
+ for (code_idx = 0; code_idx < shaped.count; code_idx++) {
|
||||
+ int idx = shaped.glyphs[code_idx].cluster;
|
||||
+
|
||||
+ if (glyphs[idx].mode & ATTR_WDUMMY)
|
||||
continue;
|
||||
|
||||
- /* Determine font for glyph if different from previous glyph. */
|
||||
- if (prevmode != mode) {
|
||||
- prevmode = mode;
|
||||
- font = &dc.font;
|
||||
- frcflags = FRC_NORMAL;
|
||||
- runewidth = win.cw * ((mode & ATTR_WIDE) ? 2.0f : 1.0f);
|
||||
- if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
|
||||
- font = &dc.ibfont;
|
||||
- frcflags = FRC_ITALICBOLD;
|
||||
- } else if (mode & ATTR_ITALIC) {
|
||||
- font = &dc.ifont;
|
||||
- frcflags = FRC_ITALIC;
|
||||
- } else if (mode & ATTR_BOLD) {
|
||||
- font = &dc.bfont;
|
||||
- frcflags = FRC_BOLD;
|
||||
- }
|
||||
- yp = winy + font->ascent;
|
||||
+ /* Advance the drawing cursor if we've moved to a new cluster */
|
||||
+ if (code_idx > 0 && idx != shaped.glyphs[code_idx - 1].cluster) {
|
||||
+ xp += runewidth * (idx - shaped.glyphs[code_idx - 1].cluster);
|
||||
+ cluster_xp = xp;
|
||||
+ cluster_yp = yp;
|
||||
}
|
||||
|
||||
- /* Lookup character index with default font. */
|
||||
- glyphidx = XftCharIndex(xw.dpy, font->match, rune);
|
||||
- if (glyphidx) {
|
||||
+ if (shaped.glyphs[code_idx].codepoint != 0) {
|
||||
+ /* If symbol is found, put it into the specs. */
|
||||
specs[numspecs].font = font->match;
|
||||
- specs[numspecs].glyph = glyphidx;
|
||||
- specs[numspecs].x = (short)xp;
|
||||
- specs[numspecs].y = (short)yp;
|
||||
- xp += runewidth;
|
||||
+ specs[numspecs].glyph = shaped.glyphs[code_idx].codepoint;
|
||||
+ specs[numspecs].x = cluster_xp + (short)(shaped.positions[code_idx].x_offset / 64.);
|
||||
+ specs[numspecs].y = cluster_yp - (short)(shaped.positions[code_idx].y_offset / 64.);
|
||||
+ cluster_xp += shaped.positions[code_idx].x_advance / 64.;
|
||||
+ cluster_yp += shaped.positions[code_idx].y_advance / 64.;
|
||||
numspecs++;
|
||||
- continue;
|
||||
- }
|
||||
-
|
||||
- /* Fallback on font cache, search the font cache for match. */
|
||||
- for (f = 0; f < frclen; f++) {
|
||||
- glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune);
|
||||
- /* Everything correct. */
|
||||
- if (glyphidx && frc[f].flags == frcflags)
|
||||
- break;
|
||||
- /* We got a default font for a not found glyph. */
|
||||
- if (!glyphidx && frc[f].flags == frcflags
|
||||
- && frc[f].unicodep == rune) {
|
||||
- break;
|
||||
+ } else {
|
||||
+ /* If it's not found, try to fetch it through the font cache. */
|
||||
+ rune = glyphs[idx].u;
|
||||
+ for (f = 0; f < frclen; f++) {
|
||||
+ glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune);
|
||||
+ /* Everything correct. */
|
||||
+ if (glyphidx && frc[f].flags == frcflags)
|
||||
+ break;
|
||||
+ /* We got a default font for a not found glyph. */
|
||||
+ if (!glyphidx && frc[f].flags == frcflags
|
||||
+ && frc[f].unicodep == rune) {
|
||||
+ break;
|
||||
+ }
|
||||
}
|
||||
- }
|
||||
-
|
||||
- /* Nothing was found. Use fontconfig to find matching font. */
|
||||
- if (f >= frclen) {
|
||||
- if (!font->set)
|
||||
- font->set = FcFontSort(0, font->pattern,
|
||||
- 1, 0, &fcres);
|
||||
- fcsets[0] = font->set;
|
||||
|
||||
- /*
|
||||
- * Nothing was found in the cache. Now use
|
||||
- * some dozen of Fontconfig calls to get the
|
||||
- * font for one single character.
|
||||
- *
|
||||
- * Xft and fontconfig are design failures.
|
||||
- */
|
||||
- fcpattern = FcPatternDuplicate(font->pattern);
|
||||
- fccharset = FcCharSetCreate();
|
||||
-
|
||||
- FcCharSetAddChar(fccharset, rune);
|
||||
- FcPatternAddCharSet(fcpattern, FC_CHARSET,
|
||||
- fccharset);
|
||||
- FcPatternAddBool(fcpattern, FC_SCALABLE, 1);
|
||||
-
|
||||
- FcConfigSubstitute(0, fcpattern,
|
||||
- FcMatchPattern);
|
||||
- FcDefaultSubstitute(fcpattern);
|
||||
-
|
||||
- fontpattern = FcFontSetMatch(0, fcsets, 1,
|
||||
- fcpattern, &fcres);
|
||||
-
|
||||
- /* Allocate memory for the new cache entry. */
|
||||
- if (frclen >= frccap) {
|
||||
- frccap += 16;
|
||||
- frc = xrealloc(frc, frccap * sizeof(Fontcache));
|
||||
+ /* Nothing was found. Use fontconfig to find matching font. */
|
||||
+ if (f >= frclen) {
|
||||
+ if (!font->set)
|
||||
+ font->set = FcFontSort(0, font->pattern,
|
||||
+ 1, 0, &fcres);
|
||||
+ fcsets[0] = font->set;
|
||||
+
|
||||
+ /*
|
||||
+ * Nothing was found in the cache. Now use
|
||||
+ * some dozen of Fontconfig calls to get the
|
||||
+ * font for one single character.
|
||||
+ *
|
||||
+ * Xft and fontconfig are design failures.
|
||||
+ */
|
||||
+ fcpattern = FcPatternDuplicate(font->pattern);
|
||||
+ fccharset = FcCharSetCreate();
|
||||
+
|
||||
+ FcCharSetAddChar(fccharset, rune);
|
||||
+ FcPatternAddCharSet(fcpattern, FC_CHARSET,
|
||||
+ fccharset);
|
||||
+ FcPatternAddBool(fcpattern, FC_SCALABLE, 1);
|
||||
+
|
||||
+ FcConfigSubstitute(0, fcpattern,
|
||||
+ FcMatchPattern);
|
||||
+ FcDefaultSubstitute(fcpattern);
|
||||
+
|
||||
+ fontpattern = FcFontSetMatch(0, fcsets, 1,
|
||||
+ fcpattern, &fcres);
|
||||
+
|
||||
+ /* Allocate memory for the new cache entry. */
|
||||
+ if (frclen >= frccap) {
|
||||
+ frccap += 16;
|
||||
+ frc = xrealloc(frc, frccap * sizeof(Fontcache));
|
||||
+ }
|
||||
+
|
||||
+ frc[frclen].font = XftFontOpenPattern(xw.dpy,
|
||||
+ fontpattern);
|
||||
+ if (!frc[frclen].font)
|
||||
+ die("XftFontOpenPattern failed seeking fallback font: %s\n",
|
||||
+ strerror(errno));
|
||||
+ frc[frclen].flags = frcflags;
|
||||
+ frc[frclen].unicodep = rune;
|
||||
+
|
||||
+ glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune);
|
||||
+
|
||||
+ f = frclen;
|
||||
+ frclen++;
|
||||
+
|
||||
+ FcPatternDestroy(fcpattern);
|
||||
+ FcCharSetDestroy(fccharset);
|
||||
}
|
||||
|
||||
- frc[frclen].font = XftFontOpenPattern(xw.dpy,
|
||||
- fontpattern);
|
||||
- if (!frc[frclen].font)
|
||||
- die("XftFontOpenPattern failed seeking fallback font: %s\n",
|
||||
- strerror(errno));
|
||||
- frc[frclen].flags = frcflags;
|
||||
- frc[frclen].unicodep = rune;
|
||||
-
|
||||
- glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune);
|
||||
-
|
||||
- f = frclen;
|
||||
- frclen++;
|
||||
-
|
||||
- FcPatternDestroy(fcpattern);
|
||||
- FcCharSetDestroy(fccharset);
|
||||
+ specs[numspecs].font = frc[f].font;
|
||||
+ specs[numspecs].glyph = glyphidx;
|
||||
+ specs[numspecs].x = (short)xp;
|
||||
+ specs[numspecs].y = (short)yp;
|
||||
+ numspecs++;
|
||||
}
|
||||
-
|
||||
- specs[numspecs].font = frc[f].font;
|
||||
- specs[numspecs].glyph = glyphidx;
|
||||
- specs[numspecs].x = (short)xp;
|
||||
- specs[numspecs].y = (short)yp;
|
||||
- xp += runewidth;
|
||||
- numspecs++;
|
||||
}
|
||||
|
||||
+ /* Cleanup and get ready for next segment. */
|
||||
+ hbcleanup(&shaped);
|
||||
return numspecs;
|
||||
}
|
||||
|
||||
void
|
||||
-xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y)
|
||||
+xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y, int charlen)
|
||||
{
|
||||
- int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1);
|
||||
int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch,
|
||||
width = charlen * win.cw;
|
||||
Color *fg, *bg, *temp, revfg, revbg, truefg, truebg;
|
||||
@@ -1513,21 +1529,24 @@ void
|
||||
xdrawglyph(Glyph g, int x, int y)
|
||||
{
|
||||
int numspecs;
|
||||
- XftGlyphFontSpec spec;
|
||||
+ XftGlyphFontSpec *specs = xw.specbuf;
|
||||
|
||||
- numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y);
|
||||
- xdrawglyphfontspecs(&spec, g, numspecs, x, y);
|
||||
+ numspecs = xmakeglyphfontspecs(specs, &g, 1, x, y);
|
||||
+ xdrawglyphfontspecs(specs, g, numspecs, x, y, (g.mode & ATTR_WIDE) ? 2 : 1);
|
||||
}
|
||||
|
||||
void
|
||||
-xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
|
||||
+xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int len)
|
||||
{
|
||||
Color drawcol;
|
||||
|
||||
/* remove the old cursor */
|
||||
if (selected(ox, oy))
|
||||
og.mode ^= ATTR_REVERSE;
|
||||
- xdrawglyph(og, ox, oy);
|
||||
+
|
||||
+ /* Redraw the line where cursor was previously.
|
||||
+ * It will restore the ligatures broken by the cursor. */
|
||||
+ xdrawline(line, 0, oy, len);
|
||||
|
||||
if (IS_SET(MODE_HIDE))
|
||||
return;
|
||||
@@ -1661,18 +1680,16 @@ xdrawline(Line line, int x1, int y1, int x2)
|
||||
Glyph base, new;
|
||||
XftGlyphFontSpec *specs = xw.specbuf;
|
||||
|
||||
- numspecs = xmakeglyphfontspecs(specs, &line[x1], x2 - x1, x1, y1);
|
||||
i = ox = 0;
|
||||
- for (x = x1; x < x2 && i < numspecs; x++) {
|
||||
+ for (x = x1; x < x2; x++) {
|
||||
new = line[x];
|
||||
if (new.mode == ATTR_WDUMMY)
|
||||
continue;
|
||||
if (selected(x, y1))
|
||||
new.mode ^= ATTR_REVERSE;
|
||||
- if (i > 0 && ATTRCMP(base, new)) {
|
||||
- xdrawglyphfontspecs(specs, base, i, ox, y1);
|
||||
- specs += i;
|
||||
- numspecs -= i;
|
||||
+ if ((i > 0) && ATTRCMP(base, new)) {
|
||||
+ numspecs = xmakeglyphfontspecs(specs, &line[ox], x - ox, ox, y1);
|
||||
+ xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x - ox);
|
||||
i = 0;
|
||||
}
|
||||
if (i == 0) {
|
||||
@@ -1681,8 +1698,10 @@ xdrawline(Line line, int x1, int y1, int x2)
|
||||
}
|
||||
i++;
|
||||
}
|
||||
- if (i > 0)
|
||||
- xdrawglyphfontspecs(specs, base, i, ox, y1);
|
||||
+ if (i > 0) {
|
||||
+ numspecs = xmakeglyphfontspecs(specs, &line[ox], x2 - ox, ox, y1);
|
||||
+ xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x2 - ox);
|
||||
+ }
|
||||
}
|
||||
|
||||
void
|
||||
3
st/st.c
3
st/st.c
|
|
@ -2689,8 +2689,7 @@ draw(void)
|
|||
|
||||
drawregion(0, 0, term.col, term.row);
|
||||
xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
|
||||
term.ocx, term.ocy, term.line[term.ocy][term.ocx],
|
||||
term.line[term.ocy], term.col);
|
||||
term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
|
||||
term.ocx = cx;
|
||||
term.ocy = term.c.y;
|
||||
xfinishdraw();
|
||||
|
|
|
|||
3
st/st.h
3
st/st.h
|
|
@ -11,8 +11,7 @@
|
|||
#define DIVCEIL(n, d) (((n) + ((d) - 1)) / (d))
|
||||
#define DEFAULT(a, b) (a) = (a) ? (a) : (b)
|
||||
#define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)
|
||||
#define ATTRCMP(a, b) (((a).mode & (~ATTR_WRAP)) != ((b).mode & (~ATTR_WRAP)) || \
|
||||
(a).fg != (b).fg || \
|
||||
#define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \
|
||||
(a).bg != (b).bg)
|
||||
#define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \
|
||||
(t1.tv_nsec-t2.tv_nsec)/1E6)
|
||||
|
|
|
|||
2
st/win.h
2
st/win.h
|
|
@ -25,7 +25,7 @@ enum win_mode {
|
|||
|
||||
void xbell(void);
|
||||
void xclipcopy(void);
|
||||
void xdrawcursor(int, int, Glyph, int, int, Glyph, Line, int);
|
||||
void xdrawcursor(int, int, Glyph, int, int, Glyph);
|
||||
void xdrawline(Line, int, int, int);
|
||||
void xfinishdraw(void);
|
||||
void xloadcols(void);
|
||||
|
|
|
|||
365
st/x.c
365
st/x.c
|
|
@ -19,7 +19,6 @@ char *argv0;
|
|||
#include "arg.h"
|
||||
#include "st.h"
|
||||
#include "win.h"
|
||||
#include "hb.h"
|
||||
|
||||
/* types used in config.h */
|
||||
typedef struct {
|
||||
|
|
@ -82,7 +81,6 @@ typedef XftGlyphFontSpec GlyphFontSpec;
|
|||
typedef struct {
|
||||
int tw, th; /* tty width and height */
|
||||
int w, h; /* window width and height */
|
||||
int hborderpx, vborderpx;
|
||||
int ch; /* char height */
|
||||
int cw; /* char width */
|
||||
int mode; /* window state/mode flags */
|
||||
|
|
@ -107,7 +105,6 @@ typedef struct {
|
|||
XSetWindowAttributes attrs;
|
||||
int scr;
|
||||
int isfixed; /* is fixed geometry? */
|
||||
int depth; /* bit depth */
|
||||
int l, t; /* left and top offset */
|
||||
int gm; /* geometry mask */
|
||||
} XWindow;
|
||||
|
|
@ -144,9 +141,8 @@ typedef struct {
|
|||
} DC;
|
||||
|
||||
static inline ushort sixd_to_16bit(int);
|
||||
static void xresetfontsettings(ushort mode, Font **font, int *frcflags);
|
||||
static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int);
|
||||
static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int, int);
|
||||
static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int);
|
||||
static void xdrawglyph(Glyph, int, int);
|
||||
static void xclear(int, int, int, int);
|
||||
static int xgeommasktogravity(int);
|
||||
|
|
@ -335,7 +331,7 @@ ttysend(const Arg *arg)
|
|||
int
|
||||
evcol(XEvent *e)
|
||||
{
|
||||
int x = e->xbutton.x - win.hborderpx;
|
||||
int x = e->xbutton.x - borderpx;
|
||||
LIMIT(x, 0, win.tw - 1);
|
||||
return x / win.cw;
|
||||
}
|
||||
|
|
@ -343,7 +339,7 @@ evcol(XEvent *e)
|
|||
int
|
||||
evrow(XEvent *e)
|
||||
{
|
||||
int y = e->xbutton.y - win.vborderpx;
|
||||
int y = e->xbutton.y - borderpx;
|
||||
LIMIT(y, 0, win.th - 1);
|
||||
return y / win.ch;
|
||||
}
|
||||
|
|
@ -743,9 +739,6 @@ cresize(int width, int height)
|
|||
col = MAX(1, col);
|
||||
row = MAX(1, row);
|
||||
|
||||
win.hborderpx = (win.w - col * win.cw) / 2;
|
||||
win.vborderpx = (win.h - row * win.ch) / 2;
|
||||
|
||||
tresize(col, row);
|
||||
xresize(col, row);
|
||||
ttyresize(win.tw, win.th);
|
||||
|
|
@ -759,12 +752,12 @@ xresize(int col, int row)
|
|||
|
||||
XFreePixmap(xw.dpy, xw.buf);
|
||||
xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h,
|
||||
xw.depth);
|
||||
DefaultDepth(xw.dpy, xw.scr));
|
||||
XftDrawChange(xw.draw, xw.buf);
|
||||
xclear(0, 0, win.w, win.h);
|
||||
|
||||
/* resize to new width */
|
||||
xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec) * 4);
|
||||
xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec));
|
||||
}
|
||||
|
||||
ushort
|
||||
|
|
@ -819,10 +812,6 @@ xloadcols(void)
|
|||
else
|
||||
die("could not allocate color %d\n", i);
|
||||
}
|
||||
|
||||
dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * alpha);
|
||||
dc.col[defaultbg].pixel &= 0x00FFFFFF;
|
||||
dc.col[defaultbg].pixel |= (unsigned char)(0xff * alpha) << 24;
|
||||
loaded = 1;
|
||||
}
|
||||
|
||||
|
|
@ -853,12 +842,6 @@ xsetcolorname(int x, const char *name)
|
|||
XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]);
|
||||
dc.col[x] = ncolor;
|
||||
|
||||
if (x == defaultbg) {
|
||||
dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * alpha);
|
||||
dc.col[defaultbg].pixel &= 0x00FFFFFF;
|
||||
dc.col[defaultbg].pixel |= (unsigned char)(0xff * alpha) << 24;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -886,8 +869,8 @@ xhints(void)
|
|||
sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize;
|
||||
sizeh->height = win.h;
|
||||
sizeh->width = win.w;
|
||||
sizeh->height_inc = 1;
|
||||
sizeh->width_inc = 1;
|
||||
sizeh->height_inc = win.ch;
|
||||
sizeh->width_inc = win.cw;
|
||||
sizeh->base_height = 2 * borderpx;
|
||||
sizeh->base_width = 2 * borderpx;
|
||||
sizeh->min_height = win.ch + 2 * borderpx;
|
||||
|
|
@ -1079,9 +1062,6 @@ xunloadfont(Font *f)
|
|||
void
|
||||
xunloadfonts(void)
|
||||
{
|
||||
/* Clear Harfbuzz font cache. */
|
||||
hbunloadfonts();
|
||||
|
||||
/* Free the loaded fonts in the font cache. */
|
||||
while (frclen > 0)
|
||||
XftFontClose(xw.dpy, frc[--frclen].font);
|
||||
|
|
@ -1154,25 +1134,11 @@ xinit(int cols, int rows)
|
|||
Window parent, root;
|
||||
pid_t thispid = getpid();
|
||||
XColor xmousefg, xmousebg;
|
||||
XWindowAttributes attr;
|
||||
XVisualInfo vis;
|
||||
|
||||
if (!(xw.dpy = XOpenDisplay(NULL)))
|
||||
die("can't open display\n");
|
||||
xw.scr = XDefaultScreen(xw.dpy);
|
||||
|
||||
root = XRootWindow(xw.dpy, xw.scr);
|
||||
if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0))))
|
||||
parent = root;
|
||||
|
||||
if (XMatchVisualInfo(xw.dpy, xw.scr, 32, TrueColor, &vis) != 0) {
|
||||
xw.vis = vis.visual;
|
||||
xw.depth = vis.depth;
|
||||
} else {
|
||||
XGetWindowAttributes(xw.dpy, parent, &attr);
|
||||
xw.vis = attr.visual;
|
||||
xw.depth = attr.depth;
|
||||
}
|
||||
xw.vis = XDefaultVisual(xw.dpy, xw.scr);
|
||||
|
||||
/* font */
|
||||
if (!FcInit())
|
||||
|
|
@ -1182,12 +1148,12 @@ xinit(int cols, int rows)
|
|||
xloadfonts(usedfont, 0);
|
||||
|
||||
/* colors */
|
||||
xw.cmap = XCreateColormap(xw.dpy, parent, xw.vis, None);
|
||||
xw.cmap = XDefaultColormap(xw.dpy, xw.scr);
|
||||
xloadcols();
|
||||
|
||||
/* adjust fixed window geometry */
|
||||
win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw;
|
||||
win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch;
|
||||
win.w = 2 * borderpx + cols * win.cw;
|
||||
win.h = 2 * borderpx + rows * win.ch;
|
||||
if (xw.gm & XNegative)
|
||||
xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2;
|
||||
if (xw.gm & YNegative)
|
||||
|
|
@ -1202,8 +1168,11 @@ xinit(int cols, int rows)
|
|||
| ButtonMotionMask | ButtonPressMask | ButtonReleaseMask;
|
||||
xw.attrs.colormap = xw.cmap;
|
||||
|
||||
xw.win = XCreateWindow(xw.dpy, parent, xw.l, xw.t,
|
||||
win.w, win.h, 0, xw.depth, InputOutput,
|
||||
root = XRootWindow(xw.dpy, xw.scr);
|
||||
if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0))))
|
||||
parent = root;
|
||||
xw.win = XCreateWindow(xw.dpy, root, xw.l, xw.t,
|
||||
win.w, win.h, 0, XDefaultDepth(xw.dpy, xw.scr), InputOutput,
|
||||
xw.vis, CWBackPixel | CWBorderPixel | CWBitGravity
|
||||
| CWEventMask | CWColormap, &xw.attrs);
|
||||
if (parent != root)
|
||||
|
|
@ -1214,12 +1183,12 @@ xinit(int cols, int rows)
|
|||
dc.gc = XCreateGC(xw.dpy, xw.win, GCGraphicsExposures,
|
||||
&gcvalues);
|
||||
xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h,
|
||||
xw.depth);
|
||||
DefaultDepth(xw.dpy, xw.scr));
|
||||
XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel);
|
||||
XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h);
|
||||
|
||||
/* font spec buffer */
|
||||
xw.specbuf = xmalloc(cols * sizeof(GlyphFontSpec) * 4);
|
||||
xw.specbuf = xmalloc(cols * sizeof(GlyphFontSpec));
|
||||
|
||||
/* Xft rendering context */
|
||||
xw.draw = XftDrawCreate(xw.dpy, xw.buf, xw.vis, xw.cmap);
|
||||
|
|
@ -1273,154 +1242,143 @@ xinit(int cols, int rows)
|
|||
xsel.xtarget = XA_STRING;
|
||||
}
|
||||
|
||||
void
|
||||
xresetfontsettings(ushort mode, Font **font, int *frcflags)
|
||||
{
|
||||
*font = &dc.font;
|
||||
if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
|
||||
*font = &dc.ibfont;
|
||||
*frcflags = FRC_ITALICBOLD;
|
||||
} else if (mode & ATTR_ITALIC) {
|
||||
*font = &dc.ifont;
|
||||
*frcflags = FRC_ITALIC;
|
||||
} else if (mode & ATTR_BOLD) {
|
||||
*font = &dc.bfont;
|
||||
*frcflags = FRC_BOLD;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
|
||||
{
|
||||
float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp;
|
||||
ushort mode = glyphs[0].mode & ~ATTR_WRAP;
|
||||
float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
|
||||
ushort mode, prevmode = USHRT_MAX;
|
||||
Font *font = &dc.font;
|
||||
int frcflags = FRC_NORMAL;
|
||||
float runewidth = win.cw * ((glyphs[0].mode & ATTR_WIDE) ? 2.0f : 1.0f);
|
||||
float runewidth = win.cw;
|
||||
Rune rune;
|
||||
FT_UInt glyphidx;
|
||||
FcResult fcres;
|
||||
FcPattern *fcpattern, *fontpattern;
|
||||
FcFontSet *fcsets[] = { NULL };
|
||||
FcCharSet *fccharset;
|
||||
int f, code_idx, numspecs = 0;
|
||||
float cluster_xp = xp, cluster_yp = yp;
|
||||
HbTransformData shaped = { 0 };
|
||||
int i, f, numspecs = 0;
|
||||
|
||||
/* Initial values. */
|
||||
xresetfontsettings(mode, &font, &frcflags);
|
||||
for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) {
|
||||
/* Fetch rune and mode for current glyph. */
|
||||
rune = glyphs[i].u;
|
||||
mode = glyphs[i].mode;
|
||||
|
||||
/* Shape the segment. */
|
||||
hbtransform(&shaped, font->match, glyphs, 0, len);
|
||||
xp = winx; yp = winy + font->ascent;
|
||||
cluster_xp = xp; cluster_yp = yp;
|
||||
|
||||
for (code_idx = 0; code_idx < shaped.count; code_idx++) {
|
||||
int idx = shaped.glyphs[code_idx].cluster;
|
||||
|
||||
if (glyphs[idx].mode & ATTR_WDUMMY)
|
||||
/* Skip dummy wide-character spacing. */
|
||||
if (mode == ATTR_WDUMMY)
|
||||
continue;
|
||||
|
||||
/* Advance the drawing cursor if we've moved to a new cluster */
|
||||
if (code_idx > 0 && idx != shaped.glyphs[code_idx - 1].cluster) {
|
||||
xp += runewidth * (idx - shaped.glyphs[code_idx - 1].cluster);
|
||||
cluster_xp = xp;
|
||||
cluster_yp = yp;
|
||||
/* Determine font for glyph if different from previous glyph. */
|
||||
if (prevmode != mode) {
|
||||
prevmode = mode;
|
||||
font = &dc.font;
|
||||
frcflags = FRC_NORMAL;
|
||||
runewidth = win.cw * ((mode & ATTR_WIDE) ? 2.0f : 1.0f);
|
||||
if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
|
||||
font = &dc.ibfont;
|
||||
frcflags = FRC_ITALICBOLD;
|
||||
} else if (mode & ATTR_ITALIC) {
|
||||
font = &dc.ifont;
|
||||
frcflags = FRC_ITALIC;
|
||||
} else if (mode & ATTR_BOLD) {
|
||||
font = &dc.bfont;
|
||||
frcflags = FRC_BOLD;
|
||||
}
|
||||
yp = winy + font->ascent;
|
||||
}
|
||||
|
||||
if (shaped.glyphs[code_idx].codepoint != 0) {
|
||||
/* If symbol is found, put it into the specs. */
|
||||
/* Lookup character index with default font. */
|
||||
glyphidx = XftCharIndex(xw.dpy, font->match, rune);
|
||||
if (glyphidx) {
|
||||
specs[numspecs].font = font->match;
|
||||
specs[numspecs].glyph = shaped.glyphs[code_idx].codepoint;
|
||||
specs[numspecs].x = cluster_xp + (short)(shaped.positions[code_idx].x_offset / 64.);
|
||||
specs[numspecs].y = cluster_yp - (short)(shaped.positions[code_idx].y_offset / 64.);
|
||||
cluster_xp += shaped.positions[code_idx].x_advance / 64.;
|
||||
cluster_yp += shaped.positions[code_idx].y_advance / 64.;
|
||||
numspecs++;
|
||||
} else {
|
||||
/* If it's not found, try to fetch it through the font cache. */
|
||||
rune = glyphs[idx].u;
|
||||
for (f = 0; f < frclen; f++) {
|
||||
glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune);
|
||||
/* Everything correct. */
|
||||
if (glyphidx && frc[f].flags == frcflags)
|
||||
break;
|
||||
/* We got a default font for a not found glyph. */
|
||||
if (!glyphidx && frc[f].flags == frcflags
|
||||
&& frc[f].unicodep == rune) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nothing was found. Use fontconfig to find matching font. */
|
||||
if (f >= frclen) {
|
||||
if (!font->set)
|
||||
font->set = FcFontSort(0, font->pattern,
|
||||
1, 0, &fcres);
|
||||
fcsets[0] = font->set;
|
||||
|
||||
/*
|
||||
* Nothing was found in the cache. Now use
|
||||
* some dozen of Fontconfig calls to get the
|
||||
* font for one single character.
|
||||
*
|
||||
* Xft and fontconfig are design failures.
|
||||
*/
|
||||
fcpattern = FcPatternDuplicate(font->pattern);
|
||||
fccharset = FcCharSetCreate();
|
||||
|
||||
FcCharSetAddChar(fccharset, rune);
|
||||
FcPatternAddCharSet(fcpattern, FC_CHARSET,
|
||||
fccharset);
|
||||
FcPatternAddBool(fcpattern, FC_SCALABLE, 1);
|
||||
|
||||
FcConfigSubstitute(0, fcpattern,
|
||||
FcMatchPattern);
|
||||
FcDefaultSubstitute(fcpattern);
|
||||
|
||||
fontpattern = FcFontSetMatch(0, fcsets, 1,
|
||||
fcpattern, &fcres);
|
||||
|
||||
/* Allocate memory for the new cache entry. */
|
||||
if (frclen >= frccap) {
|
||||
frccap += 16;
|
||||
frc = xrealloc(frc, frccap * sizeof(Fontcache));
|
||||
}
|
||||
|
||||
frc[frclen].font = XftFontOpenPattern(xw.dpy,
|
||||
fontpattern);
|
||||
if (!frc[frclen].font)
|
||||
die("XftFontOpenPattern failed seeking fallback font: %s\n",
|
||||
strerror(errno));
|
||||
frc[frclen].flags = frcflags;
|
||||
frc[frclen].unicodep = rune;
|
||||
|
||||
glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune);
|
||||
|
||||
f = frclen;
|
||||
frclen++;
|
||||
|
||||
FcPatternDestroy(fcpattern);
|
||||
FcCharSetDestroy(fccharset);
|
||||
}
|
||||
|
||||
specs[numspecs].font = frc[f].font;
|
||||
specs[numspecs].glyph = glyphidx;
|
||||
specs[numspecs].x = (short)xp;
|
||||
specs[numspecs].y = (short)yp;
|
||||
xp += runewidth;
|
||||
numspecs++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Fallback on font cache, search the font cache for match. */
|
||||
for (f = 0; f < frclen; f++) {
|
||||
glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune);
|
||||
/* Everything correct. */
|
||||
if (glyphidx && frc[f].flags == frcflags)
|
||||
break;
|
||||
/* We got a default font for a not found glyph. */
|
||||
if (!glyphidx && frc[f].flags == frcflags
|
||||
&& frc[f].unicodep == rune) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nothing was found. Use fontconfig to find matching font. */
|
||||
if (f >= frclen) {
|
||||
if (!font->set)
|
||||
font->set = FcFontSort(0, font->pattern,
|
||||
1, 0, &fcres);
|
||||
fcsets[0] = font->set;
|
||||
|
||||
/*
|
||||
* Nothing was found in the cache. Now use
|
||||
* some dozen of Fontconfig calls to get the
|
||||
* font for one single character.
|
||||
*
|
||||
* Xft and fontconfig are design failures.
|
||||
*/
|
||||
fcpattern = FcPatternDuplicate(font->pattern);
|
||||
fccharset = FcCharSetCreate();
|
||||
|
||||
FcCharSetAddChar(fccharset, rune);
|
||||
FcPatternAddCharSet(fcpattern, FC_CHARSET,
|
||||
fccharset);
|
||||
FcPatternAddBool(fcpattern, FC_SCALABLE, 1);
|
||||
|
||||
FcConfigSubstitute(0, fcpattern,
|
||||
FcMatchPattern);
|
||||
FcDefaultSubstitute(fcpattern);
|
||||
|
||||
fontpattern = FcFontSetMatch(0, fcsets, 1,
|
||||
fcpattern, &fcres);
|
||||
|
||||
/* Allocate memory for the new cache entry. */
|
||||
if (frclen >= frccap) {
|
||||
frccap += 16;
|
||||
frc = xrealloc(frc, frccap * sizeof(Fontcache));
|
||||
}
|
||||
|
||||
frc[frclen].font = XftFontOpenPattern(xw.dpy,
|
||||
fontpattern);
|
||||
if (!frc[frclen].font)
|
||||
die("XftFontOpenPattern failed seeking fallback font: %s\n",
|
||||
strerror(errno));
|
||||
frc[frclen].flags = frcflags;
|
||||
frc[frclen].unicodep = rune;
|
||||
|
||||
glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune);
|
||||
|
||||
f = frclen;
|
||||
frclen++;
|
||||
|
||||
FcPatternDestroy(fcpattern);
|
||||
FcCharSetDestroy(fccharset);
|
||||
}
|
||||
|
||||
specs[numspecs].font = frc[f].font;
|
||||
specs[numspecs].glyph = glyphidx;
|
||||
specs[numspecs].x = (short)xp;
|
||||
specs[numspecs].y = (short)yp;
|
||||
xp += runewidth;
|
||||
numspecs++;
|
||||
}
|
||||
|
||||
/* Cleanup and get ready for next segment. */
|
||||
hbcleanup(&shaped);
|
||||
return numspecs;
|
||||
}
|
||||
|
||||
void
|
||||
xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y, int charlen)
|
||||
xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y)
|
||||
{
|
||||
int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch,
|
||||
int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1);
|
||||
int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch,
|
||||
width = charlen * win.cw;
|
||||
Color *fg, *bg, *temp, revfg, revbg, truefg, truebg;
|
||||
XRenderColor colfg, colbg;
|
||||
|
|
@ -1510,17 +1468,17 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
|
|||
|
||||
/* Intelligent cleaning up of the borders. */
|
||||
if (x == 0) {
|
||||
xclear(0, (y == 0)? 0 : winy, win.hborderpx,
|
||||
xclear(0, (y == 0)? 0 : winy, borderpx,
|
||||
winy + win.ch +
|
||||
((winy + win.ch >= win.vborderpx + win.th)? win.h : 0));
|
||||
((winy + win.ch >= borderpx + win.th)? win.h : 0));
|
||||
}
|
||||
if (winx + width >= win.hborderpx + win.tw) {
|
||||
if (winx + width >= borderpx + win.tw) {
|
||||
xclear(winx + width, (y == 0)? 0 : winy, win.w,
|
||||
((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch)));
|
||||
((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch)));
|
||||
}
|
||||
if (y == 0)
|
||||
xclear(winx, 0, winx + width, win.vborderpx);
|
||||
if (winy + win.ch >= win.vborderpx + win.th)
|
||||
xclear(winx, 0, winx + width, borderpx);
|
||||
if (winy + win.ch >= borderpx + win.th)
|
||||
xclear(winx, winy + win.ch, winx + width, win.h);
|
||||
|
||||
/* Clean up the region we want to draw to. */
|
||||
|
|
@ -1555,24 +1513,21 @@ void
|
|||
xdrawglyph(Glyph g, int x, int y)
|
||||
{
|
||||
int numspecs;
|
||||
XftGlyphFontSpec *specs = xw.specbuf;
|
||||
XftGlyphFontSpec spec;
|
||||
|
||||
numspecs = xmakeglyphfontspecs(specs, &g, 1, x, y);
|
||||
xdrawglyphfontspecs(specs, g, numspecs, x, y, (g.mode & ATTR_WIDE) ? 2 : 1);
|
||||
numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y);
|
||||
xdrawglyphfontspecs(&spec, g, numspecs, x, y);
|
||||
}
|
||||
|
||||
void
|
||||
xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int len)
|
||||
xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
|
||||
{
|
||||
Color drawcol;
|
||||
|
||||
/* remove the old cursor */
|
||||
if (selected(ox, oy))
|
||||
og.mode ^= ATTR_REVERSE;
|
||||
|
||||
/* Redraw the line where cursor was previously.
|
||||
* It will restore the ligatures broken by the cursor. */
|
||||
xdrawline(line, 0, oy, len);
|
||||
xdrawglyph(og, ox, oy);
|
||||
|
||||
if (IS_SET(MODE_HIDE))
|
||||
return;
|
||||
|
|
@ -1617,35 +1572,35 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int le
|
|||
case 3: /* Blinking Underline */
|
||||
case 4: /* Steady Underline */
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
win.hborderpx + cx * win.cw,
|
||||
win.vborderpx + (cy + 1) * win.ch - \
|
||||
borderpx + cx * win.cw,
|
||||
borderpx + (cy + 1) * win.ch - \
|
||||
cursorthickness,
|
||||
win.cw, cursorthickness);
|
||||
break;
|
||||
case 5: /* Blinking bar */
|
||||
case 6: /* Steady bar */
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
win.hborderpx + cx * win.cw,
|
||||
win.vborderpx + cy * win.ch,
|
||||
borderpx + cx * win.cw,
|
||||
borderpx + cy * win.ch,
|
||||
cursorthickness, win.ch);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
win.hborderpx + cx * win.cw,
|
||||
win.vborderpx + cy * win.ch,
|
||||
borderpx + cx * win.cw,
|
||||
borderpx + cy * win.ch,
|
||||
win.cw - 1, 1);
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
win.hborderpx + cx * win.cw,
|
||||
win.vborderpx + cy * win.ch,
|
||||
borderpx + cx * win.cw,
|
||||
borderpx + cy * win.ch,
|
||||
1, win.ch - 1);
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
win.hborderpx + (cx + 1) * win.cw - 1,
|
||||
win.vborderpx + cy * win.ch,
|
||||
borderpx + (cx + 1) * win.cw - 1,
|
||||
borderpx + cy * win.ch,
|
||||
1, win.ch - 1);
|
||||
XftDrawRect(xw.draw, &drawcol,
|
||||
win.hborderpx + cx * win.cw,
|
||||
win.vborderpx + (cy + 1) * win.ch - 1,
|
||||
borderpx + cx * win.cw,
|
||||
borderpx + (cy + 1) * win.ch - 1,
|
||||
win.cw, 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1706,16 +1661,18 @@ xdrawline(Line line, int x1, int y1, int x2)
|
|||
Glyph base, new;
|
||||
XftGlyphFontSpec *specs = xw.specbuf;
|
||||
|
||||
numspecs = xmakeglyphfontspecs(specs, &line[x1], x2 - x1, x1, y1);
|
||||
i = ox = 0;
|
||||
for (x = x1; x < x2; x++) {
|
||||
for (x = x1; x < x2 && i < numspecs; x++) {
|
||||
new = line[x];
|
||||
if (new.mode == ATTR_WDUMMY)
|
||||
continue;
|
||||
if (selected(x, y1))
|
||||
new.mode ^= ATTR_REVERSE;
|
||||
if ((i > 0) && ATTRCMP(base, new)) {
|
||||
numspecs = xmakeglyphfontspecs(specs, &line[ox], x - ox, ox, y1);
|
||||
xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x - ox);
|
||||
if (i > 0 && ATTRCMP(base, new)) {
|
||||
xdrawglyphfontspecs(specs, base, i, ox, y1);
|
||||
specs += i;
|
||||
numspecs -= i;
|
||||
i = 0;
|
||||
}
|
||||
if (i == 0) {
|
||||
|
|
@ -1724,10 +1681,8 @@ xdrawline(Line line, int x1, int y1, int x2)
|
|||
}
|
||||
i++;
|
||||
}
|
||||
if (i > 0) {
|
||||
numspecs = xmakeglyphfontspecs(specs, &line[ox], x2 - ox, ox, y1);
|
||||
xdrawglyphfontspecs(specs, base, numspecs, ox, y1, x2 - ox);
|
||||
}
|
||||
if (i > 0)
|
||||
xdrawglyphfontspecs(specs, base, i, ox, y1);
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -1746,8 +1701,8 @@ xximspot(int x, int y)
|
|||
if (xw.ime.xic == NULL)
|
||||
return;
|
||||
|
||||
xw.ime.spot.x = win.hborderpx + x * win.cw;
|
||||
xw.ime.spot.y = win.vborderpx + (y + 1) * win.ch;
|
||||
xw.ime.spot.x = borderpx + x * win.cw;
|
||||
xw.ime.spot.y = borderpx + (y + 1) * win.ch;
|
||||
|
||||
XSetICValues(xw.ime.xic, XNPreeditAttributes, xw.ime.spotlist, NULL);
|
||||
}
|
||||
|
|
@ -2092,10 +2047,6 @@ main(int argc, char *argv[])
|
|||
case 'a':
|
||||
allowaltscreen = 0;
|
||||
break;
|
||||
case 'A':
|
||||
alpha = strtof(EARGF(usage()), NULL);
|
||||
LIMIT(alpha, 0.0, 1.0);
|
||||
break;
|
||||
case 'c':
|
||||
opt_class = EARGF(usage());
|
||||
break;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue