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

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