Mover los programas a suckless/
Primer paso para convertir el repo en dotfiles: los programas de suckless van juntos en una carpeta y la raíz queda para la configuración de la sesión y el script de instalación. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016UdJsHrbkmzhoCb6TthP2m
This commit is contained in:
parent
ce3a0db5bf
commit
fbda573897
148 changed files with 14 additions and 14 deletions
11
suckless/clipmenu/.clang-format
Normal file
11
suckless/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
suckless/clipmenu/.github/workflows/ci.yml
vendored
Normal file
28
suckless/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
suckless/clipmenu/.gitignore
vendored
Normal file
9
suckless/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
suckless/clipmenu/LICENSE
Normal file
21
suckless/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
suckless/clipmenu/Makefile
Normal file
135
suckless/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
suckless/clipmenu/README.md
Normal file
97
suckless/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
suckless/clipmenu/init/clipmenud.service.in
Normal file
21
suckless/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
suckless/clipmenu/man/clipctl.1
Normal file
56
suckless/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
suckless/clipmenu/man/clipdel.1
Normal file
53
suckless/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
suckless/clipmenu/man/clipdelmenu.1
Normal file
43
suckless/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
suckless/clipmenu/man/clipmenu.1
Normal file
42
suckless/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
suckless/clipmenu/man/clipmenu.conf.5
Normal file
124
suckless/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
suckless/clipmenu/man/clipmenud.1
Normal file
32
suckless/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
suckless/clipmenu/man/clipserve.1
Normal file
44
suckless/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
suckless/clipmenu/src/clipctl.c
Normal file
152
suckless/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
suckless/clipmenu/src/clipdel.c
Normal file
162
suckless/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
suckless/clipmenu/src/clipdelmenu.c
Normal file
45
suckless/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
suckless/clipmenu/src/clipmenu.c
Normal file
42
suckless/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
suckless/clipmenu/src/clipmenud.c
Normal file
765
suckless/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
suckless/clipmenu/src/clipserve.c
Normal file
286
suckless/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
suckless/clipmenu/src/config.c
Normal file
500
suckless/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
suckless/clipmenu/src/config.h
Normal file
104
suckless/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
suckless/clipmenu/src/menu_util.c
Normal file
302
suckless/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
suckless/clipmenu/src/menu_util.h
Normal file
18
suckless/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
suckless/clipmenu/src/store.c
Normal file
847
suckless/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
suckless/clipmenu/src/store.h
Normal file
192
suckless/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
suckless/clipmenu/src/util.c
Normal file
165
suckless/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
suckless/clipmenu/src/util.h
Normal file
111
suckless/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
suckless/clipmenu/src/x.c
Normal file
121
suckless/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
suckless/clipmenu/src/x.h
Normal file
34
suckless/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
suckless/clipmenu/tests/test_store.c
Normal file
848
suckless/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
suckless/clipmenu/tests/x_integration_tests
Executable file
350
suckless/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
|
||||
30
suckless/dmenu/LICENSE
Normal file
30
suckless/dmenu/LICENSE
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
MIT/X Consortium License
|
||||
|
||||
© 2006-2019 Anselm R Garbe <anselm@garbe.ca>
|
||||
© 2006-2008 Sander van Dijk <a.h.vandijk@gmail.com>
|
||||
© 2006-2007 Michał Janeczek <janeczek@gmail.com>
|
||||
© 2007 Kris Maglione <jg@suckless.org>
|
||||
© 2009 Gottox <gottox@s01.de>
|
||||
© 2009 Markus Schnalke <meillo@marmaro.de>
|
||||
© 2009 Evan Gates <evan.gates@gmail.com>
|
||||
© 2010-2012 Connor Lane Smith <cls@lubutu.com>
|
||||
© 2014-2022 Hiltjo Posthuma <hiltjo@codemadness.org>
|
||||
© 2015-2019 Quentin Rameau <quinq@fifth.space>
|
||||
|
||||
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.
|
||||
58
suckless/dmenu/Makefile
Normal file
58
suckless/dmenu/Makefile
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# dmenu - dynamic menu
|
||||
# See LICENSE file for copyright and license details.
|
||||
|
||||
include config.mk
|
||||
|
||||
SRC = drw.c dmenu.c stest.c util.c
|
||||
OBJ = $(SRC:.c=.o)
|
||||
|
||||
all: dmenu stest
|
||||
|
||||
.c.o:
|
||||
$(CC) -c $(CFLAGS) $<
|
||||
|
||||
config.h:
|
||||
cp config.def.h $@
|
||||
|
||||
$(OBJ): arg.h config.h config.mk drw.h
|
||||
|
||||
dmenu: dmenu.o drw.o util.o
|
||||
$(CC) -o $@ dmenu.o drw.o util.o $(LDFLAGS)
|
||||
|
||||
stest: stest.o
|
||||
$(CC) -o $@ stest.o $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f dmenu stest $(OBJ) dmenu-$(VERSION).tar.gz
|
||||
|
||||
dist: clean
|
||||
mkdir -p dmenu-$(VERSION)
|
||||
cp LICENSE Makefile README arg.h config.def.h config.mk dmenu.1\
|
||||
drw.h util.h dmenu_path dmenu_run stest.1 $(SRC)\
|
||||
dmenu-$(VERSION)
|
||||
tar -cf dmenu-$(VERSION).tar dmenu-$(VERSION)
|
||||
gzip dmenu-$(VERSION).tar
|
||||
rm -rf dmenu-$(VERSION)
|
||||
|
||||
install: all
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/bin
|
||||
cp -f dmenu dmenu_path dmenu_run stest $(DESTDIR)$(PREFIX)/bin
|
||||
chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu
|
||||
chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu_path
|
||||
chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu_run
|
||||
chmod 755 $(DESTDIR)$(PREFIX)/bin/stest
|
||||
mkdir -p $(DESTDIR)$(MANPREFIX)/man1
|
||||
sed "s/VERSION/$(VERSION)/g" < dmenu.1 > $(DESTDIR)$(MANPREFIX)/man1/dmenu.1
|
||||
sed "s/VERSION/$(VERSION)/g" < stest.1 > $(DESTDIR)$(MANPREFIX)/man1/stest.1
|
||||
chmod 644 $(DESTDIR)$(MANPREFIX)/man1/dmenu.1
|
||||
chmod 644 $(DESTDIR)$(MANPREFIX)/man1/stest.1
|
||||
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)$(PREFIX)/bin/dmenu\
|
||||
$(DESTDIR)$(PREFIX)/bin/dmenu_path\
|
||||
$(DESTDIR)$(PREFIX)/bin/dmenu_run\
|
||||
$(DESTDIR)$(PREFIX)/bin/stest\
|
||||
$(DESTDIR)$(MANPREFIX)/man1/dmenu.1\
|
||||
$(DESTDIR)$(MANPREFIX)/man1/stest.1
|
||||
|
||||
.PHONY: all clean dist install uninstall
|
||||
24
suckless/dmenu/README
Normal file
24
suckless/dmenu/README
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
dmenu - dynamic menu
|
||||
====================
|
||||
dmenu is an efficient dynamic menu for X.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
In order to build dmenu you need the Xlib header files.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
Edit config.mk to match your local setup (dmenu is installed into
|
||||
the /usr/local namespace by default).
|
||||
|
||||
Afterwards enter the following command to build and install dmenu
|
||||
(if necessary as root):
|
||||
|
||||
make clean install
|
||||
|
||||
|
||||
Running dmenu
|
||||
-------------
|
||||
See the man page for details.
|
||||
49
suckless/dmenu/arg.h
Normal file
49
suckless/dmenu/arg.h
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copy me if you can.
|
||||
* by 20h
|
||||
*/
|
||||
|
||||
#ifndef ARG_H__
|
||||
#define ARG_H__
|
||||
|
||||
extern char *argv0;
|
||||
|
||||
/* use main(int argc, char *argv[]) */
|
||||
#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\
|
||||
argv[0] && argv[0][0] == '-'\
|
||||
&& argv[0][1];\
|
||||
argc--, argv++) {\
|
||||
char argc_;\
|
||||
char **argv_;\
|
||||
int brk_;\
|
||||
if (argv[0][1] == '-' && argv[0][2] == '\0') {\
|
||||
argv++;\
|
||||
argc--;\
|
||||
break;\
|
||||
}\
|
||||
for (brk_ = 0, argv[0]++, argv_ = argv;\
|
||||
argv[0][0] && !brk_;\
|
||||
argv[0]++) {\
|
||||
if (argv_ != argv)\
|
||||
break;\
|
||||
argc_ = argv[0][0];\
|
||||
switch (argc_)
|
||||
|
||||
#define ARGEND }\
|
||||
}
|
||||
|
||||
#define ARGC() argc_
|
||||
|
||||
#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\
|
||||
((x), abort(), (char *)0) :\
|
||||
(brk_ = 1, (argv[0][1] != '\0')?\
|
||||
(&argv[0][1]) :\
|
||||
(argc--, argv++, argv[0])))
|
||||
|
||||
#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\
|
||||
(char *)0 :\
|
||||
(brk_ = 1, (argv[0][1] != '\0')?\
|
||||
(&argv[0][1]) :\
|
||||
(argc--, argv++, argv[0])))
|
||||
|
||||
#endif
|
||||
23
suckless/dmenu/config.def.h
Normal file
23
suckless/dmenu/config.def.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
/* Default settings; can be overriden by command line. */
|
||||
|
||||
static int topbar = 1; /* -b option; if 0, dmenu appears at bottom */
|
||||
/* -fn option overrides fonts[0]; default X11 font or font set */
|
||||
static const char *fonts[] = {
|
||||
"monospace:size=10"
|
||||
};
|
||||
static const char *prompt = NULL; /* -p option; prompt to the left of input field */
|
||||
static const char *colors[SchemeLast][2] = {
|
||||
/* fg bg */
|
||||
[SchemeNorm] = { "#bbbbbb", "#222222" },
|
||||
[SchemeSel] = { "#eeeeee", "#005577" },
|
||||
[SchemeOut] = { "#000000", "#00ffff" },
|
||||
};
|
||||
/* -l option; if nonzero, dmenu uses vertical list with given number of lines */
|
||||
static unsigned int lines = 0;
|
||||
|
||||
/*
|
||||
* Characters not considered part of a word while deleting words
|
||||
* for example: " /?\"&[]"
|
||||
*/
|
||||
static const char worddelimiters[] = " ";
|
||||
23
suckless/dmenu/config.h
Normal file
23
suckless/dmenu/config.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
/* Default settings; can be overriden by command line. */
|
||||
|
||||
static int topbar = 1; /* -b option; if 0, dmenu appears at bottom */
|
||||
/* -fn option overrides fonts[0]; default X11 font or font set */
|
||||
static const char *fonts[] = {
|
||||
"Fira Code:size=11:antialias=true:autohint=true"
|
||||
};
|
||||
static const char *prompt = NULL; /* -p option; prompt to the left of input field */
|
||||
static const char *colors[SchemeLast][2] = {
|
||||
/* fg bg */
|
||||
[SchemeNorm] = { "#d3c6aa", "#2d353b" }, /* Everforest: texto / fondo */
|
||||
[SchemeSel] = { "#2d353b", "#a7c080" }, /* texto sobre verde acento */
|
||||
[SchemeOut] = { "#2d353b", "#7fbbb3" }, /* azul Everforest */
|
||||
};
|
||||
/* -l option; if nonzero, dmenu uses vertical list with given number of lines */
|
||||
static unsigned int lines = 0;
|
||||
|
||||
/*
|
||||
* Characters not considered part of a word while deleting words
|
||||
* for example: " /?\"&[]"
|
||||
*/
|
||||
static const char worddelimiters[] = " ";
|
||||
32
suckless/dmenu/config.mk
Normal file
32
suckless/dmenu/config.mk
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# dmenu version
|
||||
VERSION = 5.4
|
||||
|
||||
# paths
|
||||
PREFIX = /usr/local
|
||||
MANPREFIX = $(PREFIX)/share/man
|
||||
|
||||
X11INC = /usr/X11R6/include
|
||||
X11LIB = /usr/X11R6/lib
|
||||
|
||||
# Xinerama, comment if you don't want it
|
||||
XINERAMALIBS = -lXinerama
|
||||
XINERAMAFLAGS = -DXINERAMA
|
||||
|
||||
# freetype
|
||||
FREETYPELIBS = -lfontconfig -lXft
|
||||
FREETYPEINC = /usr/include/freetype2
|
||||
# OpenBSD (uncomment)
|
||||
#FREETYPEINC = $(X11INC)/freetype2
|
||||
#MANPREFIX = ${PREFIX}/man
|
||||
|
||||
# includes and libs
|
||||
INCS = -I$(X11INC) -I$(FREETYPEINC)
|
||||
LIBS = -L$(X11LIB) -lX11 $(XINERAMALIBS) $(FREETYPELIBS)
|
||||
|
||||
# flags
|
||||
CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\" $(XINERAMAFLAGS)
|
||||
CFLAGS = -std=c99 -pedantic -Wall -Os $(INCS) $(CPPFLAGS)
|
||||
LDFLAGS = $(LIBS)
|
||||
|
||||
# compiler and linker
|
||||
CC = cc
|
||||
204
suckless/dmenu/dmenu.1
Normal file
204
suckless/dmenu/dmenu.1
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
.TH DMENU 1 dmenu\-VERSION
|
||||
.SH NAME
|
||||
dmenu \- dynamic menu
|
||||
.SH SYNOPSIS
|
||||
.B dmenu
|
||||
.RB [ \-bfiv ]
|
||||
.RB [ \-l
|
||||
.IR lines ]
|
||||
.RB [ \-m
|
||||
.IR monitor ]
|
||||
.RB [ \-p
|
||||
.IR prompt ]
|
||||
.RB [ \-fn
|
||||
.IR font ]
|
||||
.RB [ \-nb
|
||||
.IR color ]
|
||||
.RB [ \-nf
|
||||
.IR color ]
|
||||
.RB [ \-sb
|
||||
.IR color ]
|
||||
.RB [ \-sf
|
||||
.IR color ]
|
||||
.RB [ \-ob
|
||||
.IR color ]
|
||||
.RB [ \-of
|
||||
.IR color ]
|
||||
.RB [ \-w
|
||||
.IR windowid ]
|
||||
.P
|
||||
.BR dmenu_run " ..."
|
||||
.SH DESCRIPTION
|
||||
.B dmenu
|
||||
is a dynamic menu for X, which reads a list of newline\-separated items from
|
||||
stdin. When the user selects an item and presses Return, their choice is printed
|
||||
to stdout and dmenu terminates. Entering text will narrow the items to those
|
||||
matching the tokens in the input.
|
||||
.P
|
||||
.B dmenu_run
|
||||
is a script used by
|
||||
.IR dwm (1)
|
||||
which lists programs in the user's $PATH and runs the result in their $SHELL.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-b
|
||||
dmenu appears at the bottom of the screen.
|
||||
.TP
|
||||
.B \-f
|
||||
dmenu grabs the keyboard before reading stdin if not reading from a tty. This
|
||||
is faster, but will lock up X until stdin reaches end\-of\-file.
|
||||
.TP
|
||||
.B \-i
|
||||
dmenu matches menu items case insensitively.
|
||||
.TP
|
||||
.BI \-l " lines"
|
||||
dmenu lists items vertically, with the given number of lines.
|
||||
.TP
|
||||
.BI \-m " monitor"
|
||||
dmenu is displayed on the monitor number supplied. Monitor numbers are starting
|
||||
from 0.
|
||||
.TP
|
||||
.BI \-p " prompt"
|
||||
defines the prompt to be displayed to the left of the input field.
|
||||
.TP
|
||||
.BI \-fn " font"
|
||||
defines the font or font set used.
|
||||
.TP
|
||||
.BI \-nb " color"
|
||||
defines the normal background color.
|
||||
.IR #RGB ,
|
||||
.IR #RRGGBB ,
|
||||
and X color names are supported.
|
||||
.TP
|
||||
.BI \-nf " color"
|
||||
defines the normal foreground color.
|
||||
.TP
|
||||
.BI \-sb " color"
|
||||
defines the selected background color.
|
||||
.TP
|
||||
.BI \-sf " color"
|
||||
defines the selected foreground color.
|
||||
.TP
|
||||
.BI \-ob " color"
|
||||
defines the outline background color (for multiple selection).
|
||||
.TP
|
||||
.BI \-of " color"
|
||||
defines the outline foreground color (for multiple selection).
|
||||
.TP
|
||||
.B \-v
|
||||
prints version information to stdout, then exits.
|
||||
.TP
|
||||
.BI \-w " windowid"
|
||||
embed into windowid.
|
||||
.SH USAGE
|
||||
dmenu is completely controlled by the keyboard. Items are selected using the
|
||||
arrow keys, page up, page down, home, and end.
|
||||
.TP
|
||||
.B Tab
|
||||
Copy the selected item to the input field.
|
||||
.TP
|
||||
.B Return
|
||||
Confirm selection. Prints the selected item to stdout and exits, returning
|
||||
success.
|
||||
.TP
|
||||
.B Ctrl-Return
|
||||
Confirm selection. Prints the selected item to stdout and continues.
|
||||
.TP
|
||||
.B Shift\-Return
|
||||
Confirm input. Prints the input text to stdout and exits, returning success.
|
||||
.TP
|
||||
.B Escape
|
||||
Exit without selecting an item, returning failure.
|
||||
.TP
|
||||
.B Ctrl-Left
|
||||
Move cursor to the start of the current word
|
||||
.TP
|
||||
.B Ctrl-Right
|
||||
Move cursor to the end of the current word
|
||||
.TP
|
||||
.B C\-a
|
||||
Home
|
||||
.TP
|
||||
.B C\-b
|
||||
Left
|
||||
.TP
|
||||
.B C\-c
|
||||
Escape
|
||||
.TP
|
||||
.B C\-d
|
||||
Delete
|
||||
.TP
|
||||
.B C\-e
|
||||
End
|
||||
.TP
|
||||
.B C\-f
|
||||
Right
|
||||
.TP
|
||||
.B C\-g
|
||||
Escape
|
||||
.TP
|
||||
.B C\-h
|
||||
Backspace
|
||||
.TP
|
||||
.B C\-i
|
||||
Tab
|
||||
.TP
|
||||
.B C\-j
|
||||
Return
|
||||
.TP
|
||||
.B C\-J
|
||||
Shift-Return
|
||||
.TP
|
||||
.B C\-k
|
||||
Delete line right
|
||||
.TP
|
||||
.B C\-m
|
||||
Return
|
||||
.TP
|
||||
.B C\-M
|
||||
Shift-Return
|
||||
.TP
|
||||
.B C\-n
|
||||
Down
|
||||
.TP
|
||||
.B C\-p
|
||||
Up
|
||||
.TP
|
||||
.B C\-u
|
||||
Delete line left
|
||||
.TP
|
||||
.B C\-w
|
||||
Delete word left
|
||||
.TP
|
||||
.B C\-y
|
||||
Paste from primary X selection
|
||||
.TP
|
||||
.B C\-Y
|
||||
Paste from X clipboard
|
||||
.TP
|
||||
.B M\-b
|
||||
Move cursor to the start of the current word
|
||||
.TP
|
||||
.B M\-f
|
||||
Move cursor to the end of the current word
|
||||
.TP
|
||||
.B M\-g
|
||||
Home
|
||||
.TP
|
||||
.B M\-G
|
||||
End
|
||||
.TP
|
||||
.B M\-h
|
||||
Up
|
||||
.TP
|
||||
.B M\-j
|
||||
Page down
|
||||
.TP
|
||||
.B M\-k
|
||||
Page up
|
||||
.TP
|
||||
.B M\-l
|
||||
Down
|
||||
.SH SEE ALSO
|
||||
.IR dwm (1),
|
||||
.IR stest (1)
|
||||
799
suckless/dmenu/dmenu.c
Normal file
799
suckless/dmenu/dmenu.c
Normal file
|
|
@ -0,0 +1,799 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#include <ctype.h>
|
||||
#include <locale.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xutil.h>
|
||||
#ifdef XINERAMA
|
||||
#include <X11/extensions/Xinerama.h>
|
||||
#endif
|
||||
#include <X11/Xft/Xft.h>
|
||||
|
||||
#include "drw.h"
|
||||
#include "util.h"
|
||||
|
||||
/* macros */
|
||||
#define INTERSECT(x,y,w,h,r) (MAX(0, MIN((x)+(w),(r).x_org+(r).width) - MAX((x),(r).x_org)) \
|
||||
* MAX(0, MIN((y)+(h),(r).y_org+(r).height) - MAX((y),(r).y_org)))
|
||||
#define TEXTW(X) (drw_fontset_getwidth(drw, (X)) + lrpad)
|
||||
|
||||
/* enums */
|
||||
enum { SchemeNorm, SchemeSel, SchemeOut, SchemeLast }; /* color schemes */
|
||||
|
||||
struct item {
|
||||
char *text;
|
||||
struct item *left, *right;
|
||||
int out;
|
||||
};
|
||||
|
||||
static char text[BUFSIZ] = "";
|
||||
static char *embed;
|
||||
static int bh, mw, mh;
|
||||
static int inputw = 0, promptw;
|
||||
static int lrpad; /* sum of left and right padding */
|
||||
static size_t cursor;
|
||||
static struct item *items = NULL;
|
||||
static struct item *matches, *matchend;
|
||||
static struct item *prev, *curr, *next, *sel;
|
||||
static int mon = -1, screen;
|
||||
|
||||
static Atom clip, utf8;
|
||||
static Display *dpy;
|
||||
static Window root, parentwin, win;
|
||||
static XIC xic;
|
||||
|
||||
static Drw *drw;
|
||||
static Clr *scheme[SchemeLast];
|
||||
|
||||
#include "config.h"
|
||||
|
||||
static int (*fstrncmp)(const char *, const char *, size_t) = strncmp;
|
||||
static char *(*fstrstr)(const char *, const char *) = strstr;
|
||||
|
||||
static unsigned int
|
||||
textw_clamp(const char *str, unsigned int n)
|
||||
{
|
||||
unsigned int w = drw_fontset_getwidth_clamp(drw, str, n) + lrpad;
|
||||
return MIN(w, n);
|
||||
}
|
||||
|
||||
static void
|
||||
appenditem(struct item *item, struct item **list, struct item **last)
|
||||
{
|
||||
if (*last)
|
||||
(*last)->right = item;
|
||||
else
|
||||
*list = item;
|
||||
|
||||
item->left = *last;
|
||||
item->right = NULL;
|
||||
*last = item;
|
||||
}
|
||||
|
||||
static void
|
||||
calcoffsets(void)
|
||||
{
|
||||
int i, n;
|
||||
|
||||
if (lines > 0)
|
||||
n = lines * bh;
|
||||
else
|
||||
n = mw - (promptw + inputw + TEXTW("<") + TEXTW(">"));
|
||||
/* calculate which items will begin the next page and previous page */
|
||||
for (i = 0, next = curr; next; next = next->right)
|
||||
if ((i += (lines > 0) ? bh : textw_clamp(next->text, n)) > n)
|
||||
break;
|
||||
for (i = 0, prev = curr; prev && prev->left; prev = prev->left)
|
||||
if ((i += (lines > 0) ? bh : textw_clamp(prev->left->text, n)) > n)
|
||||
break;
|
||||
}
|
||||
|
||||
static void
|
||||
cleanup(void)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
XUngrabKeyboard(dpy, CurrentTime);
|
||||
for (i = 0; i < SchemeLast; i++)
|
||||
drw_scm_free(drw, scheme[i], 2);
|
||||
for (i = 0; items && items[i].text; ++i)
|
||||
free(items[i].text);
|
||||
free(items);
|
||||
drw_free(drw);
|
||||
XSync(dpy, False);
|
||||
XCloseDisplay(dpy);
|
||||
}
|
||||
|
||||
static char *
|
||||
cistrstr(const char *h, const char *n)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (!n[0])
|
||||
return (char *)h;
|
||||
|
||||
for (; *h; ++h) {
|
||||
for (i = 0; n[i] && tolower((unsigned char)n[i]) ==
|
||||
tolower((unsigned char)h[i]); ++i)
|
||||
;
|
||||
if (n[i] == '\0')
|
||||
return (char *)h;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int
|
||||
drawitem(struct item *item, int x, int y, int w)
|
||||
{
|
||||
if (item == sel)
|
||||
drw_setscheme(drw, scheme[SchemeSel]);
|
||||
else if (item->out)
|
||||
drw_setscheme(drw, scheme[SchemeOut]);
|
||||
else
|
||||
drw_setscheme(drw, scheme[SchemeNorm]);
|
||||
|
||||
return drw_text(drw, x, y, w, bh, lrpad / 2, item->text, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
drawmenu(void)
|
||||
{
|
||||
unsigned int curpos;
|
||||
struct item *item;
|
||||
int x = 0, y = 0, w;
|
||||
|
||||
drw_setscheme(drw, scheme[SchemeNorm]);
|
||||
drw_rect(drw, 0, 0, mw, mh, 1, 1);
|
||||
|
||||
if (prompt && *prompt) {
|
||||
drw_setscheme(drw, scheme[SchemeSel]);
|
||||
x = drw_text(drw, x, 0, promptw, bh, lrpad / 2, prompt, 0);
|
||||
}
|
||||
/* draw input field */
|
||||
w = (lines > 0 || !matches) ? mw - x : inputw;
|
||||
drw_setscheme(drw, scheme[SchemeNorm]);
|
||||
drw_text(drw, x, 0, w, bh, lrpad / 2, text, 0);
|
||||
|
||||
curpos = TEXTW(text) - TEXTW(&text[cursor]);
|
||||
if ((curpos += lrpad / 2 - 1) < w) {
|
||||
drw_setscheme(drw, scheme[SchemeNorm]);
|
||||
drw_rect(drw, x + curpos, 2, 2, bh - 4, 1, 0);
|
||||
}
|
||||
|
||||
if (lines > 0) {
|
||||
/* draw vertical list */
|
||||
for (item = curr; item != next; item = item->right)
|
||||
drawitem(item, x, y += bh, mw - x);
|
||||
} else if (matches) {
|
||||
/* draw horizontal list */
|
||||
x += inputw;
|
||||
w = TEXTW("<");
|
||||
if (curr->left) {
|
||||
drw_setscheme(drw, scheme[SchemeNorm]);
|
||||
drw_text(drw, x, 0, w, bh, lrpad / 2, "<", 0);
|
||||
}
|
||||
x += w;
|
||||
for (item = curr; item != next; item = item->right)
|
||||
x = drawitem(item, x, 0, textw_clamp(item->text, mw - x - TEXTW(">")));
|
||||
if (next) {
|
||||
w = TEXTW(">");
|
||||
drw_setscheme(drw, scheme[SchemeNorm]);
|
||||
drw_text(drw, mw - w, 0, w, bh, lrpad / 2, ">", 0);
|
||||
}
|
||||
}
|
||||
drw_map(drw, win, 0, 0, mw, mh);
|
||||
}
|
||||
|
||||
static void
|
||||
grabfocus(void)
|
||||
{
|
||||
struct timespec ts = { .tv_sec = 0, .tv_nsec = 10000000 };
|
||||
Window focuswin;
|
||||
int i, revertwin;
|
||||
|
||||
for (i = 0; i < 100; ++i) {
|
||||
XGetInputFocus(dpy, &focuswin, &revertwin);
|
||||
if (focuswin == win)
|
||||
return;
|
||||
XSetInputFocus(dpy, win, RevertToParent, CurrentTime);
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
die("cannot grab focus");
|
||||
}
|
||||
|
||||
static void
|
||||
grabkeyboard(void)
|
||||
{
|
||||
struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 };
|
||||
int i;
|
||||
|
||||
if (embed)
|
||||
return;
|
||||
/* try to grab keyboard, we may have to wait for another process to ungrab */
|
||||
for (i = 0; i < 1000; i++) {
|
||||
if (XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync,
|
||||
GrabModeAsync, CurrentTime) == GrabSuccess)
|
||||
return;
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
die("cannot grab keyboard");
|
||||
}
|
||||
|
||||
static void
|
||||
match(void)
|
||||
{
|
||||
static char **tokv = NULL;
|
||||
static int tokn = 0;
|
||||
|
||||
char buf[sizeof text], *s;
|
||||
int i, tokc = 0;
|
||||
size_t len, textsize;
|
||||
struct item *item, *lprefix, *lsubstr, *prefixend, *substrend;
|
||||
|
||||
strcpy(buf, text);
|
||||
/* separate input text into tokens to be matched individually */
|
||||
for (s = strtok(buf, " "); s; tokv[tokc - 1] = s, s = strtok(NULL, " "))
|
||||
if (++tokc > tokn && !(tokv = realloc(tokv, ++tokn * sizeof *tokv)))
|
||||
die("cannot realloc %zu bytes:", tokn * sizeof *tokv);
|
||||
len = tokc ? strlen(tokv[0]) : 0;
|
||||
|
||||
matches = lprefix = lsubstr = matchend = prefixend = substrend = NULL;
|
||||
textsize = strlen(text) + 1;
|
||||
for (item = items; item && item->text; item++) {
|
||||
for (i = 0; i < tokc; i++)
|
||||
if (!fstrstr(item->text, tokv[i]))
|
||||
break;
|
||||
if (i != tokc) /* not all tokens match */
|
||||
continue;
|
||||
/* exact matches go first, then prefixes, then substrings */
|
||||
if (!tokc || !fstrncmp(text, item->text, textsize))
|
||||
appenditem(item, &matches, &matchend);
|
||||
else if (!fstrncmp(tokv[0], item->text, len))
|
||||
appenditem(item, &lprefix, &prefixend);
|
||||
else
|
||||
appenditem(item, &lsubstr, &substrend);
|
||||
}
|
||||
if (lprefix) {
|
||||
if (matches) {
|
||||
matchend->right = lprefix;
|
||||
lprefix->left = matchend;
|
||||
} else
|
||||
matches = lprefix;
|
||||
matchend = prefixend;
|
||||
}
|
||||
if (lsubstr) {
|
||||
if (matches) {
|
||||
matchend->right = lsubstr;
|
||||
lsubstr->left = matchend;
|
||||
} else
|
||||
matches = lsubstr;
|
||||
matchend = substrend;
|
||||
}
|
||||
curr = sel = matches;
|
||||
calcoffsets();
|
||||
}
|
||||
|
||||
static void
|
||||
insert(const char *str, ssize_t n)
|
||||
{
|
||||
if (strlen(text) + n > sizeof text - 1)
|
||||
return;
|
||||
/* move existing text out of the way, insert new text, and update cursor */
|
||||
memmove(&text[cursor + n], &text[cursor], sizeof text - cursor - MAX(n, 0));
|
||||
if (n > 0)
|
||||
memcpy(&text[cursor], str, n);
|
||||
cursor += n;
|
||||
match();
|
||||
}
|
||||
|
||||
static size_t
|
||||
nextrune(int inc)
|
||||
{
|
||||
ssize_t n;
|
||||
|
||||
/* return location of next utf8 rune in the given direction (+1 or -1) */
|
||||
for (n = cursor + inc; n + inc >= 0 && (text[n] & 0xc0) == 0x80; n += inc)
|
||||
;
|
||||
return n;
|
||||
}
|
||||
|
||||
static void
|
||||
movewordedge(int dir)
|
||||
{
|
||||
if (dir < 0) { /* move cursor to the start of the word*/
|
||||
while (cursor > 0 && strchr(worddelimiters, text[nextrune(-1)]))
|
||||
cursor = nextrune(-1);
|
||||
while (cursor > 0 && !strchr(worddelimiters, text[nextrune(-1)]))
|
||||
cursor = nextrune(-1);
|
||||
} else { /* move cursor to the end of the word */
|
||||
while (text[cursor] && strchr(worddelimiters, text[cursor]))
|
||||
cursor = nextrune(+1);
|
||||
while (text[cursor] && !strchr(worddelimiters, text[cursor]))
|
||||
cursor = nextrune(+1);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
keypress(XKeyEvent *ev)
|
||||
{
|
||||
char buf[64];
|
||||
int len;
|
||||
KeySym ksym = NoSymbol;
|
||||
Status status;
|
||||
|
||||
len = XmbLookupString(xic, ev, buf, sizeof buf, &ksym, &status);
|
||||
switch (status) {
|
||||
default: /* XLookupNone, XBufferOverflow */
|
||||
return;
|
||||
case XLookupChars: /* composed string from input method */
|
||||
goto insert;
|
||||
case XLookupKeySym:
|
||||
case XLookupBoth: /* a KeySym and a string are returned: use keysym */
|
||||
break;
|
||||
}
|
||||
|
||||
if (ev->state & ControlMask) {
|
||||
switch(ksym) {
|
||||
case XK_a: ksym = XK_Home; break;
|
||||
case XK_b: ksym = XK_Left; break;
|
||||
case XK_c: ksym = XK_Escape; break;
|
||||
case XK_d: ksym = XK_Delete; break;
|
||||
case XK_e: ksym = XK_End; break;
|
||||
case XK_f: ksym = XK_Right; break;
|
||||
case XK_g: ksym = XK_Escape; break;
|
||||
case XK_h: ksym = XK_BackSpace; break;
|
||||
case XK_i: ksym = XK_Tab; break;
|
||||
case XK_j: /* fallthrough */
|
||||
case XK_J: /* fallthrough */
|
||||
case XK_m: /* fallthrough */
|
||||
case XK_M: ksym = XK_Return; ev->state &= ~ControlMask; break;
|
||||
case XK_n: ksym = XK_Down; break;
|
||||
case XK_p: ksym = XK_Up; break;
|
||||
|
||||
case XK_k: /* delete right */
|
||||
text[cursor] = '\0';
|
||||
match();
|
||||
break;
|
||||
case XK_u: /* delete left */
|
||||
insert(NULL, 0 - cursor);
|
||||
break;
|
||||
case XK_w: /* delete word */
|
||||
while (cursor > 0 && strchr(worddelimiters, text[nextrune(-1)]))
|
||||
insert(NULL, nextrune(-1) - cursor);
|
||||
while (cursor > 0 && !strchr(worddelimiters, text[nextrune(-1)]))
|
||||
insert(NULL, nextrune(-1) - cursor);
|
||||
break;
|
||||
case XK_y: /* paste selection */
|
||||
case XK_Y:
|
||||
XConvertSelection(dpy, (ev->state & ShiftMask) ? clip : XA_PRIMARY,
|
||||
utf8, utf8, win, CurrentTime);
|
||||
return;
|
||||
case XK_Left:
|
||||
case XK_KP_Left:
|
||||
movewordedge(-1);
|
||||
goto draw;
|
||||
case XK_Right:
|
||||
case XK_KP_Right:
|
||||
movewordedge(+1);
|
||||
goto draw;
|
||||
case XK_Return:
|
||||
case XK_KP_Enter:
|
||||
break;
|
||||
case XK_bracketleft:
|
||||
cleanup();
|
||||
exit(1);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
} else if (ev->state & Mod1Mask) {
|
||||
switch(ksym) {
|
||||
case XK_b:
|
||||
movewordedge(-1);
|
||||
goto draw;
|
||||
case XK_f:
|
||||
movewordedge(+1);
|
||||
goto draw;
|
||||
case XK_g: ksym = XK_Home; break;
|
||||
case XK_G: ksym = XK_End; break;
|
||||
case XK_h: ksym = XK_Up; break;
|
||||
case XK_j: ksym = XK_Next; break;
|
||||
case XK_k: ksym = XK_Prior; break;
|
||||
case XK_l: ksym = XK_Down; break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch(ksym) {
|
||||
default:
|
||||
insert:
|
||||
if (!iscntrl((unsigned char)*buf))
|
||||
insert(buf, len);
|
||||
break;
|
||||
case XK_Delete:
|
||||
case XK_KP_Delete:
|
||||
if (text[cursor] == '\0')
|
||||
return;
|
||||
cursor = nextrune(+1);
|
||||
/* fallthrough */
|
||||
case XK_BackSpace:
|
||||
if (cursor == 0)
|
||||
return;
|
||||
insert(NULL, nextrune(-1) - cursor);
|
||||
break;
|
||||
case XK_End:
|
||||
case XK_KP_End:
|
||||
if (text[cursor] != '\0') {
|
||||
cursor = strlen(text);
|
||||
break;
|
||||
}
|
||||
if (next) {
|
||||
/* jump to end of list and position items in reverse */
|
||||
curr = matchend;
|
||||
calcoffsets();
|
||||
curr = prev;
|
||||
calcoffsets();
|
||||
while (next && (curr = curr->right))
|
||||
calcoffsets();
|
||||
}
|
||||
sel = matchend;
|
||||
break;
|
||||
case XK_Escape:
|
||||
cleanup();
|
||||
exit(1);
|
||||
case XK_Home:
|
||||
case XK_KP_Home:
|
||||
if (sel == matches) {
|
||||
cursor = 0;
|
||||
break;
|
||||
}
|
||||
sel = curr = matches;
|
||||
calcoffsets();
|
||||
break;
|
||||
case XK_Left:
|
||||
case XK_KP_Left:
|
||||
if (cursor > 0 && (!sel || !sel->left || lines > 0)) {
|
||||
cursor = nextrune(-1);
|
||||
break;
|
||||
}
|
||||
if (lines > 0)
|
||||
return;
|
||||
/* fallthrough */
|
||||
case XK_Up:
|
||||
case XK_KP_Up:
|
||||
if (sel && sel->left && (sel = sel->left)->right == curr) {
|
||||
curr = prev;
|
||||
calcoffsets();
|
||||
}
|
||||
break;
|
||||
case XK_Next:
|
||||
case XK_KP_Next:
|
||||
if (!next)
|
||||
return;
|
||||
sel = curr = next;
|
||||
calcoffsets();
|
||||
break;
|
||||
case XK_Prior:
|
||||
case XK_KP_Prior:
|
||||
if (!prev)
|
||||
return;
|
||||
sel = curr = prev;
|
||||
calcoffsets();
|
||||
break;
|
||||
case XK_Return:
|
||||
case XK_KP_Enter:
|
||||
puts((sel && !(ev->state & ShiftMask)) ? sel->text : text);
|
||||
if (!(ev->state & ControlMask)) {
|
||||
cleanup();
|
||||
exit(0);
|
||||
}
|
||||
if (sel)
|
||||
sel->out = 1;
|
||||
break;
|
||||
case XK_Right:
|
||||
case XK_KP_Right:
|
||||
if (text[cursor] != '\0') {
|
||||
cursor = nextrune(+1);
|
||||
break;
|
||||
}
|
||||
if (lines > 0)
|
||||
return;
|
||||
/* fallthrough */
|
||||
case XK_Down:
|
||||
case XK_KP_Down:
|
||||
if (sel && sel->right && (sel = sel->right) == next) {
|
||||
curr = next;
|
||||
calcoffsets();
|
||||
}
|
||||
break;
|
||||
case XK_Tab:
|
||||
if (!sel)
|
||||
return;
|
||||
cursor = strnlen(sel->text, sizeof text - 1);
|
||||
memcpy(text, sel->text, cursor);
|
||||
text[cursor] = '\0';
|
||||
match();
|
||||
break;
|
||||
}
|
||||
|
||||
draw:
|
||||
drawmenu();
|
||||
}
|
||||
|
||||
static void
|
||||
paste(void)
|
||||
{
|
||||
char *p, *q;
|
||||
int di;
|
||||
unsigned long dl;
|
||||
Atom da;
|
||||
|
||||
/* we have been given the current selection, now insert it into input */
|
||||
if (XGetWindowProperty(dpy, win, utf8, 0, (sizeof text / 4) + 1, False,
|
||||
utf8, &da, &di, &dl, &dl, (unsigned char **)&p)
|
||||
== Success && p) {
|
||||
insert(p, (q = strchr(p, '\n')) ? q - p : (ssize_t)strlen(p));
|
||||
XFree(p);
|
||||
}
|
||||
drawmenu();
|
||||
}
|
||||
|
||||
static void
|
||||
readstdin(void)
|
||||
{
|
||||
char *line = NULL;
|
||||
size_t i, itemsiz = 0, linesiz = 0;
|
||||
ssize_t len;
|
||||
|
||||
/* read each line from stdin and add it to the item list */
|
||||
for (i = 0; (len = getline(&line, &linesiz, stdin)) != -1; i++) {
|
||||
if (i + 1 >= itemsiz) {
|
||||
itemsiz += 256;
|
||||
if (!(items = realloc(items, itemsiz * sizeof(*items))))
|
||||
die("cannot realloc %zu bytes:", itemsiz * sizeof(*items));
|
||||
}
|
||||
if (line[len - 1] == '\n')
|
||||
line[len - 1] = '\0';
|
||||
if (!(items[i].text = strdup(line)))
|
||||
die("strdup:");
|
||||
|
||||
items[i].out = 0;
|
||||
}
|
||||
free(line);
|
||||
if (items)
|
||||
items[i].text = NULL;
|
||||
lines = MIN(lines, i);
|
||||
}
|
||||
|
||||
static void
|
||||
run(void)
|
||||
{
|
||||
XEvent ev;
|
||||
|
||||
while (!XNextEvent(dpy, &ev)) {
|
||||
if (XFilterEvent(&ev, win))
|
||||
continue;
|
||||
switch(ev.type) {
|
||||
case DestroyNotify:
|
||||
if (ev.xdestroywindow.window != win)
|
||||
break;
|
||||
cleanup();
|
||||
exit(1);
|
||||
case Expose:
|
||||
if (ev.xexpose.count == 0)
|
||||
drw_map(drw, win, 0, 0, mw, mh);
|
||||
break;
|
||||
case FocusIn:
|
||||
/* regrab focus from parent window */
|
||||
if (ev.xfocus.window != win)
|
||||
grabfocus();
|
||||
break;
|
||||
case KeyPress:
|
||||
keypress(&ev.xkey);
|
||||
break;
|
||||
case SelectionNotify:
|
||||
if (ev.xselection.property == utf8)
|
||||
paste();
|
||||
break;
|
||||
case VisibilityNotify:
|
||||
if (ev.xvisibility.state != VisibilityUnobscured)
|
||||
XRaiseWindow(dpy, win);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
setup(void)
|
||||
{
|
||||
int x, y, i, j;
|
||||
unsigned int du;
|
||||
XSetWindowAttributes swa;
|
||||
XIM xim;
|
||||
Window w, dw, *dws;
|
||||
XWindowAttributes wa;
|
||||
XClassHint ch = {"dmenu", "dmenu"};
|
||||
#ifdef XINERAMA
|
||||
XineramaScreenInfo *info;
|
||||
Window pw;
|
||||
int a, di, n, area = 0;
|
||||
#endif
|
||||
/* init appearance */
|
||||
for (j = 0; j < SchemeLast; j++)
|
||||
scheme[j] = drw_scm_create(drw, colors[j], 2);
|
||||
|
||||
clip = XInternAtom(dpy, "CLIPBOARD", False);
|
||||
utf8 = XInternAtom(dpy, "UTF8_STRING", False);
|
||||
|
||||
/* calculate menu geometry */
|
||||
bh = drw->fonts->h + 2;
|
||||
lines = MAX(lines, 0);
|
||||
mh = (lines + 1) * bh;
|
||||
#ifdef XINERAMA
|
||||
i = 0;
|
||||
if (parentwin == root && (info = XineramaQueryScreens(dpy, &n))) {
|
||||
XGetInputFocus(dpy, &w, &di);
|
||||
if (mon >= 0 && mon < n)
|
||||
i = mon;
|
||||
else if (w != root && w != PointerRoot && w != None) {
|
||||
/* find top-level window containing current input focus */
|
||||
do {
|
||||
if (XQueryTree(dpy, (pw = w), &dw, &w, &dws, &du) && dws)
|
||||
XFree(dws);
|
||||
} while (w != root && w != pw);
|
||||
/* find xinerama screen with which the window intersects most */
|
||||
if (XGetWindowAttributes(dpy, pw, &wa))
|
||||
for (j = 0; j < n; j++)
|
||||
if ((a = INTERSECT(wa.x, wa.y, wa.width, wa.height, info[j])) > area) {
|
||||
area = a;
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
/* no focused window is on screen, so use pointer location instead */
|
||||
if (mon < 0 && !area && XQueryPointer(dpy, root, &dw, &dw, &x, &y, &di, &di, &du))
|
||||
for (i = 0; i < n; i++)
|
||||
if (INTERSECT(x, y, 1, 1, info[i]) != 0)
|
||||
break;
|
||||
|
||||
x = info[i].x_org;
|
||||
y = info[i].y_org + (topbar ? 0 : info[i].height - mh);
|
||||
mw = info[i].width;
|
||||
XFree(info);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (!XGetWindowAttributes(dpy, parentwin, &wa))
|
||||
die("could not get embedding window attributes: 0x%lx",
|
||||
parentwin);
|
||||
x = 0;
|
||||
y = topbar ? 0 : wa.height - mh;
|
||||
mw = wa.width;
|
||||
}
|
||||
promptw = (prompt && *prompt) ? TEXTW(prompt) - lrpad / 4 : 0;
|
||||
inputw = mw / 3; /* input width: ~33% of monitor width */
|
||||
match();
|
||||
|
||||
/* create menu window */
|
||||
swa.override_redirect = True;
|
||||
swa.background_pixel = scheme[SchemeNorm][ColBg].pixel;
|
||||
swa.event_mask = ExposureMask | KeyPressMask | VisibilityChangeMask;
|
||||
win = XCreateWindow(dpy, root, x, y, mw, mh, 0,
|
||||
CopyFromParent, CopyFromParent, CopyFromParent,
|
||||
CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
|
||||
XSetClassHint(dpy, win, &ch);
|
||||
|
||||
/* input methods */
|
||||
if ((xim = XOpenIM(dpy, NULL, NULL, NULL)) == NULL)
|
||||
die("XOpenIM failed: could not open input device");
|
||||
|
||||
xic = XCreateIC(xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
|
||||
XNClientWindow, win, XNFocusWindow, win, NULL);
|
||||
|
||||
XMapRaised(dpy, win);
|
||||
if (embed) {
|
||||
XReparentWindow(dpy, win, parentwin, x, y);
|
||||
XSelectInput(dpy, parentwin, FocusChangeMask | SubstructureNotifyMask);
|
||||
if (XQueryTree(dpy, parentwin, &dw, &w, &dws, &du) && dws) {
|
||||
for (i = 0; i < du && dws[i] != win; ++i)
|
||||
XSelectInput(dpy, dws[i], FocusChangeMask);
|
||||
XFree(dws);
|
||||
}
|
||||
grabfocus();
|
||||
}
|
||||
drw_resize(drw, mw, mh);
|
||||
drawmenu();
|
||||
}
|
||||
|
||||
static void
|
||||
usage(void)
|
||||
{
|
||||
die("usage: dmenu [-bfiv] [-l lines] [-p prompt] [-fn font] [-m monitor]\n"
|
||||
" [-nb color] [-nf color] [-sb color] [-sf color] [-w windowid]");
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
XWindowAttributes wa;
|
||||
int i, fast = 0;
|
||||
|
||||
for (i = 1; i < argc; i++)
|
||||
/* these options take no arguments */
|
||||
if (!strcmp(argv[i], "-v")) { /* prints version information */
|
||||
puts("dmenu-"VERSION);
|
||||
exit(0);
|
||||
} else if (!strcmp(argv[i], "-b")) /* appears at the bottom of the screen */
|
||||
topbar = 0;
|
||||
else if (!strcmp(argv[i], "-f")) /* grabs keyboard before reading stdin */
|
||||
fast = 1;
|
||||
else if (!strcmp(argv[i], "-i")) { /* case-insensitive item matching */
|
||||
fstrncmp = strncasecmp;
|
||||
fstrstr = cistrstr;
|
||||
} else if (i + 1 == argc)
|
||||
usage();
|
||||
/* these options take one argument */
|
||||
else if (!strcmp(argv[i], "-l")) /* number of lines in vertical list */
|
||||
lines = atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "-m"))
|
||||
mon = atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "-p")) /* adds prompt to left of input field */
|
||||
prompt = argv[++i];
|
||||
else if (!strcmp(argv[i], "-fn")) /* font or font set */
|
||||
fonts[0] = argv[++i];
|
||||
else if (!strcmp(argv[i], "-nb")) /* normal background color */
|
||||
colors[SchemeNorm][ColBg] = argv[++i];
|
||||
else if (!strcmp(argv[i], "-nf")) /* normal foreground color */
|
||||
colors[SchemeNorm][ColFg] = argv[++i];
|
||||
else if (!strcmp(argv[i], "-sb")) /* selected background color */
|
||||
colors[SchemeSel][ColBg] = argv[++i];
|
||||
else if (!strcmp(argv[i], "-sf")) /* selected foreground color */
|
||||
colors[SchemeSel][ColFg] = argv[++i];
|
||||
else if (!strcmp(argv[i], "-ob")) /* outline background color */
|
||||
colors[SchemeOut][ColBg] = argv[++i];
|
||||
else if (!strcmp(argv[i], "-of")) /* outline foreground color */
|
||||
colors[SchemeOut][ColFg] = argv[++i];
|
||||
else if (!strcmp(argv[i], "-w")) /* embedding window id */
|
||||
embed = argv[++i];
|
||||
else
|
||||
usage();
|
||||
|
||||
if (!setlocale(LC_CTYPE, "") || !XSupportsLocale())
|
||||
fputs("warning: no locale support\n", stderr);
|
||||
if (!(dpy = XOpenDisplay(NULL)))
|
||||
die("cannot open display");
|
||||
screen = DefaultScreen(dpy);
|
||||
root = RootWindow(dpy, screen);
|
||||
if (!embed || !(parentwin = strtol(embed, NULL, 0)))
|
||||
parentwin = root;
|
||||
if (!XGetWindowAttributes(dpy, parentwin, &wa))
|
||||
die("could not get embedding window attributes: 0x%lx",
|
||||
parentwin);
|
||||
drw = drw_create(dpy, screen, root, wa.width, wa.height);
|
||||
if (!drw_fontset_create(drw, fonts, LENGTH(fonts)))
|
||||
die("no fonts could be loaded.");
|
||||
lrpad = drw->fonts->h;
|
||||
|
||||
#ifdef __OpenBSD__
|
||||
if (pledge("stdio rpath", NULL) == -1)
|
||||
die("pledge");
|
||||
#endif
|
||||
|
||||
if (fast && !isatty(0)) {
|
||||
grabkeyboard();
|
||||
readstdin();
|
||||
} else {
|
||||
readstdin();
|
||||
grabkeyboard();
|
||||
}
|
||||
setup();
|
||||
run();
|
||||
|
||||
return 1; /* unreachable */
|
||||
}
|
||||
13
suckless/dmenu/dmenu_path
Executable file
13
suckless/dmenu/dmenu_path
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
|
||||
cachedir="${XDG_CACHE_HOME:-"$HOME/.cache"}"
|
||||
cache="$cachedir/dmenu_run"
|
||||
|
||||
[ ! -e "$cachedir" ] && mkdir -p "$cachedir"
|
||||
|
||||
IFS=:
|
||||
if stest -dqr -n "$cache" $PATH; then
|
||||
stest -flx $PATH | sort -u | tee "$cache"
|
||||
else
|
||||
cat "$cache"
|
||||
fi
|
||||
2
suckless/dmenu/dmenu_run
Executable file
2
suckless/dmenu/dmenu_run
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
dmenu_path | dmenu "$@" | ${SHELL:-"/bin/sh"} &
|
||||
471
suckless/dmenu/drw.c
Normal file
471
suckless/dmenu/drw.c
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xft/Xft.h>
|
||||
|
||||
#include "drw.h"
|
||||
#include "util.h"
|
||||
|
||||
#define UTF_INVALID 0xFFFD
|
||||
|
||||
static int
|
||||
utf8decode(const char *s_in, long *u, int *err)
|
||||
{
|
||||
static const unsigned char lens[] = {
|
||||
/* 0XXXX */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
/* 10XXX */ 0, 0, 0, 0, 0, 0, 0, 0, /* invalid */
|
||||
/* 110XX */ 2, 2, 2, 2,
|
||||
/* 1110X */ 3, 3,
|
||||
/* 11110 */ 4,
|
||||
/* 11111 */ 0, /* invalid */
|
||||
};
|
||||
static const unsigned char leading_mask[] = { 0x7F, 0x1F, 0x0F, 0x07 };
|
||||
static const unsigned int overlong[] = { 0x0, 0x80, 0x0800, 0x10000 };
|
||||
|
||||
const unsigned char *s = (const unsigned char *)s_in;
|
||||
int len = lens[*s >> 3];
|
||||
*u = UTF_INVALID;
|
||||
*err = 1;
|
||||
if (len == 0)
|
||||
return 1;
|
||||
|
||||
long cp = s[0] & leading_mask[len - 1];
|
||||
for (int i = 1; i < len; ++i) {
|
||||
if (s[i] == '\0' || (s[i] & 0xC0) != 0x80)
|
||||
return i;
|
||||
cp = (cp << 6) | (s[i] & 0x3F);
|
||||
}
|
||||
/* out of range, surrogate, overlong encoding */
|
||||
if (cp > 0x10FFFF || (cp >> 11) == 0x1B || cp < overlong[len - 1])
|
||||
return len;
|
||||
|
||||
*err = 0;
|
||||
*u = cp;
|
||||
return len;
|
||||
}
|
||||
|
||||
Drw *
|
||||
drw_create(Display *dpy, int screen, Window root, unsigned int w, unsigned int h)
|
||||
{
|
||||
Drw *drw = ecalloc(1, sizeof(Drw));
|
||||
|
||||
drw->dpy = dpy;
|
||||
drw->screen = screen;
|
||||
drw->root = root;
|
||||
drw->w = w;
|
||||
drw->h = h;
|
||||
drw->drawable = XCreatePixmap(dpy, root, w, h, DefaultDepth(dpy, screen));
|
||||
drw->gc = XCreateGC(dpy, root, 0, NULL);
|
||||
XSetLineAttributes(dpy, drw->gc, 1, LineSolid, CapButt, JoinMiter);
|
||||
|
||||
return drw;
|
||||
}
|
||||
|
||||
void
|
||||
drw_resize(Drw *drw, unsigned int w, unsigned int h)
|
||||
{
|
||||
if (!drw)
|
||||
return;
|
||||
|
||||
drw->w = w;
|
||||
drw->h = h;
|
||||
if (drw->drawable)
|
||||
XFreePixmap(drw->dpy, drw->drawable);
|
||||
drw->drawable = XCreatePixmap(drw->dpy, drw->root, w, h, DefaultDepth(drw->dpy, drw->screen));
|
||||
}
|
||||
|
||||
void
|
||||
drw_free(Drw *drw)
|
||||
{
|
||||
XFreePixmap(drw->dpy, drw->drawable);
|
||||
XFreeGC(drw->dpy, drw->gc);
|
||||
drw_fontset_free(drw->fonts);
|
||||
free(drw);
|
||||
}
|
||||
|
||||
/* This function is an implementation detail. Library users should use
|
||||
* drw_fontset_create instead.
|
||||
*/
|
||||
static Fnt *
|
||||
xfont_create(Drw *drw, const char *fontname, FcPattern *fontpattern)
|
||||
{
|
||||
Fnt *font;
|
||||
XftFont *xfont = NULL;
|
||||
FcPattern *pattern = NULL;
|
||||
|
||||
if (fontname) {
|
||||
/* Using the pattern found at font->xfont->pattern does not yield the
|
||||
* same substitution results as using the pattern returned by
|
||||
* FcNameParse; using the latter results in the desired fallback
|
||||
* behaviour whereas the former just results in missing-character
|
||||
* rectangles being drawn, at least with some fonts. */
|
||||
if (!(xfont = XftFontOpenName(drw->dpy, drw->screen, fontname))) {
|
||||
fprintf(stderr, "error, cannot load font from name: '%s'\n", fontname);
|
||||
return NULL;
|
||||
}
|
||||
if (!(pattern = FcNameParse((FcChar8 *) fontname))) {
|
||||
fprintf(stderr, "error, cannot parse font name to pattern: '%s'\n", fontname);
|
||||
XftFontClose(drw->dpy, xfont);
|
||||
return NULL;
|
||||
}
|
||||
} else if (fontpattern) {
|
||||
if (!(xfont = XftFontOpenPattern(drw->dpy, fontpattern))) {
|
||||
fprintf(stderr, "error, cannot load font from pattern.\n");
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
die("no font specified.");
|
||||
}
|
||||
|
||||
font = ecalloc(1, sizeof(Fnt));
|
||||
font->xfont = xfont;
|
||||
font->pattern = pattern;
|
||||
font->h = xfont->ascent + xfont->descent;
|
||||
font->dpy = drw->dpy;
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
static void
|
||||
xfont_free(Fnt *font)
|
||||
{
|
||||
if (!font)
|
||||
return;
|
||||
if (font->pattern)
|
||||
FcPatternDestroy(font->pattern);
|
||||
XftFontClose(font->dpy, font->xfont);
|
||||
free(font);
|
||||
}
|
||||
|
||||
Fnt*
|
||||
drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount)
|
||||
{
|
||||
Fnt *cur, *ret = NULL;
|
||||
size_t i;
|
||||
|
||||
if (!drw || !fonts)
|
||||
return NULL;
|
||||
|
||||
for (i = 1; i <= fontcount; i++) {
|
||||
if ((cur = xfont_create(drw, fonts[fontcount - i], NULL))) {
|
||||
cur->next = ret;
|
||||
ret = cur;
|
||||
}
|
||||
}
|
||||
return (drw->fonts = ret);
|
||||
}
|
||||
|
||||
void
|
||||
drw_fontset_free(Fnt *font)
|
||||
{
|
||||
if (font) {
|
||||
drw_fontset_free(font->next);
|
||||
xfont_free(font);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
drw_clr_create(Drw *drw, Clr *dest, const char *clrname)
|
||||
{
|
||||
if (!drw || !dest || !clrname)
|
||||
return;
|
||||
|
||||
if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen),
|
||||
DefaultColormap(drw->dpy, drw->screen),
|
||||
clrname, dest))
|
||||
die("error, cannot allocate color '%s'", clrname);
|
||||
}
|
||||
|
||||
/* Create color schemes. */
|
||||
Clr *
|
||||
drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount)
|
||||
{
|
||||
size_t i;
|
||||
Clr *ret;
|
||||
|
||||
/* need at least two colors for a scheme */
|
||||
if (!drw || !clrnames || clrcount < 2 || !(ret = ecalloc(clrcount, sizeof(Clr))))
|
||||
return NULL;
|
||||
|
||||
for (i = 0; i < clrcount; i++)
|
||||
drw_clr_create(drw, &ret[i], clrnames[i]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void
|
||||
drw_clr_free(Drw *drw, Clr *c)
|
||||
{
|
||||
if (!drw || !c)
|
||||
return;
|
||||
|
||||
/* c is typedef XftColor Clr */
|
||||
XftColorFree(drw->dpy, DefaultVisual(drw->dpy, drw->screen),
|
||||
DefaultColormap(drw->dpy, drw->screen), c);
|
||||
}
|
||||
|
||||
void
|
||||
drw_scm_free(Drw *drw, Clr *scm, size_t clrcount)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (!drw || !scm)
|
||||
return;
|
||||
|
||||
for (i = 0; i < clrcount; i++)
|
||||
drw_clr_free(drw, &scm[i]);
|
||||
free(scm);
|
||||
}
|
||||
|
||||
void
|
||||
drw_setfontset(Drw *drw, Fnt *set)
|
||||
{
|
||||
if (drw)
|
||||
drw->fonts = set;
|
||||
}
|
||||
|
||||
void
|
||||
drw_setscheme(Drw *drw, Clr *scm)
|
||||
{
|
||||
if (drw)
|
||||
drw->scheme = scm;
|
||||
}
|
||||
|
||||
void
|
||||
drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert)
|
||||
{
|
||||
if (!drw || !drw->scheme)
|
||||
return;
|
||||
XSetForeground(drw->dpy, drw->gc, invert ? drw->scheme[ColBg].pixel : drw->scheme[ColFg].pixel);
|
||||
if (filled)
|
||||
XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
|
||||
else
|
||||
XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1);
|
||||
}
|
||||
|
||||
int
|
||||
drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert)
|
||||
{
|
||||
int ty, ellipsis_x = 0;
|
||||
unsigned int tmpw, ew, ellipsis_w = 0, ellipsis_len, hash, h0, h1;
|
||||
XftDraw *d = NULL;
|
||||
Fnt *usedfont, *curfont, *nextfont;
|
||||
int utf8strlen, utf8charlen, utf8err, render = x || y || w || h;
|
||||
long utf8codepoint = 0;
|
||||
const char *utf8str;
|
||||
FcCharSet *fccharset;
|
||||
FcPattern *fcpattern;
|
||||
FcPattern *match;
|
||||
XftResult result;
|
||||
int charexists = 0, overflow = 0;
|
||||
/* keep track of a couple codepoints for which we have no match. */
|
||||
static unsigned int nomatches[128], ellipsis_width, invalid_width;
|
||||
static const char invalid[] = "<EFBFBD>";
|
||||
|
||||
if (!drw || (render && (!drw->scheme || !w)) || !text || !drw->fonts)
|
||||
return 0;
|
||||
|
||||
if (!render) {
|
||||
w = invert ? invert : ~invert;
|
||||
} else {
|
||||
XSetForeground(drw->dpy, drw->gc, drw->scheme[invert ? ColFg : ColBg].pixel);
|
||||
XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
|
||||
if (w < lpad)
|
||||
return x + w;
|
||||
d = XftDrawCreate(drw->dpy, drw->drawable,
|
||||
DefaultVisual(drw->dpy, drw->screen),
|
||||
DefaultColormap(drw->dpy, drw->screen));
|
||||
x += lpad;
|
||||
w -= lpad;
|
||||
}
|
||||
|
||||
usedfont = drw->fonts;
|
||||
if (!ellipsis_width && render)
|
||||
ellipsis_width = drw_fontset_getwidth(drw, "...");
|
||||
if (!invalid_width && render)
|
||||
invalid_width = drw_fontset_getwidth(drw, invalid);
|
||||
while (1) {
|
||||
ew = ellipsis_len = utf8err = utf8charlen = utf8strlen = 0;
|
||||
utf8str = text;
|
||||
nextfont = NULL;
|
||||
while (*text) {
|
||||
utf8charlen = utf8decode(text, &utf8codepoint, &utf8err);
|
||||
for (curfont = drw->fonts; curfont; curfont = curfont->next) {
|
||||
charexists = charexists || XftCharExists(drw->dpy, curfont->xfont, utf8codepoint);
|
||||
if (charexists) {
|
||||
drw_font_getexts(curfont, text, utf8charlen, &tmpw, NULL);
|
||||
if (ew + ellipsis_width <= w) {
|
||||
/* keep track where the ellipsis still fits */
|
||||
ellipsis_x = x + ew;
|
||||
ellipsis_w = w - ew;
|
||||
ellipsis_len = utf8strlen;
|
||||
}
|
||||
|
||||
if (ew + tmpw > w) {
|
||||
overflow = 1;
|
||||
/* called from drw_fontset_getwidth_clamp():
|
||||
* it wants the width AFTER the overflow
|
||||
*/
|
||||
if (!render)
|
||||
x += tmpw;
|
||||
else
|
||||
utf8strlen = ellipsis_len;
|
||||
} else if (curfont == usedfont) {
|
||||
text += utf8charlen;
|
||||
utf8strlen += utf8err ? 0 : utf8charlen;
|
||||
ew += utf8err ? 0 : tmpw;
|
||||
} else {
|
||||
nextfont = curfont;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (overflow || !charexists || nextfont || utf8err)
|
||||
break;
|
||||
else
|
||||
charexists = 0;
|
||||
}
|
||||
|
||||
if (utf8strlen) {
|
||||
if (render) {
|
||||
ty = y + (h - usedfont->h) / 2 + usedfont->xfont->ascent;
|
||||
XftDrawStringUtf8(d, &drw->scheme[invert ? ColBg : ColFg],
|
||||
usedfont->xfont, x, ty, (XftChar8 *)utf8str, utf8strlen);
|
||||
}
|
||||
x += ew;
|
||||
w -= ew;
|
||||
}
|
||||
if (utf8err && (!render || invalid_width < w)) {
|
||||
if (render)
|
||||
drw_text(drw, x, y, w, h, 0, invalid, invert);
|
||||
x += invalid_width;
|
||||
w -= invalid_width;
|
||||
}
|
||||
if (render && overflow)
|
||||
drw_text(drw, ellipsis_x, y, ellipsis_w, h, 0, "...", invert);
|
||||
|
||||
if (!*text || overflow) {
|
||||
break;
|
||||
} else if (nextfont) {
|
||||
charexists = 0;
|
||||
usedfont = nextfont;
|
||||
} else {
|
||||
/* Regardless of whether or not a fallback font is found, the
|
||||
* character must be drawn. */
|
||||
charexists = 1;
|
||||
|
||||
hash = (unsigned int)utf8codepoint;
|
||||
hash = ((hash >> 16) ^ hash) * 0x21F0AAAD;
|
||||
hash = ((hash >> 15) ^ hash) * 0xD35A2D97;
|
||||
h0 = ((hash >> 15) ^ hash) % LENGTH(nomatches);
|
||||
h1 = (hash >> 17) % LENGTH(nomatches);
|
||||
/* avoid expensive XftFontMatch call when we know we won't find a match */
|
||||
if (nomatches[h0] == utf8codepoint || nomatches[h1] == utf8codepoint)
|
||||
goto no_match;
|
||||
|
||||
fccharset = FcCharSetCreate();
|
||||
FcCharSetAddChar(fccharset, utf8codepoint);
|
||||
|
||||
if (!drw->fonts->pattern) {
|
||||
/* Refer to the comment in xfont_create for more information. */
|
||||
die("the first font in the cache must be loaded from a font string.");
|
||||
}
|
||||
|
||||
fcpattern = FcPatternDuplicate(drw->fonts->pattern);
|
||||
FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset);
|
||||
FcPatternAddBool(fcpattern, FC_SCALABLE, FcTrue);
|
||||
|
||||
FcConfigSubstitute(NULL, fcpattern, FcMatchPattern);
|
||||
FcDefaultSubstitute(fcpattern);
|
||||
match = XftFontMatch(drw->dpy, drw->screen, fcpattern, &result);
|
||||
|
||||
FcCharSetDestroy(fccharset);
|
||||
FcPatternDestroy(fcpattern);
|
||||
|
||||
if (match) {
|
||||
usedfont = xfont_create(drw, NULL, match);
|
||||
if (usedfont && XftCharExists(drw->dpy, usedfont->xfont, utf8codepoint)) {
|
||||
for (curfont = drw->fonts; curfont->next; curfont = curfont->next)
|
||||
; /* NOP */
|
||||
curfont->next = usedfont;
|
||||
} else {
|
||||
xfont_free(usedfont);
|
||||
nomatches[nomatches[h0] ? h1 : h0] = utf8codepoint;
|
||||
no_match:
|
||||
usedfont = drw->fonts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (d)
|
||||
XftDrawDestroy(d);
|
||||
|
||||
return x + (render ? w : 0);
|
||||
}
|
||||
|
||||
void
|
||||
drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h)
|
||||
{
|
||||
if (!drw)
|
||||
return;
|
||||
|
||||
XCopyArea(drw->dpy, drw->drawable, win, drw->gc, x, y, w, h, x, y);
|
||||
XSync(drw->dpy, False);
|
||||
}
|
||||
|
||||
unsigned int
|
||||
drw_fontset_getwidth(Drw *drw, const char *text)
|
||||
{
|
||||
if (!drw || !drw->fonts || !text)
|
||||
return 0;
|
||||
return drw_text(drw, 0, 0, 0, 0, 0, text, 0);
|
||||
}
|
||||
|
||||
unsigned int
|
||||
drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n)
|
||||
{
|
||||
unsigned int tmp = 0;
|
||||
if (drw && drw->fonts && text && n)
|
||||
tmp = drw_text(drw, 0, 0, 0, 0, 0, text, n);
|
||||
return MIN(n, tmp);
|
||||
}
|
||||
|
||||
void
|
||||
drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h)
|
||||
{
|
||||
XGlyphInfo ext;
|
||||
|
||||
if (!font || !text)
|
||||
return;
|
||||
|
||||
XftTextExtentsUtf8(font->dpy, font->xfont, (XftChar8 *)text, len, &ext);
|
||||
if (w)
|
||||
*w = ext.xOff;
|
||||
if (h)
|
||||
*h = font->h;
|
||||
}
|
||||
|
||||
Cur *
|
||||
drw_cur_create(Drw *drw, int shape)
|
||||
{
|
||||
Cur *cur;
|
||||
|
||||
if (!drw || !(cur = ecalloc(1, sizeof(Cur))))
|
||||
return NULL;
|
||||
|
||||
cur->cursor = XCreateFontCursor(drw->dpy, shape);
|
||||
|
||||
return cur;
|
||||
}
|
||||
|
||||
void
|
||||
drw_cur_free(Drw *drw, Cur *cursor)
|
||||
{
|
||||
if (!cursor)
|
||||
return;
|
||||
|
||||
XFreeCursor(drw->dpy, cursor->cursor);
|
||||
free(cursor);
|
||||
}
|
||||
60
suckless/dmenu/drw.h
Normal file
60
suckless/dmenu/drw.h
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
|
||||
typedef struct {
|
||||
Cursor cursor;
|
||||
} Cur;
|
||||
|
||||
typedef struct Fnt {
|
||||
Display *dpy;
|
||||
unsigned int h;
|
||||
XftFont *xfont;
|
||||
FcPattern *pattern;
|
||||
struct Fnt *next;
|
||||
} Fnt;
|
||||
|
||||
enum { ColFg, ColBg }; /* Clr scheme index */
|
||||
typedef XftColor Clr;
|
||||
|
||||
typedef struct {
|
||||
unsigned int w, h;
|
||||
Display *dpy;
|
||||
int screen;
|
||||
Window root;
|
||||
Drawable drawable;
|
||||
GC gc;
|
||||
Clr *scheme;
|
||||
Fnt *fonts;
|
||||
} Drw;
|
||||
|
||||
/* Drawable abstraction */
|
||||
Drw *drw_create(Display *dpy, int screen, Window win, unsigned int w, unsigned int h);
|
||||
void drw_resize(Drw *drw, unsigned int w, unsigned int h);
|
||||
void drw_free(Drw *drw);
|
||||
|
||||
/* Fnt abstraction */
|
||||
Fnt *drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount);
|
||||
void drw_fontset_free(Fnt* set);
|
||||
unsigned int drw_fontset_getwidth(Drw *drw, const char *text);
|
||||
unsigned int drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n);
|
||||
void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h);
|
||||
|
||||
/* Colorscheme abstraction */
|
||||
void drw_clr_create(Drw *drw, Clr *dest, const char *clrname);
|
||||
void drw_clr_free(Drw *drw, Clr *c);
|
||||
Clr *drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount);
|
||||
void drw_scm_free(Drw *drw, Clr *scm, size_t clrcount);
|
||||
|
||||
/* Cursor abstraction */
|
||||
Cur *drw_cur_create(Drw *drw, int shape);
|
||||
void drw_cur_free(Drw *drw, Cur *cursor);
|
||||
|
||||
/* Drawing context manipulation */
|
||||
void drw_setfontset(Drw *drw, Fnt *set);
|
||||
void drw_setscheme(Drw *drw, Clr *scm);
|
||||
|
||||
/* Drawing functions */
|
||||
void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert);
|
||||
int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert);
|
||||
|
||||
/* Map functions */
|
||||
void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h);
|
||||
90
suckless/dmenu/stest.1
Normal file
90
suckless/dmenu/stest.1
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
.TH STEST 1 dmenu\-VERSION
|
||||
.SH NAME
|
||||
stest \- filter a list of files by properties
|
||||
.SH SYNOPSIS
|
||||
.B stest
|
||||
.RB [ -abcdefghlpqrsuwx ]
|
||||
.RB [ -n
|
||||
.IR file ]
|
||||
.RB [ -o
|
||||
.IR file ]
|
||||
.RI [ file ...]
|
||||
.SH DESCRIPTION
|
||||
.B stest
|
||||
takes a list of files and filters by the files' properties, analogous to
|
||||
.IR test (1).
|
||||
Files which pass all tests are printed to stdout. If no files are given, stest
|
||||
reads files from stdin.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-a
|
||||
Test hidden files.
|
||||
.TP
|
||||
.B \-b
|
||||
Test that files are block specials.
|
||||
.TP
|
||||
.B \-c
|
||||
Test that files are character specials.
|
||||
.TP
|
||||
.B \-d
|
||||
Test that files are directories.
|
||||
.TP
|
||||
.B \-e
|
||||
Test that files exist.
|
||||
.TP
|
||||
.B \-f
|
||||
Test that files are regular files.
|
||||
.TP
|
||||
.B \-g
|
||||
Test that files have their set-group-ID flag set.
|
||||
.TP
|
||||
.B \-h
|
||||
Test that files are symbolic links.
|
||||
.TP
|
||||
.B \-l
|
||||
Test the contents of a directory given as an argument.
|
||||
.TP
|
||||
.BI \-n " file"
|
||||
Test that files are newer than
|
||||
.IR file .
|
||||
.TP
|
||||
.BI \-o " file"
|
||||
Test that files are older than
|
||||
.IR file .
|
||||
.TP
|
||||
.B \-p
|
||||
Test that files are named pipes.
|
||||
.TP
|
||||
.B \-q
|
||||
No files are printed, only the exit status is returned.
|
||||
.TP
|
||||
.B \-r
|
||||
Test that files are readable.
|
||||
.TP
|
||||
.B \-s
|
||||
Test that files are not empty.
|
||||
.TP
|
||||
.B \-u
|
||||
Test that files have their set-user-ID flag set.
|
||||
.TP
|
||||
.B \-v
|
||||
Invert the sense of tests, only failing files pass.
|
||||
.TP
|
||||
.B \-w
|
||||
Test that files are writable.
|
||||
.TP
|
||||
.B \-x
|
||||
Test that files are executable.
|
||||
.SH EXIT STATUS
|
||||
.TP
|
||||
.B 0
|
||||
At least one file passed all tests.
|
||||
.TP
|
||||
.B 1
|
||||
No files passed all tests.
|
||||
.TP
|
||||
.B 2
|
||||
An error occurred.
|
||||
.SH SEE ALSO
|
||||
.IR dmenu (1),
|
||||
.IR test (1)
|
||||
109
suckless/dmenu/stest.c
Normal file
109
suckless/dmenu/stest.c
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <dirent.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "arg.h"
|
||||
char *argv0;
|
||||
|
||||
#define FLAG(x) (flag[(x)-'a'])
|
||||
|
||||
static void test(const char *, const char *);
|
||||
static void usage(void);
|
||||
|
||||
static int match = 0;
|
||||
static int flag[26];
|
||||
static struct stat old, new;
|
||||
|
||||
static void
|
||||
test(const char *path, const char *name)
|
||||
{
|
||||
struct stat st, ln;
|
||||
|
||||
if ((!stat(path, &st) && (FLAG('a') || name[0] != '.') /* hidden files */
|
||||
&& (!FLAG('b') || S_ISBLK(st.st_mode)) /* block special */
|
||||
&& (!FLAG('c') || S_ISCHR(st.st_mode)) /* character special */
|
||||
&& (!FLAG('d') || S_ISDIR(st.st_mode)) /* directory */
|
||||
&& (!FLAG('e') || access(path, F_OK) == 0) /* exists */
|
||||
&& (!FLAG('f') || S_ISREG(st.st_mode)) /* regular file */
|
||||
&& (!FLAG('g') || st.st_mode & S_ISGID) /* set-group-id flag */
|
||||
&& (!FLAG('h') || (!lstat(path, &ln) && S_ISLNK(ln.st_mode))) /* symbolic link */
|
||||
&& (!FLAG('n') || st.st_mtime > new.st_mtime) /* newer than file */
|
||||
&& (!FLAG('o') || st.st_mtime < old.st_mtime) /* older than file */
|
||||
&& (!FLAG('p') || S_ISFIFO(st.st_mode)) /* named pipe */
|
||||
&& (!FLAG('r') || access(path, R_OK) == 0) /* readable */
|
||||
&& (!FLAG('s') || st.st_size > 0) /* not empty */
|
||||
&& (!FLAG('u') || st.st_mode & S_ISUID) /* set-user-id flag */
|
||||
&& (!FLAG('w') || access(path, W_OK) == 0) /* writable */
|
||||
&& (!FLAG('x') || access(path, X_OK) == 0)) != FLAG('v')) { /* executable */
|
||||
if (FLAG('q'))
|
||||
exit(0);
|
||||
match = 1;
|
||||
puts(name);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
usage(void)
|
||||
{
|
||||
fprintf(stderr, "usage: %s [-abcdefghlpqrsuvwx] "
|
||||
"[-n file] [-o file] [file...]\n", argv0);
|
||||
exit(2); /* like test(1) return > 1 on error */
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
struct dirent *d;
|
||||
char path[PATH_MAX], *line = NULL, *file;
|
||||
size_t linesiz = 0;
|
||||
ssize_t n;
|
||||
DIR *dir;
|
||||
int r;
|
||||
|
||||
ARGBEGIN {
|
||||
case 'n': /* newer than file */
|
||||
case 'o': /* older than file */
|
||||
file = EARGF(usage());
|
||||
if (!(FLAG(ARGC()) = !stat(file, (ARGC() == 'n' ? &new : &old))))
|
||||
perror(file);
|
||||
break;
|
||||
default:
|
||||
/* miscellaneous operators */
|
||||
if (strchr("abcdefghlpqrsuvwx", ARGC()))
|
||||
FLAG(ARGC()) = 1;
|
||||
else
|
||||
usage(); /* unknown flag */
|
||||
} ARGEND;
|
||||
|
||||
if (!argc) {
|
||||
/* read list from stdin */
|
||||
while ((n = getline(&line, &linesiz, stdin)) > 0) {
|
||||
if (line[n - 1] == '\n')
|
||||
line[n - 1] = '\0';
|
||||
test(line, line);
|
||||
}
|
||||
free(line);
|
||||
} else {
|
||||
for (; argc; argc--, argv++) {
|
||||
if (FLAG('l') && (dir = opendir(*argv))) {
|
||||
/* test directory contents */
|
||||
while ((d = readdir(dir))) {
|
||||
r = snprintf(path, sizeof path, "%s/%s",
|
||||
*argv, d->d_name);
|
||||
if (r >= 0 && (size_t)r < sizeof path)
|
||||
test(path, d->d_name);
|
||||
}
|
||||
closedir(dir);
|
||||
} else {
|
||||
test(*argv, *argv);
|
||||
}
|
||||
}
|
||||
}
|
||||
return match ? 0 : 1;
|
||||
}
|
||||
37
suckless/dmenu/util.c
Normal file
37
suckless/dmenu/util.c
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "util.h"
|
||||
|
||||
void
|
||||
die(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
int saved_errno;
|
||||
|
||||
saved_errno = errno;
|
||||
|
||||
va_start(ap, fmt);
|
||||
vfprintf(stderr, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
if (fmt[0] && fmt[strlen(fmt)-1] == ':')
|
||||
fprintf(stderr, " %s", strerror(saved_errno));
|
||||
fputc('\n', stderr);
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
void *
|
||||
ecalloc(size_t nmemb, size_t size)
|
||||
{
|
||||
void *p;
|
||||
|
||||
if (!(p = calloc(nmemb, size)))
|
||||
die("calloc:");
|
||||
return p;
|
||||
}
|
||||
9
suckless/dmenu/util.h
Normal file
9
suckless/dmenu/util.h
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
|
||||
#define MAX(A, B) ((A) > (B) ? (A) : (B))
|
||||
#define MIN(A, B) ((A) < (B) ? (A) : (B))
|
||||
#define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B))
|
||||
#define LENGTH(X) (sizeof (X) / sizeof (X)[0])
|
||||
|
||||
void die(const char *fmt, ...);
|
||||
void *ecalloc(size_t nmemb, size_t size);
|
||||
38
suckless/dwm/LICENSE
Normal file
38
suckless/dwm/LICENSE
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
MIT/X Consortium License
|
||||
|
||||
© 2010-2026 Hiltjo Posthuma <hiltjo@codemadness.org>
|
||||
© 2006-2019 Anselm R Garbe <anselm@garbe.ca>
|
||||
© 2006-2009 Jukka Salmi <jukka at salmi dot ch>
|
||||
© 2006-2007 Sander van Dijk <a dot h dot vandijk at gmail dot com>
|
||||
© 2007-2011 Peter Hartlich <sgkkr at hartlich dot com>
|
||||
© 2007-2009 Szabolcs Nagy <nszabolcs at gmail dot com>
|
||||
© 2007-2009 Christof Musik <christof at sendfax dot de>
|
||||
© 2007-2009 Premysl Hruby <dfenze at gmail dot com>
|
||||
© 2007-2008 Enno Gottox Boland <gottox at s01 dot de>
|
||||
© 2008 Martin Hurton <martin dot hurton at gmail dot com>
|
||||
© 2008 Neale Pickett <neale dot woozle dot org>
|
||||
© 2009 Mate Nagy <mnagy at port70 dot net>
|
||||
© 2010-2012 Connor Lane Smith <cls@lubutu.com>
|
||||
© 2011 Christoph Lohmann <20h@r-36.net>
|
||||
© 2015-2016 Quentin Rameau <quinq@fifth.space>
|
||||
© 2015-2016 Eric Pruitt <eric.pruitt@gmail.com>
|
||||
© 2016-2017 Markus Teich <markus.teich@stusta.mhn.de>
|
||||
© 2020-2022 Chris Down <chris@chrisdown.name>
|
||||
|
||||
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.
|
||||
45
suckless/dwm/Makefile
Normal file
45
suckless/dwm/Makefile
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# dwm - dynamic window manager
|
||||
# See LICENSE file for copyright and license details.
|
||||
|
||||
include config.mk
|
||||
|
||||
SRC = drw.c dwm.c util.c
|
||||
OBJ = ${SRC:.c=.o}
|
||||
|
||||
all: dwm
|
||||
|
||||
.c.o:
|
||||
${CC} -c ${CFLAGS} $<
|
||||
|
||||
${OBJ}: config.h config.mk
|
||||
|
||||
config.h:
|
||||
cp config.def.h $@
|
||||
|
||||
dwm: ${OBJ}
|
||||
${CC} -o $@ ${OBJ} ${LDFLAGS}
|
||||
|
||||
clean:
|
||||
rm -f dwm ${OBJ} dwm-${VERSION}.tar.gz
|
||||
|
||||
dist: clean
|
||||
mkdir -p dwm-${VERSION}
|
||||
cp -R LICENSE Makefile README config.def.h config.mk\
|
||||
dwm.1 drw.h util.h ${SRC} dwm.png transient.c dwm-${VERSION}
|
||||
tar -cf dwm-${VERSION}.tar dwm-${VERSION}
|
||||
gzip dwm-${VERSION}.tar
|
||||
rm -rf dwm-${VERSION}
|
||||
|
||||
install: all
|
||||
mkdir -p ${DESTDIR}${PREFIX}/bin
|
||||
cp -f dwm ${DESTDIR}${PREFIX}/bin
|
||||
chmod 755 ${DESTDIR}${PREFIX}/bin/dwm
|
||||
mkdir -p ${DESTDIR}${MANPREFIX}/man1
|
||||
sed "s/VERSION/${VERSION}/g" < dwm.1 > ${DESTDIR}${MANPREFIX}/man1/dwm.1
|
||||
chmod 644 ${DESTDIR}${MANPREFIX}/man1/dwm.1
|
||||
|
||||
uninstall:
|
||||
rm -f ${DESTDIR}${PREFIX}/bin/dwm\
|
||||
${DESTDIR}${MANPREFIX}/man1/dwm.1
|
||||
|
||||
.PHONY: all clean dist install uninstall
|
||||
48
suckless/dwm/README
Normal file
48
suckless/dwm/README
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
dwm - dynamic window manager
|
||||
============================
|
||||
dwm is an extremely fast, small, and dynamic window manager for X.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
In order to build dwm you need the Xlib header files.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
Edit config.mk to match your local setup (dwm is installed into
|
||||
the /usr/local namespace by default).
|
||||
|
||||
Afterwards enter the following command to build and install dwm (if
|
||||
necessary as root):
|
||||
|
||||
make clean install
|
||||
|
||||
|
||||
Running dwm
|
||||
-----------
|
||||
Add the following line to your .xinitrc to start dwm using startx:
|
||||
|
||||
exec dwm
|
||||
|
||||
In order to connect dwm to a specific display, make sure that
|
||||
the DISPLAY environment variable is set correctly, e.g.:
|
||||
|
||||
DISPLAY=foo.bar:1 exec dwm
|
||||
|
||||
(This will start dwm on display :1 of the host foo.bar.)
|
||||
|
||||
In order to display status info in the bar, you can do something
|
||||
like this in your .xinitrc:
|
||||
|
||||
while xsetroot -name "`date` `uptime | sed 's/.*,//'`"
|
||||
do
|
||||
sleep 1
|
||||
done &
|
||||
exec dwm
|
||||
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
The configuration of dwm is done by creating a custom config.h
|
||||
and (re)compiling the source code.
|
||||
133
suckless/dwm/config.def.h
Normal file
133
suckless/dwm/config.def.h
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
|
||||
/* appearance */
|
||||
static const unsigned int borderpx = 1; /* border pixel of windows */
|
||||
static const unsigned int gappx = 5; /* gaps between windows */
|
||||
static const unsigned int snap = 32; /* snap pixel */
|
||||
static const int swallowfloating = 0; /* 1 means swallow floating windows by default */
|
||||
static const int showbar = 1; /* 0 means no bar */
|
||||
static const int topbar = 1; /* 0 means bottom bar */
|
||||
static const char *fonts[] = { "monospace:size=10" };
|
||||
static const char dmenufont[] = "monospace:size=10";
|
||||
static const char col_gray1[] = "#222222";
|
||||
static const char col_gray2[] = "#444444";
|
||||
static const char col_gray3[] = "#bbbbbb";
|
||||
static const char col_gray4[] = "#eeeeee";
|
||||
static const char col_cyan[] = "#005577";
|
||||
static const char *colors[][3] = {
|
||||
/* fg bg border */
|
||||
[SchemeNorm] = { col_gray3, col_gray1, col_gray2 },
|
||||
[SchemeSel] = { col_gray4, col_cyan, col_cyan },
|
||||
};
|
||||
|
||||
/* tagging */
|
||||
static const char *tags[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9" };
|
||||
|
||||
static const Rule rules[] = {
|
||||
/* xprop(1):
|
||||
* WM_CLASS(STRING) = instance, class
|
||||
* WM_NAME(STRING) = title
|
||||
*/
|
||||
/* class instance title tags mask isfloating isterminal noswallow monitor */
|
||||
{ "Gimp", NULL, NULL, 0, 1, 0, 0, -1 },
|
||||
{ "Firefox", NULL, NULL, 1 << 8, 0, 0, -1, -1 },
|
||||
{ "St", NULL, NULL, 0, 0, 1, 0, -1 },
|
||||
{ NULL, NULL, "Event Tester", 0, 0, 0, 1, -1 }, /* xev */
|
||||
};
|
||||
|
||||
/* layout(s) */
|
||||
static const float mfact = 0.55; /* factor of master area size [0.05..0.95] */
|
||||
static const int nmaster = 1; /* number of clients in master area */
|
||||
static const int resizehints = 1; /* 1 means respect size hints in tiled resizals */
|
||||
static const int lockfullscreen = 1; /* 1 will force focus on the fullscreen window */
|
||||
static const int refreshrate = 120; /* refresh rate (per second) for client move/resize */
|
||||
|
||||
static const Layout layouts[] = {
|
||||
/* symbol arrange function */
|
||||
{ "[]=", tile }, /* first entry is default */
|
||||
{ "><>", NULL }, /* no layout function means floating behavior */
|
||||
{ "[M]", monocle },
|
||||
};
|
||||
|
||||
/* key definitions */
|
||||
#define MODKEY Mod1Mask
|
||||
#define TAGKEYS(KEY,TAG) \
|
||||
{ MODKEY, KEY, view, {.ui = 1 << TAG} }, \
|
||||
{ MODKEY|ControlMask, KEY, toggleview, {.ui = 1 << TAG} }, \
|
||||
{ MODKEY|ShiftMask, KEY, tag, {.ui = 1 << TAG} }, \
|
||||
{ MODKEY|ControlMask|ShiftMask, KEY, toggletag, {.ui = 1 << TAG} },
|
||||
|
||||
/* helper for spawning shell commands in the pre dwm-5.0 fashion */
|
||||
#define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } }
|
||||
|
||||
/* commands */
|
||||
static char dmenumon[2] = "0"; /* component of dmenucmd, manipulated in spawn() */
|
||||
static const char *dmenucmd[] = { "dmenu_run", "-m", dmenumon, "-fn", dmenufont, "-nb", col_gray1, "-nf", col_gray3, "-sb", col_cyan, "-sf", col_gray4, NULL };
|
||||
static const char *termcmd[] = { "st", NULL };
|
||||
|
||||
/* commands spawned when clicking statusbar, the mouse button pressed is exported as BUTTON */
|
||||
static const StatusCmd statuscmds[] = {
|
||||
{ "notify-send Mouse$BUTTON", 1 },
|
||||
};
|
||||
static const char *statuscmd[] = { "/bin/sh", "-c", NULL, NULL };
|
||||
|
||||
static const Key keys[] = {
|
||||
/* modifier key function argument */
|
||||
{ MODKEY, XK_p, spawn, {.v = dmenucmd } },
|
||||
{ MODKEY|ShiftMask, XK_Return, spawn, {.v = termcmd } },
|
||||
{ MODKEY, XK_b, togglebar, {0} },
|
||||
{ MODKEY, XK_j, focusstack, {.i = +1 } },
|
||||
{ MODKEY, XK_k, focusstack, {.i = -1 } },
|
||||
{ MODKEY, XK_i, incnmaster, {.i = +1 } },
|
||||
{ MODKEY, XK_d, incnmaster, {.i = -1 } },
|
||||
{ MODKEY, XK_h, setmfact, {.f = -0.05} },
|
||||
{ MODKEY, XK_l, setmfact, {.f = +0.05} },
|
||||
{ MODKEY, XK_Return, zoom, {0} },
|
||||
{ MODKEY, XK_Tab, view, {0} },
|
||||
{ MODKEY|ShiftMask, XK_c, killclient, {0} },
|
||||
{ MODKEY, XK_t, setlayout, {.v = &layouts[0]} },
|
||||
{ MODKEY, XK_f, setlayout, {.v = &layouts[1]} },
|
||||
{ MODKEY, XK_m, setlayout, {.v = &layouts[2]} },
|
||||
{ MODKEY, XK_space, setlayout, {0} },
|
||||
{ MODKEY|ShiftMask, XK_space, togglefloating, {0} },
|
||||
{ MODKEY, XK_0, view, {.ui = ~0 } },
|
||||
{ MODKEY|ShiftMask, XK_0, tag, {.ui = ~0 } },
|
||||
{ MODKEY, XK_comma, focusmon, {.i = -1 } },
|
||||
{ MODKEY, XK_period, focusmon, {.i = +1 } },
|
||||
{ MODKEY|ShiftMask, XK_comma, tagmon, {.i = -1 } },
|
||||
{ MODKEY|ShiftMask, XK_period, tagmon, {.i = +1 } },
|
||||
{ MODKEY, XK_minus, setgaps, {.i = -1 } },
|
||||
{ MODKEY, XK_equal, setgaps, {.i = +1 } },
|
||||
{ MODKEY|ShiftMask, XK_equal, setgaps, {.i = 0 } },
|
||||
TAGKEYS( XK_1, 0)
|
||||
TAGKEYS( XK_2, 1)
|
||||
TAGKEYS( XK_3, 2)
|
||||
TAGKEYS( XK_4, 3)
|
||||
TAGKEYS( XK_5, 4)
|
||||
TAGKEYS( XK_6, 5)
|
||||
TAGKEYS( XK_7, 6)
|
||||
TAGKEYS( XK_8, 7)
|
||||
TAGKEYS( XK_9, 8)
|
||||
{ MODKEY|ShiftMask, XK_q, quit, {0} },
|
||||
{ MODKEY|ControlMask|ShiftMask, XK_q, quit, {1} },
|
||||
};
|
||||
|
||||
/* button definitions */
|
||||
/* click can be ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, ClkClientWin, or ClkRootWin */
|
||||
static const Button buttons[] = {
|
||||
/* click event mask button function argument */
|
||||
{ ClkLtSymbol, 0, Button1, setlayout, {0} },
|
||||
{ ClkLtSymbol, 0, Button3, setlayout, {.v = &layouts[2]} },
|
||||
{ ClkWinTitle, 0, Button2, zoom, {0} },
|
||||
{ ClkStatusText, 0, Button1, spawn, {.v = statuscmd } },
|
||||
{ ClkStatusText, 0, Button2, spawn, {.v = statuscmd } },
|
||||
{ ClkStatusText, 0, Button3, spawn, {.v = statuscmd } },
|
||||
{ ClkClientWin, MODKEY, Button1, movemouse, {0} },
|
||||
{ ClkClientWin, MODKEY, Button2, togglefloating, {0} },
|
||||
{ ClkClientWin, MODKEY, Button3, resizemouse, {0} },
|
||||
{ ClkTagBar, 0, Button1, view, {0} },
|
||||
{ ClkTagBar, 0, Button3, toggleview, {0} },
|
||||
{ ClkTagBar, MODKEY, Button1, tag, {0} },
|
||||
{ ClkTagBar, MODKEY, Button3, toggletag, {0} },
|
||||
};
|
||||
|
||||
203
suckless/dwm/config.h
Normal file
203
suckless/dwm/config.h
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
|
||||
#include <X11/XF86keysym.h>
|
||||
|
||||
/* appearance */
|
||||
static const unsigned int borderpx = 2; /* border pixel of windows */
|
||||
static const unsigned int gappx = 4; /* gaps between windows */
|
||||
static const unsigned int snap = 32; /* snap pixel */
|
||||
static const int swallowfloating = 0; /* 1 means swallow floating windows by default */
|
||||
static const int showbar = 1; /* 0 means no bar */
|
||||
static const int topbar = 1; /* 0 means bottom bar */
|
||||
static const char *fonts[] = { "Fira Code:size=11:antialias=true:autohint=true" };
|
||||
static const char dmenufont[] = "Fira Code:size=11:antialias=true:autohint=true";
|
||||
|
||||
/* Paleta Everforest */
|
||||
static const char col_bg[] = "#2d353b"; /* Fondo general */
|
||||
static const char col_fg[] = "#d3c6aa"; /* Texto general */
|
||||
static const char col_border[] = "#475258"; /* Borde ventana inactiva */
|
||||
static const char col_accent[] = "#a7c080"; /* Verde acento (borde activo y barra) */
|
||||
static const char col_accent_fg[] = "#2d353b"; /* Texto sobre verde acento */
|
||||
|
||||
static const char *colors[][3] = {
|
||||
/* fg bg border */
|
||||
[SchemeNorm] = { col_fg, col_bg, col_border },
|
||||
[SchemeSel] = { col_accent_fg, col_accent, col_accent },
|
||||
};
|
||||
|
||||
/* tagging */
|
||||
static const char *tags[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9" };
|
||||
|
||||
static const Rule rules[] = {
|
||||
/* xprop(1):
|
||||
* WM_CLASS(STRING) = instance, class
|
||||
* WM_NAME(STRING) = title
|
||||
*/
|
||||
/* Las ventanas que flotan por una regla se abren centradas.
|
||||
* swallow: lo que se abre desde una terminal (isterminal) la sustituye
|
||||
* hasta que se cierra; noswallow = 1 lo evita para esa ventana. */
|
||||
/* class instance title tags mask isfloating isterminal noswallow monitor */
|
||||
{ "st-256color", NULL, NULL, 0, 0, 1, 0, -1 },
|
||||
{ "st-float", NULL, NULL, 0, 1, 0, 1, -1 }, /* st -c st-float (p. ej. nmtui) */
|
||||
{ "Gimp", NULL, NULL, 0, 1, 0, 0, -1 },
|
||||
{ NULL, "pavucontrol", NULL, 0, 1, 0, 0, -1 },
|
||||
{ NULL, "blueman-manager", NULL, 0, 1, 0, 0, -1 },
|
||||
{ "Firefox", "Places", NULL, 0, 1, 0, 0, -1 }, /* biblioteca/descargas */
|
||||
{ "Firefox", "Toolkit", NULL, 0, 1, 0, 0, -1 }, /* picture-in-picture */
|
||||
{ NULL, NULL, "Event Tester", 0, 0, 0, 1, -1 }, /* xev */
|
||||
};
|
||||
|
||||
/* layout(s) */
|
||||
static const float mfact = 0.55; /* factor of master area size [0.05..0.95] */
|
||||
static const int nmaster = 1; /* number of clients in master area */
|
||||
static const int resizehints = 1; /* 1 means respect size hints in tiled resizals */
|
||||
static const int lockfullscreen = 1; /* 1 will force focus on the fullscreen window */
|
||||
static const int refreshrate = 120; /* refresh rate (per second) for client move/resize */
|
||||
|
||||
static const Layout layouts[] = {
|
||||
/* symbol arrange function */
|
||||
{ "[]=", tile }, /* first entry is default */
|
||||
{ "><>", NULL }, /* no layout function means floating behavior */
|
||||
{ "[M]", monocle },
|
||||
};
|
||||
|
||||
/* key definitions */
|
||||
#define MODKEY Mod4Mask
|
||||
#define TAGKEYS(KEY,TAG) \
|
||||
{ MODKEY, KEY, view, {.ui = 1 << TAG} }, \
|
||||
{ MODKEY|ControlMask, KEY, toggleview, {.ui = 1 << TAG} }, \
|
||||
{ MODKEY|ShiftMask, KEY, tag, {.ui = 1 << TAG} }, \
|
||||
{ MODKEY|ControlMask|ShiftMask, KEY, toggletag, {.ui = 1 << TAG} },
|
||||
|
||||
/* helper for spawning shell commands in the pre dwm-5.0 fashion */
|
||||
#define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } }
|
||||
|
||||
/* commands */
|
||||
static char dmenumon[2] = "0"; /* component of dmenucmd, manipulated in spawn() */
|
||||
static const char *dmenucmd[] = { "dmenu_run", "-m", dmenumon, "-fn", dmenufont, "-nb", col_bg, "-nf", col_fg, "-sb", col_accent, "-sf", col_accent_fg, NULL };
|
||||
static const char *termcmd[] = { "st", NULL };
|
||||
static const char *clipcmd[] = { "clipmenu", "-fn", dmenufont, "-nb", col_bg, "-nf", col_fg, "-sb", col_accent, "-sf", col_accent_fg, NULL };
|
||||
static const char *lockcmd[] = { "slock", NULL };
|
||||
|
||||
/* Audio: tras cada cambio se avisa a slstatus (SIGUSR1) para que refresque la barra al momento */
|
||||
#define STATUSREFRESH "; pkill -USR1 -x slstatus"
|
||||
#define VOLUPCMD "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+" STATUSREFRESH
|
||||
#define VOLDOWNCMD "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-" STATUSREFRESH
|
||||
#define VOLMUTECMD "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle" STATUSREFRESH
|
||||
#define MICMUTECMD "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle" STATUSREFRESH
|
||||
#define VOLUP SHCMD(VOLUPCMD)
|
||||
#define VOLDOWN SHCMD(VOLDOWNCMD)
|
||||
#define VOLMUTE SHCMD(VOLMUTECMD)
|
||||
#define MICMUTE SHCMD(MICMUTECMD)
|
||||
|
||||
/*
|
||||
* Clics en la barra (parche statuscmd). El número es el byte que rodea cada
|
||||
* zona en el config.h de slstatus; el botón pulsado llega en $BUTTON
|
||||
* (1 izquierdo, 2 central, 3 derecho, 4/5 rueda arriba/abajo).
|
||||
*/
|
||||
static const StatusCmd statuscmds[] = {
|
||||
/* red: izquierdo conectarse a una red, derecho el menú completo de nmtui */
|
||||
{ "case $BUTTON in "
|
||||
"1) st -c st-float -g 90x30 -e nmtui connect ;; "
|
||||
"3) st -c st-float -g 90x30 -e nmtui ;; "
|
||||
"esac", 1 },
|
||||
/* volumen: izquierdo pavucontrol, central silenciar, derecho micro, rueda subir/bajar */
|
||||
{ "case $BUTTON in "
|
||||
"1) pavucontrol ;; "
|
||||
"2) " VOLMUTECMD " ;; "
|
||||
"3) " MICMUTECMD " ;; "
|
||||
"4) " VOLUPCMD " ;; "
|
||||
"5) " VOLDOWNCMD " ;; "
|
||||
"esac", 2 },
|
||||
/* bluetooth: izquierdo blueman, derecho encender/apagar el adaptador */
|
||||
{ "case $BUTTON in "
|
||||
"1) blueman-manager ;; "
|
||||
"3) if bluetoothctl show | grep -q 'Powered: yes'; "
|
||||
"then bluetoothctl power off; else bluetoothctl power on; fi >/dev/null" STATUSREFRESH " ;; "
|
||||
"esac", 3 },
|
||||
};
|
||||
static const char *statuscmd[] = { "/bin/sh", "-c", NULL, NULL };
|
||||
|
||||
/* Capturas: se guardan en ~/Imágenes/Capturas y se copian al portapapeles */
|
||||
#define SCREENSHOT(opts) SHCMD( \
|
||||
"d=\"$HOME/Imágenes/Capturas\"; mkdir -p \"$d\"; " \
|
||||
"f=\"$d/$(date +%Y-%m-%d_%H-%M-%S).png\"; " \
|
||||
"maim " opts " \"$f\" && xclip -selection clipboard -t image/png -i \"$f\" " \
|
||||
"&& notify-send -i \"$f\" 'Captura guardada' \"$f\"")
|
||||
|
||||
static const Key keys[] = {
|
||||
/* modifier key function argument */
|
||||
{ MODKEY, XK_p, spawn, {.v = dmenucmd } },
|
||||
{ MODKEY, XK_Return, spawn, {.v = termcmd } },
|
||||
{ MODKEY, XK_v, spawn, {.v = clipcmd } },
|
||||
{ MODKEY|ShiftMask, XK_l, spawn, {.v = lockcmd } },
|
||||
{ MODKEY, XK_b, togglebar, {0} },
|
||||
{ MODKEY, XK_j, focusstack, {.i = +1 } },
|
||||
{ MODKEY, XK_k, focusstack, {.i = -1 } },
|
||||
{ MODKEY, XK_i, incnmaster, {.i = +1 } },
|
||||
{ MODKEY, XK_d, incnmaster, {.i = -1 } },
|
||||
{ MODKEY, XK_h, setmfact, {.f = -0.05} },
|
||||
{ MODKEY, XK_l, setmfact, {.f = +0.05} },
|
||||
{ MODKEY|ShiftMask, XK_Return, zoom, {0} },
|
||||
{ MODKEY, XK_Tab, view, {0} },
|
||||
{ MODKEY, XK_q, killclient, {0} },
|
||||
{ MODKEY, XK_t, setlayout, {.v = &layouts[0]} },
|
||||
{ MODKEY, XK_f, setlayout, {.v = &layouts[1]} },
|
||||
{ MODKEY, XK_m, setlayout, {.v = &layouts[2]} },
|
||||
{ MODKEY, XK_space, setlayout, {0} },
|
||||
{ MODKEY|ShiftMask, XK_space, togglefloating, {0} },
|
||||
{ MODKEY, XK_0, view, {.ui = ~0 } },
|
||||
{ MODKEY|ShiftMask, XK_0, tag, {.ui = ~0 } },
|
||||
{ MODKEY, XK_comma, focusmon, {.i = -1 } },
|
||||
{ MODKEY, XK_period, focusmon, {.i = +1 } },
|
||||
{ MODKEY|ShiftMask, XK_comma, tagmon, {.i = -1 } },
|
||||
{ MODKEY|ShiftMask, XK_period, tagmon, {.i = +1 } },
|
||||
/* gaps: en teclado español '+' y '-' están sin Shift (XK_equal no existe sin Shift) */
|
||||
{ MODKEY, XK_minus, setgaps, {.i = -1 } },
|
||||
{ MODKEY, XK_plus, setgaps, {.i = +1 } },
|
||||
{ MODKEY|ShiftMask, XK_plus, setgaps, {.i = 0 } },
|
||||
TAGKEYS( XK_1, 0)
|
||||
TAGKEYS( XK_2, 1)
|
||||
TAGKEYS( XK_3, 2)
|
||||
TAGKEYS( XK_4, 3)
|
||||
TAGKEYS( XK_5, 4)
|
||||
TAGKEYS( XK_6, 5)
|
||||
TAGKEYS( XK_7, 6)
|
||||
TAGKEYS( XK_8, 7)
|
||||
TAGKEYS( XK_9, 8)
|
||||
{ MODKEY|ShiftMask, XK_m, quit, {0} },
|
||||
{ MODKEY|ShiftMask, XK_r, quit, {1} }, /* reiniciar dwm sin cerrar ventanas */
|
||||
|
||||
/* audio */
|
||||
{ 0, XF86XK_AudioRaiseVolume, spawn, VOLUP },
|
||||
{ 0, XF86XK_AudioLowerVolume, spawn, VOLDOWN },
|
||||
{ 0, XF86XK_AudioMute, spawn, VOLMUTE },
|
||||
{ 0, XF86XK_AudioMicMute, spawn, MICMUTE },
|
||||
{ MODKEY, XK_ntilde, spawn, MICMUTE },
|
||||
|
||||
/* capturas */
|
||||
{ 0, XK_Print, spawn, SCREENSHOT("") },
|
||||
{ ShiftMask, XK_Print, spawn, SCREENSHOT("-s") },
|
||||
};
|
||||
|
||||
/* button definitions */
|
||||
/* click can be ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, ClkClientWin, or ClkRootWin */
|
||||
static const Button buttons[] = {
|
||||
/* click event mask button function argument */
|
||||
{ ClkLtSymbol, 0, Button1, setlayout, {0} },
|
||||
{ ClkLtSymbol, 0, Button3, setlayout, {.v = &layouts[2]} },
|
||||
{ ClkWinTitle, 0, Button2, zoom, {0} },
|
||||
{ ClkStatusText, 0, Button1, spawn, {.v = statuscmd } },
|
||||
{ ClkStatusText, 0, Button2, spawn, {.v = statuscmd } },
|
||||
{ ClkStatusText, 0, Button3, spawn, {.v = statuscmd } },
|
||||
{ ClkStatusText, 0, Button4, spawn, {.v = statuscmd } },
|
||||
{ ClkStatusText, 0, Button5, spawn, {.v = statuscmd } },
|
||||
{ ClkClientWin, MODKEY, Button1, movemouse, {0} },
|
||||
{ ClkClientWin, MODKEY, Button2, togglefloating, {0} },
|
||||
{ ClkClientWin, MODKEY, Button3, resizemouse, {0} },
|
||||
{ ClkTagBar, 0, Button1, view, {0} },
|
||||
{ ClkTagBar, 0, Button3, toggleview, {0} },
|
||||
{ ClkTagBar, MODKEY, Button1, tag, {0} },
|
||||
{ ClkTagBar, MODKEY, Button3, toggletag, {0} },
|
||||
};
|
||||
|
||||
40
suckless/dwm/config.mk
Normal file
40
suckless/dwm/config.mk
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# dwm version
|
||||
VERSION = 6.8
|
||||
|
||||
# Customize below to fit your system
|
||||
|
||||
# paths
|
||||
PREFIX = /usr/local
|
||||
MANPREFIX = ${PREFIX}/share/man
|
||||
|
||||
X11INC = /usr/X11R6/include
|
||||
X11LIB = /usr/X11R6/lib
|
||||
|
||||
# Xinerama, comment if you don't want it
|
||||
XINERAMALIBS = -lXinerama
|
||||
XINERAMAFLAGS = -DXINERAMA
|
||||
|
||||
# freetype
|
||||
FREETYPELIBS = -lfontconfig -lXft
|
||||
FREETYPEINC = /usr/include/freetype2
|
||||
# OpenBSD (uncomment)
|
||||
#FREETYPEINC = ${X11INC}/freetype2
|
||||
#MANPREFIX = ${PREFIX}/man
|
||||
#KVMLIB = -lkvm
|
||||
|
||||
# includes and libs
|
||||
INCS = -I${X11INC} -I${FREETYPEINC}
|
||||
LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} ${FREETYPELIBS} -lX11-xcb -lxcb -lxcb-res ${KVMLIB}
|
||||
|
||||
# flags
|
||||
CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700L -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS}
|
||||
#CFLAGS = -g -std=c99 -pedantic -Wall -O0 ${INCS} ${CPPFLAGS}
|
||||
CFLAGS = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os ${INCS} ${CPPFLAGS}
|
||||
LDFLAGS = ${LIBS}
|
||||
|
||||
# Solaris
|
||||
#CFLAGS = -fast ${INCS} -DVERSION=\"${VERSION}\"
|
||||
#LDFLAGS = ${LIBS}
|
||||
|
||||
# compiler and linker
|
||||
CC = cc
|
||||
474
suckless/dwm/drw.c
Normal file
474
suckless/dwm/drw.c
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xft/Xft.h>
|
||||
|
||||
#include "drw.h"
|
||||
#include "util.h"
|
||||
|
||||
#define UTF_INVALID 0xFFFD
|
||||
|
||||
static int
|
||||
utf8decode(const char *s_in, long *u, int *err)
|
||||
{
|
||||
static const unsigned char lens[] = {
|
||||
/* 0XXXX */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
/* 10XXX */ 0, 0, 0, 0, 0, 0, 0, 0, /* invalid */
|
||||
/* 110XX */ 2, 2, 2, 2,
|
||||
/* 1110X */ 3, 3,
|
||||
/* 11110 */ 4,
|
||||
/* 11111 */ 0, /* invalid */
|
||||
};
|
||||
static const unsigned char leading_mask[] = { 0x7F, 0x1F, 0x0F, 0x07 };
|
||||
static const unsigned int overlong[] = { 0x0, 0x80, 0x0800, 0x10000 };
|
||||
|
||||
const unsigned char *s = (const unsigned char *)s_in;
|
||||
int len = lens[*s >> 3];
|
||||
*u = UTF_INVALID;
|
||||
*err = 1;
|
||||
if (len == 0)
|
||||
return 1;
|
||||
|
||||
long cp = s[0] & leading_mask[len - 1];
|
||||
for (int i = 1; i < len; ++i) {
|
||||
if (s[i] == '\0' || (s[i] & 0xC0) != 0x80)
|
||||
return i;
|
||||
cp = (cp << 6) | (s[i] & 0x3F);
|
||||
}
|
||||
/* out of range, surrogate, overlong encoding */
|
||||
if (cp > 0x10FFFF || (cp >> 11) == 0x1B || cp < overlong[len - 1])
|
||||
return len;
|
||||
|
||||
*err = 0;
|
||||
*u = cp;
|
||||
return len;
|
||||
}
|
||||
|
||||
Drw *
|
||||
drw_create(Display *dpy, int screen, Window root, unsigned int w, unsigned int h)
|
||||
{
|
||||
Drw *drw = ecalloc(1, sizeof(Drw));
|
||||
|
||||
drw->dpy = dpy;
|
||||
drw->screen = screen;
|
||||
drw->root = root;
|
||||
drw->w = w;
|
||||
drw->h = h;
|
||||
drw->drawable = XCreatePixmap(dpy, root, w, h, DefaultDepth(dpy, screen));
|
||||
drw->gc = XCreateGC(dpy, root, 0, NULL);
|
||||
XSetLineAttributes(dpy, drw->gc, 1, LineSolid, CapButt, JoinMiter);
|
||||
|
||||
return drw;
|
||||
}
|
||||
|
||||
void
|
||||
drw_resize(Drw *drw, unsigned int w, unsigned int h)
|
||||
{
|
||||
if (!drw)
|
||||
return;
|
||||
|
||||
drw->w = w;
|
||||
drw->h = h;
|
||||
if (drw->drawable)
|
||||
XFreePixmap(drw->dpy, drw->drawable);
|
||||
drw->drawable = XCreatePixmap(drw->dpy, drw->root, w, h, DefaultDepth(drw->dpy, drw->screen));
|
||||
}
|
||||
|
||||
void
|
||||
drw_free(Drw *drw)
|
||||
{
|
||||
XFreePixmap(drw->dpy, drw->drawable);
|
||||
XFreeGC(drw->dpy, drw->gc);
|
||||
drw_fontset_free(drw->fonts);
|
||||
free(drw);
|
||||
}
|
||||
|
||||
/* This function is an implementation detail. Library users should use
|
||||
* drw_fontset_create instead.
|
||||
*/
|
||||
static Fnt *
|
||||
xfont_create(Drw *drw, const char *fontname, FcPattern *fontpattern)
|
||||
{
|
||||
Fnt *font;
|
||||
XftFont *xfont = NULL;
|
||||
FcPattern *pattern = NULL;
|
||||
|
||||
if (fontname) {
|
||||
/* Using the pattern found at font->xfont->pattern does not yield the
|
||||
* same substitution results as using the pattern returned by
|
||||
* FcNameParse; using the latter results in the desired fallback
|
||||
* behaviour whereas the former just results in missing-character
|
||||
* rectangles being drawn, at least with some fonts. */
|
||||
if (!(xfont = XftFontOpenName(drw->dpy, drw->screen, fontname))) {
|
||||
fprintf(stderr, "error, cannot load font from name: '%s'\n", fontname);
|
||||
return NULL;
|
||||
}
|
||||
if (!(pattern = FcNameParse((FcChar8 *) fontname))) {
|
||||
fprintf(stderr, "error, cannot parse font name to pattern: '%s'\n", fontname);
|
||||
XftFontClose(drw->dpy, xfont);
|
||||
return NULL;
|
||||
}
|
||||
} else if (fontpattern) {
|
||||
if (!(xfont = XftFontOpenPattern(drw->dpy, fontpattern))) {
|
||||
fprintf(stderr, "error, cannot load font from pattern.\n");
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
die("no font specified.");
|
||||
}
|
||||
|
||||
font = ecalloc(1, sizeof(Fnt));
|
||||
font->xfont = xfont;
|
||||
font->pattern = pattern;
|
||||
font->h = xfont->ascent + xfont->descent;
|
||||
font->dpy = drw->dpy;
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
static void
|
||||
xfont_free(Fnt *font)
|
||||
{
|
||||
if (!font)
|
||||
return;
|
||||
if (font->pattern)
|
||||
FcPatternDestroy(font->pattern);
|
||||
XftFontClose(font->dpy, font->xfont);
|
||||
free(font);
|
||||
}
|
||||
|
||||
Fnt*
|
||||
drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount)
|
||||
{
|
||||
Fnt *cur, *ret = NULL;
|
||||
size_t i;
|
||||
|
||||
if (!drw || !fonts)
|
||||
return NULL;
|
||||
|
||||
for (i = 1; i <= fontcount; i++) {
|
||||
if ((cur = xfont_create(drw, fonts[fontcount - i], NULL))) {
|
||||
cur->next = ret;
|
||||
ret = cur;
|
||||
}
|
||||
}
|
||||
return (drw->fonts = ret);
|
||||
}
|
||||
|
||||
void
|
||||
drw_fontset_free(Fnt *font)
|
||||
{
|
||||
if (font) {
|
||||
drw_fontset_free(font->next);
|
||||
xfont_free(font);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
drw_clr_create(Drw *drw, Clr *dest, const char *clrname)
|
||||
{
|
||||
if (!drw || !dest || !clrname)
|
||||
return;
|
||||
|
||||
if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen),
|
||||
DefaultColormap(drw->dpy, drw->screen),
|
||||
clrname, dest))
|
||||
die("error, cannot allocate color '%s'", clrname);
|
||||
|
||||
/* alfa opaco: si no, el borde de las ventanas ARGB (st con alpha) sale transparente */
|
||||
dest->pixel |= 0xffU << 24;
|
||||
}
|
||||
|
||||
/* Create color schemes. */
|
||||
Clr *
|
||||
drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount)
|
||||
{
|
||||
size_t i;
|
||||
Clr *ret;
|
||||
|
||||
/* need at least two colors for a scheme */
|
||||
if (!drw || !clrnames || clrcount < 2 || !(ret = ecalloc(clrcount, sizeof(Clr))))
|
||||
return NULL;
|
||||
|
||||
for (i = 0; i < clrcount; i++)
|
||||
drw_clr_create(drw, &ret[i], clrnames[i]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void
|
||||
drw_clr_free(Drw *drw, Clr *c)
|
||||
{
|
||||
if (!drw || !c)
|
||||
return;
|
||||
|
||||
/* c is typedef XftColor Clr */
|
||||
XftColorFree(drw->dpy, DefaultVisual(drw->dpy, drw->screen),
|
||||
DefaultColormap(drw->dpy, drw->screen), c);
|
||||
}
|
||||
|
||||
void
|
||||
drw_scm_free(Drw *drw, Clr *scm, size_t clrcount)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (!drw || !scm)
|
||||
return;
|
||||
|
||||
for (i = 0; i < clrcount; i++)
|
||||
drw_clr_free(drw, &scm[i]);
|
||||
free(scm);
|
||||
}
|
||||
|
||||
void
|
||||
drw_setfontset(Drw *drw, Fnt *set)
|
||||
{
|
||||
if (drw)
|
||||
drw->fonts = set;
|
||||
}
|
||||
|
||||
void
|
||||
drw_setscheme(Drw *drw, Clr *scm)
|
||||
{
|
||||
if (drw)
|
||||
drw->scheme = scm;
|
||||
}
|
||||
|
||||
void
|
||||
drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert)
|
||||
{
|
||||
if (!drw || !drw->scheme)
|
||||
return;
|
||||
XSetForeground(drw->dpy, drw->gc, invert ? drw->scheme[ColBg].pixel : drw->scheme[ColFg].pixel);
|
||||
if (filled)
|
||||
XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
|
||||
else
|
||||
XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1);
|
||||
}
|
||||
|
||||
int
|
||||
drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert)
|
||||
{
|
||||
int ty, ellipsis_x = 0;
|
||||
unsigned int tmpw, ew, ellipsis_w = 0, ellipsis_len, hash, h0, h1;
|
||||
XftDraw *d = NULL;
|
||||
Fnt *usedfont, *curfont, *nextfont;
|
||||
int utf8strlen, utf8charlen, utf8err, render = x || y || w || h;
|
||||
long utf8codepoint = 0;
|
||||
const char *utf8str;
|
||||
FcCharSet *fccharset;
|
||||
FcPattern *fcpattern;
|
||||
FcPattern *match;
|
||||
XftResult result;
|
||||
int charexists = 0, overflow = 0;
|
||||
/* keep track of a couple codepoints for which we have no match. */
|
||||
static unsigned int nomatches[128], ellipsis_width, invalid_width;
|
||||
static const char invalid[] = "<EFBFBD>";
|
||||
|
||||
if (!drw || (render && (!drw->scheme || !w)) || !text || !drw->fonts)
|
||||
return 0;
|
||||
|
||||
if (!render) {
|
||||
w = invert ? invert : ~invert;
|
||||
} else {
|
||||
XSetForeground(drw->dpy, drw->gc, drw->scheme[invert ? ColFg : ColBg].pixel);
|
||||
XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
|
||||
if (w < lpad)
|
||||
return x + w;
|
||||
d = XftDrawCreate(drw->dpy, drw->drawable,
|
||||
DefaultVisual(drw->dpy, drw->screen),
|
||||
DefaultColormap(drw->dpy, drw->screen));
|
||||
x += lpad;
|
||||
w -= lpad;
|
||||
}
|
||||
|
||||
usedfont = drw->fonts;
|
||||
if (!ellipsis_width && render)
|
||||
ellipsis_width = drw_fontset_getwidth(drw, "...");
|
||||
if (!invalid_width && render)
|
||||
invalid_width = drw_fontset_getwidth(drw, invalid);
|
||||
while (1) {
|
||||
ew = ellipsis_len = utf8err = utf8charlen = utf8strlen = 0;
|
||||
utf8str = text;
|
||||
nextfont = NULL;
|
||||
while (*text) {
|
||||
utf8charlen = utf8decode(text, &utf8codepoint, &utf8err);
|
||||
for (curfont = drw->fonts; curfont; curfont = curfont->next) {
|
||||
charexists = charexists || XftCharExists(drw->dpy, curfont->xfont, utf8codepoint);
|
||||
if (charexists) {
|
||||
drw_font_getexts(curfont, text, utf8charlen, &tmpw, NULL);
|
||||
if (ew + ellipsis_width <= w) {
|
||||
/* keep track where the ellipsis still fits */
|
||||
ellipsis_x = x + ew;
|
||||
ellipsis_w = w - ew;
|
||||
ellipsis_len = utf8strlen;
|
||||
}
|
||||
|
||||
if (ew + tmpw > w) {
|
||||
overflow = 1;
|
||||
/* called from drw_fontset_getwidth_clamp():
|
||||
* it wants the width AFTER the overflow
|
||||
*/
|
||||
if (!render)
|
||||
x += tmpw;
|
||||
else
|
||||
utf8strlen = ellipsis_len;
|
||||
} else if (curfont == usedfont) {
|
||||
text += utf8charlen;
|
||||
utf8strlen += utf8err ? 0 : utf8charlen;
|
||||
ew += utf8err ? 0 : tmpw;
|
||||
} else {
|
||||
nextfont = curfont;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (overflow || !charexists || nextfont || utf8err)
|
||||
break;
|
||||
else
|
||||
charexists = 0;
|
||||
}
|
||||
|
||||
if (utf8strlen) {
|
||||
if (render) {
|
||||
ty = y + (h - usedfont->h) / 2 + usedfont->xfont->ascent;
|
||||
XftDrawStringUtf8(d, &drw->scheme[invert ? ColBg : ColFg],
|
||||
usedfont->xfont, x, ty, (XftChar8 *)utf8str, utf8strlen);
|
||||
}
|
||||
x += ew;
|
||||
w -= ew;
|
||||
}
|
||||
if (utf8err && (!render || invalid_width < w)) {
|
||||
if (render)
|
||||
drw_text(drw, x, y, w, h, 0, invalid, invert);
|
||||
x += invalid_width;
|
||||
w -= invalid_width;
|
||||
}
|
||||
if (render && overflow)
|
||||
drw_text(drw, ellipsis_x, y, ellipsis_w, h, 0, "...", invert);
|
||||
|
||||
if (!*text || overflow) {
|
||||
break;
|
||||
} else if (nextfont) {
|
||||
charexists = 0;
|
||||
usedfont = nextfont;
|
||||
} else {
|
||||
/* Regardless of whether or not a fallback font is found, the
|
||||
* character must be drawn. */
|
||||
charexists = 1;
|
||||
|
||||
hash = (unsigned int)utf8codepoint;
|
||||
hash = ((hash >> 16) ^ hash) * 0x21F0AAAD;
|
||||
hash = ((hash >> 15) ^ hash) * 0xD35A2D97;
|
||||
h0 = ((hash >> 15) ^ hash) % LENGTH(nomatches);
|
||||
h1 = (hash >> 17) % LENGTH(nomatches);
|
||||
/* avoid expensive XftFontMatch call when we know we won't find a match */
|
||||
if (nomatches[h0] == utf8codepoint || nomatches[h1] == utf8codepoint)
|
||||
goto no_match;
|
||||
|
||||
fccharset = FcCharSetCreate();
|
||||
FcCharSetAddChar(fccharset, utf8codepoint);
|
||||
|
||||
if (!drw->fonts->pattern) {
|
||||
/* Refer to the comment in xfont_create for more information. */
|
||||
die("the first font in the cache must be loaded from a font string.");
|
||||
}
|
||||
|
||||
fcpattern = FcPatternDuplicate(drw->fonts->pattern);
|
||||
FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset);
|
||||
FcPatternAddBool(fcpattern, FC_SCALABLE, FcTrue);
|
||||
|
||||
FcConfigSubstitute(NULL, fcpattern, FcMatchPattern);
|
||||
FcDefaultSubstitute(fcpattern);
|
||||
match = XftFontMatch(drw->dpy, drw->screen, fcpattern, &result);
|
||||
|
||||
FcCharSetDestroy(fccharset);
|
||||
FcPatternDestroy(fcpattern);
|
||||
|
||||
if (match) {
|
||||
usedfont = xfont_create(drw, NULL, match);
|
||||
if (usedfont && XftCharExists(drw->dpy, usedfont->xfont, utf8codepoint)) {
|
||||
for (curfont = drw->fonts; curfont->next; curfont = curfont->next)
|
||||
; /* NOP */
|
||||
curfont->next = usedfont;
|
||||
} else {
|
||||
xfont_free(usedfont);
|
||||
nomatches[nomatches[h0] ? h1 : h0] = utf8codepoint;
|
||||
no_match:
|
||||
usedfont = drw->fonts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (d)
|
||||
XftDrawDestroy(d);
|
||||
|
||||
return x + (render ? w : 0);
|
||||
}
|
||||
|
||||
void
|
||||
drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h)
|
||||
{
|
||||
if (!drw)
|
||||
return;
|
||||
|
||||
XCopyArea(drw->dpy, drw->drawable, win, drw->gc, x, y, w, h, x, y);
|
||||
XSync(drw->dpy, False);
|
||||
}
|
||||
|
||||
unsigned int
|
||||
drw_fontset_getwidth(Drw *drw, const char *text)
|
||||
{
|
||||
if (!drw || !drw->fonts || !text)
|
||||
return 0;
|
||||
return drw_text(drw, 0, 0, 0, 0, 0, text, 0);
|
||||
}
|
||||
|
||||
unsigned int
|
||||
drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n)
|
||||
{
|
||||
unsigned int tmp = 0;
|
||||
if (drw && drw->fonts && text && n)
|
||||
tmp = drw_text(drw, 0, 0, 0, 0, 0, text, n);
|
||||
return MIN(n, tmp);
|
||||
}
|
||||
|
||||
void
|
||||
drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h)
|
||||
{
|
||||
XGlyphInfo ext;
|
||||
|
||||
if (!font || !text)
|
||||
return;
|
||||
|
||||
XftTextExtentsUtf8(font->dpy, font->xfont, (XftChar8 *)text, len, &ext);
|
||||
if (w)
|
||||
*w = ext.xOff;
|
||||
if (h)
|
||||
*h = font->h;
|
||||
}
|
||||
|
||||
Cur *
|
||||
drw_cur_create(Drw *drw, int shape)
|
||||
{
|
||||
Cur *cur;
|
||||
|
||||
if (!drw || !(cur = ecalloc(1, sizeof(Cur))))
|
||||
return NULL;
|
||||
|
||||
cur->cursor = XCreateFontCursor(drw->dpy, shape);
|
||||
|
||||
return cur;
|
||||
}
|
||||
|
||||
void
|
||||
drw_cur_free(Drw *drw, Cur *cursor)
|
||||
{
|
||||
if (!cursor)
|
||||
return;
|
||||
|
||||
XFreeCursor(drw->dpy, cursor->cursor);
|
||||
free(cursor);
|
||||
}
|
||||
60
suckless/dwm/drw.h
Normal file
60
suckless/dwm/drw.h
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
|
||||
typedef struct {
|
||||
Cursor cursor;
|
||||
} Cur;
|
||||
|
||||
typedef struct Fnt {
|
||||
Display *dpy;
|
||||
unsigned int h;
|
||||
XftFont *xfont;
|
||||
FcPattern *pattern;
|
||||
struct Fnt *next;
|
||||
} Fnt;
|
||||
|
||||
enum { ColFg, ColBg, ColBorder }; /* Clr scheme index */
|
||||
typedef XftColor Clr;
|
||||
|
||||
typedef struct {
|
||||
unsigned int w, h;
|
||||
Display *dpy;
|
||||
int screen;
|
||||
Window root;
|
||||
Drawable drawable;
|
||||
GC gc;
|
||||
Clr *scheme;
|
||||
Fnt *fonts;
|
||||
} Drw;
|
||||
|
||||
/* Drawable abstraction */
|
||||
Drw *drw_create(Display *dpy, int screen, Window win, unsigned int w, unsigned int h);
|
||||
void drw_resize(Drw *drw, unsigned int w, unsigned int h);
|
||||
void drw_free(Drw *drw);
|
||||
|
||||
/* Fnt abstraction */
|
||||
Fnt *drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount);
|
||||
void drw_fontset_free(Fnt* set);
|
||||
unsigned int drw_fontset_getwidth(Drw *drw, const char *text);
|
||||
unsigned int drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n);
|
||||
void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h);
|
||||
|
||||
/* Colorscheme abstraction */
|
||||
void drw_clr_create(Drw *drw, Clr *dest, const char *clrname);
|
||||
void drw_clr_free(Drw *drw, Clr *c);
|
||||
Clr *drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount);
|
||||
void drw_scm_free(Drw *drw, Clr *scm, size_t clrcount);
|
||||
|
||||
/* Cursor abstraction */
|
||||
Cur *drw_cur_create(Drw *drw, int shape);
|
||||
void drw_cur_free(Drw *drw, Cur *cursor);
|
||||
|
||||
/* Drawing context manipulation */
|
||||
void drw_setfontset(Drw *drw, Fnt *set);
|
||||
void drw_setscheme(Drw *drw, Clr *scm);
|
||||
|
||||
/* Drawing functions */
|
||||
void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert);
|
||||
int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert);
|
||||
|
||||
/* Map functions */
|
||||
void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h);
|
||||
186
suckless/dwm/dwm.1
Normal file
186
suckless/dwm/dwm.1
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
.TH DWM 1 dwm\-VERSION
|
||||
.SH NAME
|
||||
dwm \- dynamic window manager
|
||||
.SH SYNOPSIS
|
||||
.B dwm
|
||||
.RB [ \-v ]
|
||||
.SH DESCRIPTION
|
||||
dwm is a dynamic window manager for X. It manages windows in tiled, monocle
|
||||
and floating layouts. Either layout can be applied dynamically, optimising the
|
||||
environment for the application in use and the task performed.
|
||||
.P
|
||||
In tiled layouts windows are managed in a master and stacking area. The master
|
||||
area on the left contains one window by default, and the stacking area on the
|
||||
right contains all other windows. The number of master area windows can be
|
||||
adjusted from zero to an arbitrary number. In monocle layout all windows are
|
||||
maximised to the screen size. In floating layout windows can be resized and
|
||||
moved freely. Dialog windows are always managed floating, regardless of the
|
||||
layout applied.
|
||||
.P
|
||||
Windows are grouped by tags. Each window can be tagged with one or multiple
|
||||
tags. Selecting certain tags displays all windows with these tags.
|
||||
.P
|
||||
Each screen contains a small status bar which displays all available tags, the
|
||||
layout, the title of the focused window, and the text read from the root window
|
||||
name property, if the screen is focused. A floating window is indicated with an
|
||||
empty square and a maximised floating window is indicated with a filled square
|
||||
before the windows title. The selected tags are indicated with a different
|
||||
color. The tags of the focused window are indicated with a filled square in the
|
||||
top left corner. The tags which are applied to one or more windows are
|
||||
indicated with an empty square in the top left corner.
|
||||
.P
|
||||
dwm draws a small border around windows to indicate the focus state.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-v
|
||||
prints version information to stderr, then exits.
|
||||
.SH USAGE
|
||||
.SS Status bar
|
||||
.TP
|
||||
.B X root window name
|
||||
is read and displayed in the status text area. It can be set with the
|
||||
.BR xsetroot (1)
|
||||
command.
|
||||
.TP
|
||||
.B Button1
|
||||
click on a tag label to display all windows with that tag, click on the layout
|
||||
label toggles between tiled and floating layout.
|
||||
.TP
|
||||
.B Button3
|
||||
click on a tag label adds/removes all windows with that tag to/from the view.
|
||||
.TP
|
||||
.B Mod1\-Button1
|
||||
click on a tag label applies that tag to the focused window.
|
||||
.TP
|
||||
.B Mod1\-Button3
|
||||
click on a tag label adds/removes that tag to/from the focused window.
|
||||
.SS Keyboard commands
|
||||
.TP
|
||||
.B Mod1\-Shift\-Return
|
||||
Start
|
||||
.BR st(1).
|
||||
.TP
|
||||
.B Mod1\-p
|
||||
Spawn
|
||||
.BR dmenu(1)
|
||||
for launching other programs.
|
||||
.TP
|
||||
.B Mod1\-,
|
||||
Focus previous screen, if any.
|
||||
.TP
|
||||
.B Mod1\-.
|
||||
Focus next screen, if any.
|
||||
.TP
|
||||
.B Mod1\-Shift\-,
|
||||
Send focused window to previous screen, if any.
|
||||
.TP
|
||||
.B Mod1\-Shift\-.
|
||||
Send focused window to next screen, if any.
|
||||
.TP
|
||||
.B Mod1\-b
|
||||
Toggles bar on and off.
|
||||
.TP
|
||||
.B Mod1\-t
|
||||
Sets tiled layout.
|
||||
.TP
|
||||
.B Mod1\-f
|
||||
Sets floating layout.
|
||||
.TP
|
||||
.B Mod1\-m
|
||||
Sets monocle layout.
|
||||
.TP
|
||||
.B Mod1\-space
|
||||
Toggles between current and previous layout.
|
||||
.TP
|
||||
.B Mod1\-j
|
||||
Focus next window.
|
||||
.TP
|
||||
.B Mod1\-k
|
||||
Focus previous window.
|
||||
.TP
|
||||
.B Mod1\-i
|
||||
Increase number of windows in master area.
|
||||
.TP
|
||||
.B Mod1\-d
|
||||
Decrease number of windows in master area.
|
||||
.TP
|
||||
.B Mod1\-l
|
||||
Increase master area size.
|
||||
.TP
|
||||
.B Mod1\-h
|
||||
Decrease master area size.
|
||||
.TP
|
||||
.B Mod1\-Return
|
||||
Zooms/cycles focused window to/from master area (tiled layouts only).
|
||||
.TP
|
||||
.B Mod1\-Shift\-c
|
||||
Close focused window.
|
||||
.TP
|
||||
.B Mod1\-Shift\-space
|
||||
Toggle focused window between tiled and floating state.
|
||||
.TP
|
||||
.B Mod1\-Tab
|
||||
Toggles to the previously selected tags.
|
||||
.TP
|
||||
.B Mod1\-Shift\-[1..n]
|
||||
Apply nth tag to focused window.
|
||||
.TP
|
||||
.B Mod1\-Shift\-0
|
||||
Apply all tags to focused window.
|
||||
.TP
|
||||
.B Mod1\-Control\-Shift\-[1..n]
|
||||
Add/remove nth tag to/from focused window.
|
||||
.TP
|
||||
.B Mod1\-[1..n]
|
||||
View all windows with nth tag.
|
||||
.TP
|
||||
.B Mod1\-0
|
||||
View all windows with any tag.
|
||||
.TP
|
||||
.B Mod1\-Control\-[1..n]
|
||||
Add/remove all windows with nth tag to/from the view.
|
||||
.TP
|
||||
.B Mod1\-Shift\-q
|
||||
Quit dwm.
|
||||
.TP
|
||||
.B Mod1\-Control\-Shift\-q
|
||||
Restart dwm.
|
||||
.SS Mouse commands
|
||||
.TP
|
||||
.B Mod1\-Button1
|
||||
Move focused window while dragging. Tiled windows will be toggled to the floating state.
|
||||
.TP
|
||||
.B Mod1\-Button2
|
||||
Toggles focused window between floating and tiled state.
|
||||
.TP
|
||||
.B Mod1\-Button3
|
||||
Resize focused window while dragging. Tiled windows will be toggled to the floating state.
|
||||
.SH CUSTOMIZATION
|
||||
dwm is customized by creating a custom config.h and (re)compiling the source
|
||||
code. This keeps it fast, secure and simple.
|
||||
.SH SIGNALS
|
||||
.TP
|
||||
.B SIGHUP - 1
|
||||
Restart the dwm process.
|
||||
.TP
|
||||
.B SIGTERM - 15
|
||||
Cleanly terminate the dwm process.
|
||||
.SH SEE ALSO
|
||||
.BR dmenu (1),
|
||||
.BR st (1)
|
||||
.SH ISSUES
|
||||
Java applications which use the XToolkit/XAWT backend may draw grey windows
|
||||
only. The XToolkit/XAWT backend breaks ICCCM-compliance in recent JDK 1.5 and early
|
||||
JDK 1.6 versions, because it assumes a reparenting window manager. Possible workarounds
|
||||
are using JDK 1.4 (which doesn't contain the XToolkit/XAWT backend) or setting the
|
||||
environment variable
|
||||
.BR AWT_TOOLKIT=MToolkit
|
||||
(to use the older Motif backend instead) or running
|
||||
.B xprop -root -f _NET_WM_NAME 32a -set _NET_WM_NAME LG3D
|
||||
or
|
||||
.B wmname LG3D
|
||||
(to pretend that a non-reparenting window manager is running that the
|
||||
XToolkit/XAWT backend can recognize) or when using OpenJDK setting the environment variable
|
||||
.BR _JAVA_AWT_WM_NONREPARENTING=1 .
|
||||
.SH BUGS
|
||||
Send all bug reports with a patch to hackers@suckless.org.
|
||||
2541
suckless/dwm/dwm.c
Normal file
2541
suckless/dwm/dwm.c
Normal file
File diff suppressed because it is too large
Load diff
BIN
suckless/dwm/dwm.png
Normal file
BIN
suckless/dwm/dwm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 373 B |
94
suckless/dwm/patches/dwm-fullgaps-6.4.diff
Normal file
94
suckless/dwm/patches/dwm-fullgaps-6.4.diff
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
diff -up a/config.def.h b/config.def.h
|
||||
--- a/config.def.h
|
||||
+++ b/config.def.h
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
/* appearance */
|
||||
static const unsigned int borderpx = 1; /* border pixel of windows */
|
||||
+static const unsigned int gappx = 5; /* gaps between windows */
|
||||
static const unsigned int snap = 32; /* snap pixel */
|
||||
static const int showbar = 1; /* 0 means no bar */
|
||||
static const int topbar = 1; /* 0 means bottom bar */
|
||||
@@ -85,6 +86,9 @@ static const Key keys[] = {
|
||||
{ MODKEY, XK_period, focusmon, {.i = +1 } },
|
||||
{ MODKEY|ShiftMask, XK_comma, tagmon, {.i = -1 } },
|
||||
{ MODKEY|ShiftMask, XK_period, tagmon, {.i = +1 } },
|
||||
+ { MODKEY, XK_minus, setgaps, {.i = -1 } },
|
||||
+ { MODKEY, XK_equal, setgaps, {.i = +1 } },
|
||||
+ { MODKEY|ShiftMask, XK_equal, setgaps, {.i = 0 } },
|
||||
TAGKEYS( XK_1, 0)
|
||||
TAGKEYS( XK_2, 1)
|
||||
TAGKEYS( XK_3, 2)
|
||||
diff -up a/dwm.c b/dwm.c
|
||||
--- a/dwm.c 2023-04-30
|
||||
+++ b/dwm.c 2023-04-30
|
||||
@@ -119,6 +119,7 @@ struct Monitor {
|
||||
int by; /* bar geometry */
|
||||
int mx, my, mw, mh; /* screen size */
|
||||
int wx, wy, ww, wh; /* window area */
|
||||
+ int gappx; /* gaps between windows */
|
||||
unsigned int seltags;
|
||||
unsigned int sellt;
|
||||
unsigned int tagset[2];
|
||||
@@ -200,6 +201,7 @@ static void sendmon(Client *c, Monitor *
|
||||
static void setclientstate(Client *c, long state);
|
||||
static void setfocus(Client *c);
|
||||
static void setfullscreen(Client *c, int fullscreen);
|
||||
+static void setgaps(const Arg *arg);
|
||||
static void setlayout(const Arg *arg);
|
||||
static void setmfact(const Arg *arg);
|
||||
static void setup(void);
|
||||
@@ -641,6 +643,7 @@ createmon(void)
|
||||
m->nmaster = nmaster;
|
||||
m->showbar = showbar;
|
||||
m->topbar = topbar;
|
||||
+ m->gappx = gappx;
|
||||
m->lt[0] = &layouts[0];
|
||||
m->lt[1] = &layouts[1 % LENGTH(layouts)];
|
||||
strncpy(m->ltsymbol, layouts[0].symbol, sizeof m->ltsymbol);
|
||||
@@ -1508,6 +1511,16 @@ setfullscreen(Client *c, int fullscreen)
|
||||
}
|
||||
|
||||
void
|
||||
+setgaps(const Arg *arg)
|
||||
+{
|
||||
+ if ((arg->i == 0) || (selmon->gappx + arg->i < 0))
|
||||
+ selmon->gappx = 0;
|
||||
+ else
|
||||
+ selmon->gappx += arg->i;
|
||||
+ arrange(selmon);
|
||||
+}
|
||||
+
|
||||
+void
|
||||
setlayout(const Arg *arg)
|
||||
{
|
||||
if (!arg || !arg->v || arg->v != selmon->lt[selmon->sellt])
|
||||
@@ -1697,18 +1710,18 @@ tile(Monitor *m)
|
||||
if (n > m->nmaster)
|
||||
mw = m->nmaster ? m->ww * m->mfact : 0;
|
||||
else
|
||||
- mw = m->ww;
|
||||
- for (i = my = ty = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), i++)
|
||||
- if (i < m->nmaster) {
|
||||
- h = (m->wh - my) / (MIN(n, m->nmaster) - i);
|
||||
- resize(c, m->wx, m->wy + my, mw - (2*c->bw), h - (2*c->bw), 0);
|
||||
- if (my + HEIGHT(c) < m->wh)
|
||||
- my += HEIGHT(c);
|
||||
+ mw = m->ww - m->gappx;
|
||||
+ for (i = 0, my = ty = m->gappx, c = nexttiled(m->clients); c; c = nexttiled(c->next), i++)
|
||||
+ if (i < m->nmaster) {
|
||||
+ h = (m->wh - my) / (MIN(n, m->nmaster) - i) - m->gappx;
|
||||
+ resize(c, m->wx + m->gappx, m->wy + my, mw - (2*c->bw) - m->gappx, h - (2*c->bw), 0);
|
||||
+ if (my + HEIGHT(c) + m->gappx < m->wh)
|
||||
+ my += HEIGHT(c) + m->gappx;
|
||||
} else {
|
||||
- h = (m->wh - ty) / (n - i);
|
||||
- resize(c, m->wx + mw, m->wy + ty, m->ww - mw - (2*c->bw), h - (2*c->bw), 0);
|
||||
- if (ty + HEIGHT(c) < m->wh)
|
||||
- ty += HEIGHT(c);
|
||||
+ h = (m->wh - ty) / (n - i) - m->gappx;
|
||||
+ resize(c, m->wx + mw + m->gappx, m->wy + ty, m->ww - mw - (2*c->bw) - 2*m->gappx, h - (2*c->bw), 0);
|
||||
+ if (ty + HEIGHT(c) + m->gappx < m->wh)
|
||||
+ ty += HEIGHT(c) + m->gappx;
|
||||
}
|
||||
}
|
||||
118
suckless/dwm/patches/dwm-preserveonrestart-6.3.diff
Normal file
118
suckless/dwm/patches/dwm-preserveonrestart-6.3.diff
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
From 713fa8650f5a20006451ebcccf57a4512e83bae8 Mon Sep 17 00:00:00 2001
|
||||
From: Arda Atci <arda@phytech.io>
|
||||
Date: Wed, 18 May 2022 17:23:16 +0300
|
||||
Subject: [PATCH] preserve clients on old tags when renewing dwm
|
||||
|
||||
By default, when dwm is recompiled-restarted all clients will
|
||||
lose it's current tag and collapse to first tag. This patch preserves
|
||||
clients on old tags, however note that layout order is not preserved.
|
||||
|
||||
---
|
||||
dwm.c | 38 +++++++++++++++++++++++++++++++++++++-
|
||||
1 file changed, 37 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/dwm.c b/dwm.c
|
||||
index a96f33c..a12e0bd 100644
|
||||
--- a/dwm.c
|
||||
+++ b/dwm.c
|
||||
@@ -62,7 +62,7 @@ enum { CurNormal, CurResize, CurMove, CurLast }; /* cursor */
|
||||
enum { SchemeNorm, SchemeSel }; /* color schemes */
|
||||
enum { NetSupported, NetWMName, NetWMState, NetWMCheck,
|
||||
NetWMFullscreen, NetActiveWindow, NetWMWindowType,
|
||||
- NetWMWindowTypeDialog, NetClientList, NetLast }; /* EWMH atoms */
|
||||
+ NetWMWindowTypeDialog, NetClientList, NetClientInfo, NetLast }; /* EWMH atoms */
|
||||
enum { WMProtocols, WMDelete, WMState, WMTakeFocus, WMLast }; /* default atoms */
|
||||
enum { ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle,
|
||||
ClkClientWin, ClkRootWin, ClkLast }; /* clicks */
|
||||
@@ -198,6 +198,7 @@ static void scan(void);
|
||||
static int sendevent(Client *c, Atom proto);
|
||||
static void sendmon(Client *c, Monitor *m);
|
||||
static void setclientstate(Client *c, long state);
|
||||
+static void setclienttagprop(Client *c);
|
||||
static void setfocus(Client *c);
|
||||
static void setfullscreen(Client *c, int fullscreen);
|
||||
static void setlayout(const Arg *arg);
|
||||
@@ -1060,6 +1061,26 @@ manage(Window w, XWindowAttributes *wa)
|
||||
updatewindowtype(c);
|
||||
updatesizehints(c);
|
||||
updatewmhints(c);
|
||||
+ {
|
||||
+ int format;
|
||||
+ unsigned long *data, n, extra;
|
||||
+ Monitor *m;
|
||||
+ Atom atom;
|
||||
+ if (XGetWindowProperty(dpy, c->win, netatom[NetClientInfo], 0L, 2L, False, XA_CARDINAL,
|
||||
+ &atom, &format, &n, &extra, (unsigned char **)&data) == Success && n == 2) {
|
||||
+ c->tags = *data;
|
||||
+ for (m = mons; m; m = m->next) {
|
||||
+ if (m->num == *(data+1)) {
|
||||
+ c->mon = m;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ if (n > 0)
|
||||
+ XFree(data);
|
||||
+ }
|
||||
+ setclienttagprop(c);
|
||||
+
|
||||
XSelectInput(dpy, w, EnterWindowMask|FocusChangeMask|PropertyChangeMask|StructureNotifyMask);
|
||||
grabbuttons(c, 0);
|
||||
if (!c->isfloating)
|
||||
@@ -1423,6 +1444,7 @@ sendmon(Client *c, Monitor *m)
|
||||
c->tags = m->tagset[m->seltags]; /* assign tags of target monitor */
|
||||
attach(c);
|
||||
attachstack(c);
|
||||
+ setclienttagprop(c);
|
||||
focus(NULL);
|
||||
arrange(NULL);
|
||||
}
|
||||
@@ -1566,6 +1588,7 @@ setup(void)
|
||||
netatom[NetWMWindowType] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False);
|
||||
netatom[NetWMWindowTypeDialog] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False);
|
||||
netatom[NetClientList] = XInternAtom(dpy, "_NET_CLIENT_LIST", False);
|
||||
+ netatom[NetClientInfo] = XInternAtom(dpy, "_NET_CLIENT_INFO", False);
|
||||
/* init cursors */
|
||||
cursor[CurNormal] = drw_cur_create(drw, XC_left_ptr);
|
||||
cursor[CurResize] = drw_cur_create(drw, XC_sizing);
|
||||
@@ -1589,6 +1612,7 @@ setup(void)
|
||||
XChangeProperty(dpy, root, netatom[NetSupported], XA_ATOM, 32,
|
||||
PropModeReplace, (unsigned char *) netatom, NetLast);
|
||||
XDeleteProperty(dpy, root, netatom[NetClientList]);
|
||||
+ XDeleteProperty(dpy, root, netatom[NetClientInfo]);
|
||||
/* select events */
|
||||
wa.cursor = cursor[CurNormal]->cursor;
|
||||
wa.event_mask = SubstructureRedirectMask|SubstructureNotifyMask
|
||||
@@ -1656,11 +1680,22 @@ spawn(const Arg *arg)
|
||||
}
|
||||
}
|
||||
|
||||
+void
|
||||
+setclienttagprop(Client *c)
|
||||
+{
|
||||
+ long data[] = { (long) c->tags, (long) c->mon->num };
|
||||
+ XChangeProperty(dpy, c->win, netatom[NetClientInfo], XA_CARDINAL, 32,
|
||||
+ PropModeReplace, (unsigned char *) data, 2);
|
||||
+}
|
||||
+
|
||||
void
|
||||
tag(const Arg *arg)
|
||||
{
|
||||
+ Client *c;
|
||||
if (selmon->sel && arg->ui & TAGMASK) {
|
||||
+ c = selmon->sel;
|
||||
selmon->sel->tags = arg->ui & TAGMASK;
|
||||
+ setclienttagprop(c);
|
||||
focus(NULL);
|
||||
arrange(selmon);
|
||||
}
|
||||
@@ -1735,6 +1770,7 @@ toggletag(const Arg *arg)
|
||||
newtags = selmon->sel->tags ^ (arg->ui & TAGMASK);
|
||||
if (newtags) {
|
||||
selmon->sel->tags = newtags;
|
||||
+ setclienttagprop(selmon->sel);
|
||||
focus(NULL);
|
||||
arrange(selmon);
|
||||
}
|
||||
--
|
||||
2.36.1
|
||||
139
suckless/dwm/patches/dwm-restartsig-20180523-6.2.diff
Normal file
139
suckless/dwm/patches/dwm-restartsig-20180523-6.2.diff
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
From 2991f37f0aaf44b9f9b11e7893ff0af8eb88f649 Mon Sep 17 00:00:00 2001
|
||||
From: Christopher Drelich <cd@cdrakka.com>
|
||||
Date: Wed, 23 May 2018 22:50:38 -0400
|
||||
Subject: [PATCH] Modifies quit to handle restarts and adds SIGHUP and SIGTERM
|
||||
handlers.
|
||||
|
||||
Modified quit() to restart if it receives arg .i = 1
|
||||
MOD+CTRL+SHIFT+Q was added to confid.def.h to do just that.
|
||||
|
||||
Signal handlers were handled for SIGHUP and SIGTERM.
|
||||
If dwm receives these signals it calls quit() with
|
||||
arg .i = to 1 or 0, respectively.
|
||||
|
||||
To restart dwm:
|
||||
MOD+CTRL+SHIFT+Q
|
||||
or
|
||||
kill -HUP dwmpid
|
||||
|
||||
To quit dwm cleanly:
|
||||
MOD+SHIFT+Q
|
||||
or
|
||||
kill -TERM dwmpid
|
||||
---
|
||||
config.def.h | 1 +
|
||||
dwm.1 | 10 ++++++++++
|
||||
dwm.c | 22 ++++++++++++++++++++++
|
||||
3 files changed, 33 insertions(+)
|
||||
|
||||
diff --git a/config.def.h b/config.def.h
|
||||
index a9ac303..e559429 100644
|
||||
--- a/config.def.h
|
||||
+++ b/config.def.h
|
||||
@@ -94,6 +94,7 @@ static Key keys[] = {
|
||||
TAGKEYS( XK_8, 7)
|
||||
TAGKEYS( XK_9, 8)
|
||||
{ MODKEY|ShiftMask, XK_q, quit, {0} },
|
||||
+ { MODKEY|ControlMask|ShiftMask, XK_q, quit, {1} },
|
||||
};
|
||||
|
||||
/* button definitions */
|
||||
diff --git a/dwm.1 b/dwm.1
|
||||
index 13b3729..36a331c 100644
|
||||
--- a/dwm.1
|
||||
+++ b/dwm.1
|
||||
@@ -142,6 +142,9 @@ Add/remove all windows with nth tag to/from the view.
|
||||
.TP
|
||||
.B Mod1\-Shift\-q
|
||||
Quit dwm.
|
||||
+.TP
|
||||
+.B Mod1\-Control\-Shift\-q
|
||||
+Restart dwm.
|
||||
.SS Mouse commands
|
||||
.TP
|
||||
.B Mod1\-Button1
|
||||
@@ -155,6 +158,13 @@ Resize focused window while dragging. Tiled windows will be toggled to the float
|
||||
.SH CUSTOMIZATION
|
||||
dwm is customized by creating a custom config.h and (re)compiling the source
|
||||
code. This keeps it fast, secure and simple.
|
||||
+.SH SIGNALS
|
||||
+.TP
|
||||
+.B SIGHUP - 1
|
||||
+Restart the dwm process.
|
||||
+.TP
|
||||
+.B SIGTERM - 15
|
||||
+Cleanly terminate the dwm process.
|
||||
.SH SEE ALSO
|
||||
.BR dmenu (1),
|
||||
.BR st (1)
|
||||
diff --git a/dwm.c b/dwm.c
|
||||
index bb95e26..286eecd 100644
|
||||
--- a/dwm.c
|
||||
+++ b/dwm.c
|
||||
@@ -205,6 +205,8 @@ static void setup(void);
|
||||
static void seturgent(Client *c, int urg);
|
||||
static void showhide(Client *c);
|
||||
static void sigchld(int unused);
|
||||
+static void sighup(int unused);
|
||||
+static void sigterm(int unused);
|
||||
static void spawn(const Arg *arg);
|
||||
static void tag(const Arg *arg);
|
||||
static void tagmon(const Arg *arg);
|
||||
@@ -260,6 +262,7 @@ static void (*handler[LASTEvent]) (XEvent *) = {
|
||||
[UnmapNotify] = unmapnotify
|
||||
};
|
||||
static Atom wmatom[WMLast], netatom[NetLast];
|
||||
+static int restart = 0;
|
||||
static int running = 1;
|
||||
static Cur *cursor[CurLast];
|
||||
static Clr **scheme;
|
||||
@@ -1248,6 +1251,7 @@ propertynotify(XEvent *e)
|
||||
void
|
||||
quit(const Arg *arg)
|
||||
{
|
||||
+ if(arg->i) restart = 1;
|
||||
running = 0;
|
||||
}
|
||||
|
||||
@@ -1536,6 +1540,9 @@ setup(void)
|
||||
/* clean up any zombies immediately */
|
||||
sigchld(0);
|
||||
|
||||
+ signal(SIGHUP, sighup);
|
||||
+ signal(SIGTERM, sigterm);
|
||||
+
|
||||
/* init screen */
|
||||
screen = DefaultScreen(dpy);
|
||||
sw = DisplayWidth(dpy, screen);
|
||||
@@ -1637,6 +1644,20 @@ sigchld(int unused)
|
||||
}
|
||||
|
||||
void
|
||||
+sighup(int unused)
|
||||
+{
|
||||
+ Arg a = {.i = 1};
|
||||
+ quit(&a);
|
||||
+}
|
||||
+
|
||||
+void
|
||||
+sigterm(int unused)
|
||||
+{
|
||||
+ Arg a = {.i = 0};
|
||||
+ quit(&a);
|
||||
+}
|
||||
+
|
||||
+void
|
||||
spawn(const Arg *arg)
|
||||
{
|
||||
if (arg->v == dmenucmd)
|
||||
@@ -2139,6 +2160,7 @@ main(int argc, char *argv[])
|
||||
setup();
|
||||
scan();
|
||||
run();
|
||||
+ if(restart) execvp(argv[0], argv);
|
||||
cleanup();
|
||||
XCloseDisplay(dpy);
|
||||
return EXIT_SUCCESS;
|
||||
--
|
||||
2.7.4
|
||||
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
From 02c4a28dd7f3a88eef3a4e533ace35f79cf09d57 Mon Sep 17 00:00:00 2001
|
||||
From: Daniel Bylinka <daniel.bylinka@gmail.com>
|
||||
Date: Fri, 2 Apr 2021 19:34:38 +0200
|
||||
Subject: [PATCH] [statuscmd] Run shell commands based on mouse location and
|
||||
button
|
||||
|
||||
---
|
||||
config.def.h | 10 ++++++-
|
||||
dwm.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++++---
|
||||
2 files changed, 81 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/config.def.h b/config.def.h
|
||||
index 1c0b587..8f88366 100644
|
||||
--- a/config.def.h
|
||||
+++ b/config.def.h
|
||||
@@ -59,6 +59,12 @@ static char dmenumon[2] = "0"; /* component of dmenucmd, manipulated in spawn()
|
||||
static const char *dmenucmd[] = { "dmenu_run", "-m", dmenumon, "-fn", dmenufont, "-nb", col_gray1, "-nf", col_gray3, "-sb", col_cyan, "-sf", col_gray4, NULL };
|
||||
static const char *termcmd[] = { "st", NULL };
|
||||
|
||||
+/* commands spawned when clicking statusbar, the mouse button pressed is exported as BUTTON */
|
||||
+static const StatusCmd statuscmds[] = {
|
||||
+ { "notify-send Mouse$BUTTON", 1 },
|
||||
+};
|
||||
+static const char *statuscmd[] = { "/bin/sh", "-c", NULL, NULL };
|
||||
+
|
||||
static Key keys[] = {
|
||||
/* modifier key function argument */
|
||||
{ MODKEY, XK_p, spawn, {.v = dmenucmd } },
|
||||
@@ -103,7 +109,9 @@ static Button buttons[] = {
|
||||
{ ClkLtSymbol, 0, Button1, setlayout, {0} },
|
||||
{ ClkLtSymbol, 0, Button3, setlayout, {.v = &layouts[2]} },
|
||||
{ ClkWinTitle, 0, Button2, zoom, {0} },
|
||||
- { ClkStatusText, 0, Button2, spawn, {.v = termcmd } },
|
||||
+ { ClkStatusText, 0, Button1, spawn, {.v = statuscmd } },
|
||||
+ { ClkStatusText, 0, Button2, spawn, {.v = statuscmd } },
|
||||
+ { ClkStatusText, 0, Button3, spawn, {.v = statuscmd } },
|
||||
{ ClkClientWin, MODKEY, Button1, movemouse, {0} },
|
||||
{ ClkClientWin, MODKEY, Button2, togglefloating, {0} },
|
||||
{ ClkClientWin, MODKEY, Button3, resizemouse, {0} },
|
||||
diff --git a/dwm.c b/dwm.c
|
||||
index b0b3466..eb478a5 100644
|
||||
--- a/dwm.c
|
||||
+++ b/dwm.c
|
||||
@@ -141,6 +141,11 @@ typedef struct {
|
||||
int monitor;
|
||||
} Rule;
|
||||
|
||||
+typedef struct {
|
||||
+ const char *cmd;
|
||||
+ int id;
|
||||
+} StatusCmd;
|
||||
+
|
||||
/* function declarations */
|
||||
static void applyrules(Client *c);
|
||||
static int applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact);
|
||||
@@ -238,6 +243,9 @@ static void zoom(const Arg *arg);
|
||||
/* variables */
|
||||
static const char broken[] = "broken";
|
||||
static char stext[256];
|
||||
+static int statusw;
|
||||
+static int statuscmdn;
|
||||
+static char lastbutton[] = "-";
|
||||
static int screen;
|
||||
static int sw, sh; /* X display screen geometry width, height */
|
||||
static int bh, blw = 0; /* bar geometry */
|
||||
@@ -440,8 +448,27 @@ buttonpress(XEvent *e)
|
||||
arg.ui = 1 << i;
|
||||
} else if (ev->x < x + blw)
|
||||
click = ClkLtSymbol;
|
||||
- else if (ev->x > selmon->ww - (int)TEXTW(stext))
|
||||
+ else if (ev->x > selmon->ww - statusw) {
|
||||
+ char *text, *s, ch;
|
||||
+ *lastbutton = '0' + ev->button;
|
||||
+
|
||||
+ x = selmon->ww - statusw;
|
||||
click = ClkStatusText;
|
||||
+
|
||||
+ statuscmdn = 0;
|
||||
+ for (text = s = stext; *s && x <= ev->x; s++) {
|
||||
+ if ((unsigned char)(*s) < ' ') {
|
||||
+ ch = *s;
|
||||
+ *s = '\0';
|
||||
+ x += TEXTW(text) - lrpad;
|
||||
+ *s = ch;
|
||||
+ text = s + 1;
|
||||
+ if (x >= ev->x)
|
||||
+ break;
|
||||
+ statuscmdn = ch;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
else
|
||||
click = ClkWinTitle;
|
||||
} else if ((c = wintoclient(ev->window))) {
|
||||
@@ -704,9 +731,24 @@ drawbar(Monitor *m)
|
||||
|
||||
/* draw status first so it can be overdrawn by tags later */
|
||||
if (m == selmon) { /* status is only drawn on selected monitor */
|
||||
+ char *text, *s, ch;
|
||||
drw_setscheme(drw, scheme[SchemeNorm]);
|
||||
- tw = TEXTW(stext) - lrpad + 2; /* 2px right padding */
|
||||
- drw_text(drw, m->ww - tw, 0, tw, bh, 0, stext, 0);
|
||||
+
|
||||
+ x = 0;
|
||||
+ for (text = s = stext; *s; s++) {
|
||||
+ if ((unsigned char)(*s) < ' ') {
|
||||
+ ch = *s;
|
||||
+ *s = '\0';
|
||||
+ tw = TEXTW(text) - lrpad;
|
||||
+ drw_text(drw, m->ww - statusw + x, 0, tw, bh, 0, text, 0);
|
||||
+ x += tw;
|
||||
+ *s = ch;
|
||||
+ text = s + 1;
|
||||
+ }
|
||||
+ }
|
||||
+ tw = TEXTW(text) - lrpad + 2;
|
||||
+ drw_text(drw, m->ww - statusw + x, 0, tw, bh, 0, text, 0);
|
||||
+ tw = statusw;
|
||||
}
|
||||
|
||||
for (c = m->clients; c; c = c->next) {
|
||||
@@ -1645,6 +1687,17 @@ spawn(const Arg *arg)
|
||||
if (fork() == 0) {
|
||||
if (dpy)
|
||||
close(ConnectionNumber(dpy));
|
||||
+ if (arg->v == statuscmd) {
|
||||
+ for (int i = 0; i < LENGTH(statuscmds); i++) {
|
||||
+ if (statuscmdn == statuscmds[i].id) {
|
||||
+ statuscmd[2] = statuscmds[i].cmd;
|
||||
+ setenv("BUTTON", lastbutton, 1);
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ if (!statuscmd[2])
|
||||
+ exit(EXIT_SUCCESS);
|
||||
+ }
|
||||
setsid();
|
||||
execvp(((char **)arg->v)[0], (char **)arg->v);
|
||||
fprintf(stderr, "dwm: execvp %s", ((char **)arg->v)[0]);
|
||||
@@ -1990,8 +2043,23 @@ updatesizehints(Client *c)
|
||||
void
|
||||
updatestatus(void)
|
||||
{
|
||||
- if (!gettextprop(root, XA_WM_NAME, stext, sizeof(stext)))
|
||||
+ if (!gettextprop(root, XA_WM_NAME, stext, sizeof(stext))) {
|
||||
strcpy(stext, "dwm-"VERSION);
|
||||
+ statusw = TEXTW(stext) - lrpad + 2;
|
||||
+ } else {
|
||||
+ char *text, *s, ch;
|
||||
+ statusw = 0;
|
||||
+ for (text = s = stext; *s; s++) {
|
||||
+ if ((unsigned char)(*s) < ' ') {
|
||||
+ ch = *s;
|
||||
+ *s = '\0';
|
||||
+ statusw += TEXTW(text) - lrpad;
|
||||
+ *s = ch;
|
||||
+ text = s + 1;
|
||||
+ }
|
||||
+ }
|
||||
+ statusw += TEXTW(text) - lrpad + 2;
|
||||
+ }
|
||||
drawbar(selmon);
|
||||
}
|
||||
|
||||
--
|
||||
2.31.0
|
||||
|
||||
412
suckless/dwm/patches/dwm-swallow-6.3.diff
Normal file
412
suckless/dwm/patches/dwm-swallow-6.3.diff
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
From 0cf9a007511f7dfd7dd94171b172562ebac9b6d5 Mon Sep 17 00:00:00 2001
|
||||
From: Tom Schwindl <schwindl@posteo.de>
|
||||
Date: Sat, 10 Sep 2022 12:51:09 +0200
|
||||
Subject: [PATCH] 6.3 swallow patch
|
||||
|
||||
---
|
||||
config.def.h | 9 +-
|
||||
config.mk | 3 +-
|
||||
dwm.c | 235 +++++++++++++++++++++++++++++++++++++++++++++++++--
|
||||
3 files changed, 237 insertions(+), 10 deletions(-)
|
||||
|
||||
diff --git a/config.def.h b/config.def.h
|
||||
index 061ad662f82a..0b2b8ffd30d5 100644
|
||||
--- a/config.def.h
|
||||
+++ b/config.def.h
|
||||
@@ -3,6 +3,7 @@
|
||||
/* appearance */
|
||||
static const unsigned int borderpx = 1; /* border pixel of windows */
|
||||
static const unsigned int snap = 32; /* snap pixel */
|
||||
+static const int swallowfloating = 0; /* 1 means swallow floating windows by default */
|
||||
static const int showbar = 1; /* 0 means no bar */
|
||||
static const int topbar = 1; /* 0 means bottom bar */
|
||||
static const char *fonts[] = { "monospace:size=10" };
|
||||
@@ -26,9 +27,11 @@ static const Rule rules[] = {
|
||||
* WM_CLASS(STRING) = instance, class
|
||||
* WM_NAME(STRING) = title
|
||||
*/
|
||||
- /* class instance title tags mask isfloating monitor */
|
||||
- { "Gimp", NULL, NULL, 0, 1, -1 },
|
||||
- { "Firefox", NULL, NULL, 1 << 8, 0, -1 },
|
||||
+ /* class instance title tags mask isfloating isterminal noswallow monitor */
|
||||
+ { "Gimp", NULL, NULL, 0, 1, 0, 0, -1 },
|
||||
+ { "Firefox", NULL, NULL, 1 << 8, 0, 0, -1, -1 },
|
||||
+ { "St", NULL, NULL, 0, 0, 1, 0, -1 },
|
||||
+ { NULL, NULL, "Event Tester", 0, 0, 0, 1, -1 }, /* xev */
|
||||
};
|
||||
|
||||
/* layout(s) */
|
||||
diff --git a/config.mk b/config.mk
|
||||
index 81c493ef4aff..52d1ebf30bec 100644
|
||||
--- a/config.mk
|
||||
+++ b/config.mk
|
||||
@@ -20,10 +20,11 @@ FREETYPEINC = /usr/include/freetype2
|
||||
# OpenBSD (uncomment)
|
||||
#FREETYPEINC = ${X11INC}/freetype2
|
||||
#MANPREFIX = ${PREFIX}/man
|
||||
+#KVMLIB = -lkvm
|
||||
|
||||
# includes and libs
|
||||
INCS = -I${X11INC} -I${FREETYPEINC}
|
||||
-LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} ${FREETYPELIBS}
|
||||
+LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} ${FREETYPELIBS} -lX11-xcb -lxcb -lxcb-res ${KVMLIB}
|
||||
|
||||
# flags
|
||||
CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=200809L -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS}
|
||||
diff --git a/dwm.c b/dwm.c
|
||||
index e5efb6a22806..e68294b6b679 100644
|
||||
--- a/dwm.c
|
||||
+++ b/dwm.c
|
||||
@@ -40,6 +40,12 @@
|
||||
#include <X11/extensions/Xinerama.h>
|
||||
#endif /* XINERAMA */
|
||||
#include <X11/Xft/Xft.h>
|
||||
+#include <X11/Xlib-xcb.h>
|
||||
+#include <xcb/res.h>
|
||||
+#ifdef __OpenBSD__
|
||||
+#include <sys/sysctl.h>
|
||||
+#include <kvm.h>
|
||||
+#endif /* __OpenBSD */
|
||||
|
||||
#include "drw.h"
|
||||
#include "util.h"
|
||||
@@ -92,9 +98,11 @@ struct Client {
|
||||
int basew, baseh, incw, inch, maxw, maxh, minw, minh, hintsvalid;
|
||||
int bw, oldbw;
|
||||
unsigned int tags;
|
||||
- int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen;
|
||||
+ int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen, isterminal, noswallow;
|
||||
+ pid_t pid;
|
||||
Client *next;
|
||||
Client *snext;
|
||||
+ Client *swallowing;
|
||||
Monitor *mon;
|
||||
Window win;
|
||||
};
|
||||
@@ -138,6 +146,8 @@ typedef struct {
|
||||
const char *title;
|
||||
unsigned int tags;
|
||||
int isfloating;
|
||||
+ int isterminal;
|
||||
+ int noswallow;
|
||||
int monitor;
|
||||
} Rule;
|
||||
|
||||
@@ -235,6 +245,12 @@ static int xerrordummy(Display *dpy, XErrorEvent *ee);
|
||||
static int xerrorstart(Display *dpy, XErrorEvent *ee);
|
||||
static void zoom(const Arg *arg);
|
||||
|
||||
+static pid_t getparentprocess(pid_t p);
|
||||
+static int isdescprocess(pid_t p, pid_t c);
|
||||
+static Client *swallowingclient(Window w);
|
||||
+static Client *termforwin(const Client *c);
|
||||
+static pid_t winpid(Window w);
|
||||
+
|
||||
/* variables */
|
||||
static const char broken[] = "broken";
|
||||
static char stext[256];
|
||||
@@ -269,6 +285,8 @@ static Drw *drw;
|
||||
static Monitor *mons, *selmon;
|
||||
static Window root, wmcheckwin;
|
||||
|
||||
+static xcb_connection_t *xcon;
|
||||
+
|
||||
/* configuration, allows nested code to access above variables */
|
||||
#include "config.h"
|
||||
|
||||
@@ -298,6 +316,8 @@ applyrules(Client *c)
|
||||
&& (!r->class || strstr(class, r->class))
|
||||
&& (!r->instance || strstr(instance, r->instance)))
|
||||
{
|
||||
+ c->isterminal = r->isterminal;
|
||||
+ c->noswallow = r->noswallow;
|
||||
c->isfloating = r->isfloating;
|
||||
c->tags |= r->tags;
|
||||
for (m = mons; m && m->num != r->monitor; m = m->next);
|
||||
@@ -416,6 +436,53 @@ attachstack(Client *c)
|
||||
c->mon->stack = c;
|
||||
}
|
||||
|
||||
+void
|
||||
+swallow(Client *p, Client *c)
|
||||
+{
|
||||
+
|
||||
+ if (c->noswallow || c->isterminal)
|
||||
+ return;
|
||||
+ if (c->noswallow && !swallowfloating && c->isfloating)
|
||||
+ return;
|
||||
+
|
||||
+ detach(c);
|
||||
+ detachstack(c);
|
||||
+
|
||||
+ setclientstate(c, WithdrawnState);
|
||||
+ XUnmapWindow(dpy, p->win);
|
||||
+
|
||||
+ p->swallowing = c;
|
||||
+ c->mon = p->mon;
|
||||
+
|
||||
+ Window w = p->win;
|
||||
+ p->win = c->win;
|
||||
+ c->win = w;
|
||||
+ updatetitle(p);
|
||||
+ XMoveResizeWindow(dpy, p->win, p->x, p->y, p->w, p->h);
|
||||
+ arrange(p->mon);
|
||||
+ configure(p);
|
||||
+ updateclientlist();
|
||||
+}
|
||||
+
|
||||
+void
|
||||
+unswallow(Client *c)
|
||||
+{
|
||||
+ c->win = c->swallowing->win;
|
||||
+
|
||||
+ free(c->swallowing);
|
||||
+ c->swallowing = NULL;
|
||||
+
|
||||
+ /* unfullscreen the client */
|
||||
+ setfullscreen(c, 0);
|
||||
+ updatetitle(c);
|
||||
+ arrange(c->mon);
|
||||
+ XMapWindow(dpy, c->win);
|
||||
+ XMoveResizeWindow(dpy, c->win, c->x, c->y, c->w, c->h);
|
||||
+ setclientstate(c, NormalState);
|
||||
+ focus(NULL);
|
||||
+ arrange(c->mon);
|
||||
+}
|
||||
+
|
||||
void
|
||||
buttonpress(XEvent *e)
|
||||
{
|
||||
@@ -656,6 +723,9 @@ destroynotify(XEvent *e)
|
||||
|
||||
if ((c = wintoclient(ev->window)))
|
||||
unmanage(c, 1);
|
||||
+
|
||||
+ else if ((c = swallowingclient(ev->window)))
|
||||
+ unmanage(c->swallowing, 1);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1022,12 +1092,13 @@ killclient(const Arg *arg)
|
||||
void
|
||||
manage(Window w, XWindowAttributes *wa)
|
||||
{
|
||||
- Client *c, *t = NULL;
|
||||
+ Client *c, *t = NULL, *term = NULL;
|
||||
Window trans = None;
|
||||
XWindowChanges wc;
|
||||
|
||||
c = ecalloc(1, sizeof(Client));
|
||||
c->win = w;
|
||||
+ c->pid = winpid(w);
|
||||
/* geometry */
|
||||
c->x = c->oldx = wa->x;
|
||||
c->y = c->oldy = wa->y;
|
||||
@@ -1042,6 +1113,7 @@ manage(Window w, XWindowAttributes *wa)
|
||||
} else {
|
||||
c->mon = selmon;
|
||||
applyrules(c);
|
||||
+ term = termforwin(c);
|
||||
}
|
||||
|
||||
if (c->x + WIDTH(c) > c->mon->wx + c->mon->ww)
|
||||
@@ -1076,6 +1148,8 @@ manage(Window w, XWindowAttributes *wa)
|
||||
c->mon->sel = c;
|
||||
arrange(c->mon);
|
||||
XMapWindow(dpy, c->win);
|
||||
+ if (term)
|
||||
+ swallow(term, c);
|
||||
focus(NULL);
|
||||
}
|
||||
|
||||
@@ -1763,6 +1837,20 @@ unmanage(Client *c, int destroyed)
|
||||
Monitor *m = c->mon;
|
||||
XWindowChanges wc;
|
||||
|
||||
+ if (c->swallowing) {
|
||||
+ unswallow(c);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ Client *s = swallowingclient(c->win);
|
||||
+ if (s) {
|
||||
+ free(s->swallowing);
|
||||
+ s->swallowing = NULL;
|
||||
+ arrange(m);
|
||||
+ focus(NULL);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
detach(c);
|
||||
detachstack(c);
|
||||
if (!destroyed) {
|
||||
@@ -1778,9 +1866,12 @@ unmanage(Client *c, int destroyed)
|
||||
XUngrabServer(dpy);
|
||||
}
|
||||
free(c);
|
||||
- focus(NULL);
|
||||
- updateclientlist();
|
||||
- arrange(m);
|
||||
+
|
||||
+ if (!s) {
|
||||
+ arrange(m);
|
||||
+ focus(NULL);
|
||||
+ updateclientlist();
|
||||
+ }
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2044,6 +2135,136 @@ view(const Arg *arg)
|
||||
arrange(selmon);
|
||||
}
|
||||
|
||||
+pid_t
|
||||
+winpid(Window w)
|
||||
+{
|
||||
+
|
||||
+ pid_t result = 0;
|
||||
+
|
||||
+#ifdef __linux__
|
||||
+ xcb_res_client_id_spec_t spec = {0};
|
||||
+ spec.client = w;
|
||||
+ spec.mask = XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID;
|
||||
+
|
||||
+ xcb_generic_error_t *e = NULL;
|
||||
+ xcb_res_query_client_ids_cookie_t c = xcb_res_query_client_ids(xcon, 1, &spec);
|
||||
+ xcb_res_query_client_ids_reply_t *r = xcb_res_query_client_ids_reply(xcon, c, &e);
|
||||
+
|
||||
+ if (!r)
|
||||
+ return (pid_t)0;
|
||||
+
|
||||
+ xcb_res_client_id_value_iterator_t i = xcb_res_query_client_ids_ids_iterator(r);
|
||||
+ for (; i.rem; xcb_res_client_id_value_next(&i)) {
|
||||
+ spec = i.data->spec;
|
||||
+ if (spec.mask & XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID) {
|
||||
+ uint32_t *t = xcb_res_client_id_value_value(i.data);
|
||||
+ result = *t;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ free(r);
|
||||
+
|
||||
+ if (result == (pid_t)-1)
|
||||
+ result = 0;
|
||||
+
|
||||
+#endif /* __linux__ */
|
||||
+
|
||||
+#ifdef __OpenBSD__
|
||||
+ Atom type;
|
||||
+ int format;
|
||||
+ unsigned long len, bytes;
|
||||
+ unsigned char *prop;
|
||||
+ pid_t ret;
|
||||
+
|
||||
+ if (XGetWindowProperty(dpy, w, XInternAtom(dpy, "_NET_WM_PID", 0), 0, 1, False, AnyPropertyType, &type, &format, &len, &bytes, &prop) != Success || !prop)
|
||||
+ return 0;
|
||||
+
|
||||
+ ret = *(pid_t*)prop;
|
||||
+ XFree(prop);
|
||||
+ result = ret;
|
||||
+
|
||||
+#endif /* __OpenBSD__ */
|
||||
+ return result;
|
||||
+}
|
||||
+
|
||||
+pid_t
|
||||
+getparentprocess(pid_t p)
|
||||
+{
|
||||
+ unsigned int v = 0;
|
||||
+
|
||||
+#ifdef __linux__
|
||||
+ FILE *f;
|
||||
+ char buf[256];
|
||||
+ snprintf(buf, sizeof(buf) - 1, "/proc/%u/stat", (unsigned)p);
|
||||
+
|
||||
+ if (!(f = fopen(buf, "r")))
|
||||
+ return 0;
|
||||
+
|
||||
+ fscanf(f, "%*u %*s %*c %u", &v);
|
||||
+ fclose(f);
|
||||
+#endif /* __linux__*/
|
||||
+
|
||||
+#ifdef __OpenBSD__
|
||||
+ int n;
|
||||
+ kvm_t *kd;
|
||||
+ struct kinfo_proc *kp;
|
||||
+
|
||||
+ kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, NULL);
|
||||
+ if (!kd)
|
||||
+ return 0;
|
||||
+
|
||||
+ kp = kvm_getprocs(kd, KERN_PROC_PID, p, sizeof(*kp), &n);
|
||||
+ v = kp->p_ppid;
|
||||
+#endif /* __OpenBSD__ */
|
||||
+
|
||||
+ return (pid_t)v;
|
||||
+}
|
||||
+
|
||||
+int
|
||||
+isdescprocess(pid_t p, pid_t c)
|
||||
+{
|
||||
+ while (p != c && c != 0)
|
||||
+ c = getparentprocess(c);
|
||||
+
|
||||
+ return (int)c;
|
||||
+}
|
||||
+
|
||||
+Client *
|
||||
+termforwin(const Client *w)
|
||||
+{
|
||||
+ Client *c;
|
||||
+ Monitor *m;
|
||||
+
|
||||
+ if (!w->pid || w->isterminal)
|
||||
+ return NULL;
|
||||
+
|
||||
+ for (m = mons; m; m = m->next) {
|
||||
+ for (c = m->clients; c; c = c->next) {
|
||||
+ if (c->isterminal && !c->swallowing && c->pid && isdescprocess(c->pid, w->pid))
|
||||
+ return c;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return NULL;
|
||||
+}
|
||||
+
|
||||
+Client *
|
||||
+swallowingclient(Window w)
|
||||
+{
|
||||
+ Client *c;
|
||||
+ Monitor *m;
|
||||
+
|
||||
+ for (m = mons; m; m = m->next) {
|
||||
+ for (c = m->clients; c; c = c->next) {
|
||||
+ if (c->swallowing && c->swallowing->win == w)
|
||||
+ return c;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return NULL;
|
||||
+}
|
||||
+
|
||||
Client *
|
||||
wintoclient(Window w)
|
||||
{
|
||||
@@ -2133,10 +2354,12 @@ main(int argc, char *argv[])
|
||||
fputs("warning: no locale support\n", stderr);
|
||||
if (!(dpy = XOpenDisplay(NULL)))
|
||||
die("dwm: cannot open display");
|
||||
+ if (!(xcon = XGetXCBConnection(dpy)))
|
||||
+ die("dwm: cannot get xcb connection\n");
|
||||
checkotherwm();
|
||||
setup();
|
||||
#ifdef __OpenBSD__
|
||||
- if (pledge("stdio rpath proc exec", NULL) == -1)
|
||||
+ if (pledge("stdio rpath proc exec ps", NULL) == -1)
|
||||
die("pledge");
|
||||
#endif /* __OpenBSD__ */
|
||||
scan();
|
||||
--
|
||||
2.37.2
|
||||
|
||||
42
suckless/dwm/transient.c
Normal file
42
suckless/dwm/transient.c
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* cc transient.c -o transient -lX11 */
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
|
||||
int main(void) {
|
||||
Display *d;
|
||||
Window r, f, t = None;
|
||||
XSizeHints h;
|
||||
XEvent e;
|
||||
|
||||
d = XOpenDisplay(NULL);
|
||||
if (!d)
|
||||
exit(1);
|
||||
r = DefaultRootWindow(d);
|
||||
|
||||
f = XCreateSimpleWindow(d, r, 100, 100, 400, 400, 0, 0, 0);
|
||||
h.min_width = h.max_width = h.min_height = h.max_height = 400;
|
||||
h.flags = PMinSize | PMaxSize;
|
||||
XSetWMNormalHints(d, f, &h);
|
||||
XStoreName(d, f, "floating");
|
||||
XMapWindow(d, f);
|
||||
|
||||
XSelectInput(d, f, ExposureMask);
|
||||
while (1) {
|
||||
XNextEvent(d, &e);
|
||||
|
||||
if (t == None) {
|
||||
sleep(5);
|
||||
t = XCreateSimpleWindow(d, r, 50, 50, 100, 100, 0, 0, 0);
|
||||
XSetTransientForHint(d, t, f);
|
||||
XStoreName(d, t, "transient");
|
||||
XMapWindow(d, t);
|
||||
XSelectInput(d, t, ExposureMask);
|
||||
}
|
||||
}
|
||||
|
||||
XCloseDisplay(d);
|
||||
exit(0);
|
||||
}
|
||||
37
suckless/dwm/util.c
Normal file
37
suckless/dwm/util.c
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "util.h"
|
||||
|
||||
void
|
||||
die(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
int saved_errno;
|
||||
|
||||
saved_errno = errno;
|
||||
|
||||
va_start(ap, fmt);
|
||||
vfprintf(stderr, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
if (fmt[0] && fmt[strlen(fmt)-1] == ':')
|
||||
fprintf(stderr, " %s", strerror(saved_errno));
|
||||
fputc('\n', stderr);
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
void *
|
||||
ecalloc(size_t nmemb, size_t size)
|
||||
{
|
||||
void *p;
|
||||
|
||||
if (!(p = calloc(nmemb, size)))
|
||||
die("calloc:");
|
||||
return p;
|
||||
}
|
||||
9
suckless/dwm/util.h
Normal file
9
suckless/dwm/util.h
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
|
||||
#define MAX(A, B) ((A) > (B) ? (A) : (B))
|
||||
#define MIN(A, B) ((A) < (B) ? (A) : (B))
|
||||
#define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B))
|
||||
#define LENGTH(X) (sizeof (X) / sizeof (X)[0])
|
||||
|
||||
void die(const char *fmt, ...);
|
||||
void *ecalloc(size_t nmemb, size_t size);
|
||||
5
suckless/scroll/.gitignore
vendored
Normal file
5
suckless/scroll/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
scroll
|
||||
ptty
|
||||
*.swp
|
||||
*.core
|
||||
config.h
|
||||
16
suckless/scroll/LICENSE
Normal file
16
suckless/scroll/LICENSE
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
ISC License (ISC)
|
||||
|
||||
Copyright (c) 2020 Jan Klemkow <j.klemkow@wemelug.de>
|
||||
Copyright (c) 2020 Jochen Sprickerhof <git@jochen.sprickerhof.de>
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
45
suckless/scroll/Makefile
Normal file
45
suckless/scroll/Makefile
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
.POSIX:
|
||||
|
||||
include config.mk
|
||||
|
||||
all: scroll
|
||||
|
||||
config.h:
|
||||
cp config.def.h config.h
|
||||
|
||||
scroll: scroll.c config.h
|
||||
|
||||
install: scroll
|
||||
mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1
|
||||
cp -f scroll $(DESTDIR)$(BINDIR)
|
||||
cp -f scroll.1 $(DESTDIR)$(MANDIR)/man1
|
||||
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)$(BINDIR)/scroll $(DESTDIR)$(MANDIR)/man1/scroll.1
|
||||
|
||||
test: scroll ptty
|
||||
# check usage
|
||||
if ./ptty ./scroll -h; then exit 1; fi
|
||||
# check exit passthrough of child
|
||||
if ! ./ptty ./scroll true; then exit 1; fi
|
||||
if ./ptty ./scroll false; then exit 1; fi
|
||||
./up.sh
|
||||
|
||||
clean:
|
||||
rm -f scroll ptty
|
||||
|
||||
distclean: clean
|
||||
rm -f config.h scroll-$(VERSION).tar.gz
|
||||
|
||||
dist: clean
|
||||
mkdir -p scroll-$(VERSION)
|
||||
cp -R README scroll.1 TODO Makefile config.mk config.def.h \
|
||||
ptty.c scroll.c up.sh up.log \
|
||||
scroll-$(VERSION)
|
||||
tar -cf - scroll-$(VERSION) | gzip > scroll-$(VERSION).tar.gz
|
||||
rm -rf scroll-$(VERSION)
|
||||
|
||||
.c:
|
||||
$(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -o $@ $< -lutil
|
||||
|
||||
.PHONY: all install test clean distclean dist
|
||||
34
suckless/scroll/README
Normal file
34
suckless/scroll/README
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
This program provides a scroll back buffer for a terminal like st(1). It
|
||||
should run on any Unix-like system.
|
||||
|
||||
At the moment it is in an experimental state. Its not recommended for
|
||||
productive use.
|
||||
|
||||
The initial version of this program is from Roberto E. Vargas Caballero:
|
||||
https://lists.suckless.org/dev/1703/31256.html
|
||||
|
||||
What is the state of scroll?
|
||||
|
||||
The project is faced with some hard facts, that our original plan is not doable
|
||||
as we thought in the first place:
|
||||
|
||||
1. [crtl]+[e] is used in emacs mode (default) on the shell to jump to the end
|
||||
of the line. But, its also used so signal a scroll down mouse event from
|
||||
terminal emulators to the shell an other programs.
|
||||
|
||||
- A workaround is to use vi mode in the shell.
|
||||
- Or to give up mouse support (default behavior)
|
||||
|
||||
2. scroll could not handle backward cursor jumps and editing of old lines
|
||||
properly. We just handle current line editing and switching between
|
||||
alternative screens (curses mode). For a proper end user experience we
|
||||
would need to write a completely new terminal emulator like screen or tmux.
|
||||
|
||||
What is the performance impact of scroll?
|
||||
|
||||
indirect OpenBSD
|
||||
-------------------------------
|
||||
0x 7.53 s
|
||||
1x 10.10 s
|
||||
2x 12.00 s
|
||||
3x 13.73 s
|
||||
3
suckless/scroll/TODO
Normal file
3
suckless/scroll/TODO
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
* strlen function which is aware of unicode
|
||||
* handle wrapping lines in scrolling line count correctly
|
||||
* hotkey to dump buffer to file (like screen hardcopy)
|
||||
16
suckless/scroll/config.def.h
Normal file
16
suckless/scroll/config.def.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Define ESC sequences to use for scroll events.
|
||||
* Use "cat -v" to figure out favorite key combination.
|
||||
*
|
||||
* lines is the number of lines scrolled up or down.
|
||||
* If lines is negative, it's the fraction of the terminal size.
|
||||
*/
|
||||
|
||||
struct rule rules[] = {
|
||||
/* sequence event lines */
|
||||
{"\033[5;2~", SCROLL_UP, -1}, /* [Shift] + [PageUP] */
|
||||
{"\033[6;2~", SCROLL_DOWN, -1}, /* [Shift] + [PageDown] */
|
||||
/* mouse binding shadows ^E and ^Y, so it's disabled by default */
|
||||
//{"\031", SCROLL_UP, 1}, /* mouse wheel up */
|
||||
//{"\005", SCROLL_DOWN, 1}, /* mouse wheel Down */
|
||||
};
|
||||
16
suckless/scroll/config.h
Normal file
16
suckless/scroll/config.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Define ESC sequences to use for scroll events.
|
||||
* Use "cat -v" to figure out favorite key combination.
|
||||
*
|
||||
* lines is the number of lines scrolled up or down.
|
||||
* If lines is negative, it's the fraction of the terminal size.
|
||||
*/
|
||||
|
||||
struct rule rules[] = {
|
||||
/* sequence event lines */
|
||||
{"\033[5;2~", SCROLL_UP, -1}, /* [Shift] + [PageUP] */
|
||||
{"\033[6;2~", SCROLL_DOWN, -1}, /* [Shift] + [PageDown] */
|
||||
/* mouse binding shadows ^E and ^Y, so it's disabled by default */
|
||||
{"\031", SCROLL_UP, 1}, /* mouse wheel up */
|
||||
{"\005", SCROLL_DOWN, 1}, /* mouse wheel Down */
|
||||
};
|
||||
12
suckless/scroll/config.mk
Normal file
12
suckless/scroll/config.mk
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# scroll version
|
||||
VERSION = 0.1
|
||||
|
||||
# paths
|
||||
PREFIX = /usr/local
|
||||
BINDIR = $(PREFIX)/bin
|
||||
MANDIR = $(PREFIX)/share/man
|
||||
|
||||
CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_DEFAULT_SOURCE
|
||||
# if your system is not POSIX, add -std=c99 to CFLAGS
|
||||
CFLAGS = -Os
|
||||
LDFLAGS = -s
|
||||
29
suckless/scroll/perf.sh
Executable file
29
suckless/scroll/perf.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
export POSIXLY_CORRECT=1
|
||||
num=1000000
|
||||
seq=seq
|
||||
|
||||
if [ -x /usr/bin/jot ]; then
|
||||
seq=jot
|
||||
fi
|
||||
|
||||
rm -f perf_*.log
|
||||
|
||||
for i in `$seq 10`; do
|
||||
/usr/bin/time st -e $seq $num 2>>perf_0.log
|
||||
done
|
||||
|
||||
for i in `$seq 10`; do
|
||||
/usr/bin/time st -e ./ptty $seq $num 2>>perf_1.log
|
||||
done
|
||||
|
||||
for i in `$seq 10`; do
|
||||
/usr/bin/time st -e ./ptty ./ptty $seq $num 2>>perf_2.log
|
||||
done
|
||||
|
||||
for i in `$seq 10`; do
|
||||
/usr/bin/time st -e ./ptty ./ptty ./ptty $seq $num 2>>perf_3.log
|
||||
done
|
||||
156
suckless/scroll/ptty.c
Normal file
156
suckless/scroll/ptty.c
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#include <sys/wait.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <inttypes.h>
|
||||
#include <limits.h>
|
||||
#include <poll.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#if defined(__linux)
|
||||
#include <pty.h>
|
||||
#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
|
||||
#include <util.h>
|
||||
#elif defined(__FreeBSD__) || defined(__DragonFly__)
|
||||
#include <libutil.h>
|
||||
#endif
|
||||
|
||||
void
|
||||
die(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vfprintf(stderr, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
if (fmt[0] && fmt[strlen(fmt)-1] == ':') {
|
||||
fputc(' ', stderr);
|
||||
perror(NULL);
|
||||
} else {
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
void
|
||||
usage(void)
|
||||
{
|
||||
fputs("ptty [-C] [-c cols] [-r rows] cmd\n", stderr);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
struct winsize ws = {.ws_row = 25, .ws_col = 80, 0, 0};
|
||||
int ch;
|
||||
bool closeflag = false;
|
||||
|
||||
while ((ch = getopt(argc, argv, "c:r:Ch")) != -1) {
|
||||
switch (ch) {
|
||||
case 'c': /* cols */
|
||||
ws.ws_col = strtoimax(optarg, NULL, 10);
|
||||
if (errno != 0)
|
||||
die("strtoimax: %s", optarg);
|
||||
break;
|
||||
case 'r': /* lines */
|
||||
ws.ws_row = strtoimax(optarg, NULL, 10);
|
||||
if (errno != 0)
|
||||
die("strtoimax: %s", optarg);
|
||||
break;
|
||||
case 'C':
|
||||
closeflag = true;
|
||||
break;
|
||||
case 'h':
|
||||
default:
|
||||
usage();
|
||||
}
|
||||
}
|
||||
argc -= optind;
|
||||
argv += optind;
|
||||
|
||||
if (argc < 1)
|
||||
usage();
|
||||
|
||||
int mfd;
|
||||
pid_t child = forkpty(&mfd, NULL, NULL, &ws);
|
||||
switch (child) {
|
||||
case -1:
|
||||
die("forkpty");
|
||||
case 0: /* child */
|
||||
execvp(argv[0], argv);
|
||||
die("exec");
|
||||
}
|
||||
|
||||
/* parent */
|
||||
|
||||
if (closeflag && close(mfd) == -1)
|
||||
die("close:");
|
||||
|
||||
int pfds = 2;
|
||||
struct pollfd pfd[2] = {
|
||||
{ STDIN_FILENO, POLLIN, 0},
|
||||
{ mfd, POLLIN, 0}
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
char buf[BUFSIZ];
|
||||
ssize_t n;
|
||||
int r;
|
||||
|
||||
if ((r = poll(pfd, pfds, -1)) == -1)
|
||||
die("poll:");
|
||||
|
||||
if (pfd[0].revents & POLLIN) {
|
||||
if ((n = read(STDIN_FILENO, buf, sizeof buf)) == -1)
|
||||
die("read:");
|
||||
if (n == 0) {
|
||||
pfd[0].fd = -1;
|
||||
if (close(mfd) == -1)
|
||||
die("close:");
|
||||
break;
|
||||
}
|
||||
if (write(mfd, buf, n) == -1)
|
||||
die("write:");
|
||||
}
|
||||
|
||||
if (pfd[1].revents & POLLIN) {
|
||||
if ((n = read(mfd, buf, sizeof(buf)-1)) == -1)
|
||||
die("read:");
|
||||
|
||||
if (n == 0) break;
|
||||
|
||||
buf[n] = '\0';
|
||||
|
||||
/* handle cursor position request */
|
||||
if (strcmp("\033[6n", buf) == 0) {
|
||||
dprintf(mfd, "\033[25;1R");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (write(STDOUT_FILENO, buf, n) == -1)
|
||||
die("write:");
|
||||
}
|
||||
|
||||
if (pfd[0].revents & POLLHUP) {
|
||||
pfd[0].fd = -1;
|
||||
if (close(mfd) == -1)
|
||||
die("close:");
|
||||
break;
|
||||
}
|
||||
if (pfd[1].revents & POLLHUP)
|
||||
break;
|
||||
}
|
||||
|
||||
int status;
|
||||
if (waitpid(child, &status, 0) != child)
|
||||
die("waitpid:");
|
||||
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
68
suckless/scroll/scroll.1
Normal file
68
suckless/scroll/scroll.1
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
.\"
|
||||
.\" Copyright (c) 2020 Jan Klemkow <j.klemkow@wemelug.de>
|
||||
.\"
|
||||
.\" Permission to use, copy, modify, and distribute this software for any
|
||||
.\" purpose with or without fee is hereby granted, provided that the above
|
||||
.\" copyright notice and this permission notice appear in all copies.
|
||||
.\"
|
||||
.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
.\"
|
||||
.Dd April 9, 2020
|
||||
.Dt SCROLL 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm scroll
|
||||
.Nd scrollback buffer
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op Fl Mh
|
||||
.Op Fl m Ar size
|
||||
.Op program Op arg ...
|
||||
.Sh DESCRIPTION
|
||||
The
|
||||
.Nm
|
||||
utility saves output lines from the child
|
||||
.Ar program
|
||||
to use them for scrollback.
|
||||
If
|
||||
.Ar program
|
||||
is not set,
|
||||
.Nm
|
||||
starts the users default shell.
|
||||
.Pp
|
||||
The options are as follows:
|
||||
.Bl -tag -width Ds
|
||||
.It Fl h
|
||||
Shows usage of
|
||||
.Nm .
|
||||
.It Fl M
|
||||
Set memory limit used for scrollbackbuffer to maximum.
|
||||
.It Fl m Ar size
|
||||
Set memory limit used for scrollbackbuffer to
|
||||
.Ar size .
|
||||
.El
|
||||
.Sh EXIT STATUS
|
||||
.Nm
|
||||
exits with the status code of its the child
|
||||
.Ar program .
|
||||
.Sh EXAMPLES
|
||||
.Nm st
|
||||
.Fl e
|
||||
.Nm scroll
|
||||
.Nm /bin/sh
|
||||
.Sh SEE ALSO
|
||||
.Xr screen 1 ,
|
||||
.Xr st 1 ,
|
||||
.Xr tmux 1
|
||||
.Sh AUTHORS
|
||||
.Nm
|
||||
was written by
|
||||
.An Jan Klemkow Aq Mt j.klemkow@wemelug.de
|
||||
and
|
||||
.An Jochen Sprickerhof Aq Mt git@jochen.sprickerhof.de .
|
||||
594
suckless/scroll/scroll.c
Normal file
594
suckless/scroll/scroll.c
Normal file
|
|
@ -0,0 +1,594 @@
|
|||
/*
|
||||
* Based on an example code from Roberto E. Vargas Caballero.
|
||||
*
|
||||
* See LICENSE file for copyright and license details.
|
||||
*/
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/queue.h>
|
||||
#include <sys/resource.h>
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <pwd.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#if defined(__linux)
|
||||
#include <pty.h>
|
||||
#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
|
||||
#include <util.h>
|
||||
#elif defined(__FreeBSD__) || defined(__DragonFly__)
|
||||
#include <libutil.h>
|
||||
#endif
|
||||
|
||||
#define LENGTH(X) (sizeof (X) / sizeof ((X)[0]))
|
||||
|
||||
const char *argv0;
|
||||
|
||||
TAILQ_HEAD(tailhead, line) head;
|
||||
|
||||
struct line {
|
||||
TAILQ_ENTRY(line) entries;
|
||||
size_t size;
|
||||
size_t len;
|
||||
char *buf;
|
||||
} *bottom;
|
||||
|
||||
pid_t child;
|
||||
int mfd;
|
||||
struct termios dfl;
|
||||
struct winsize ws;
|
||||
static bool altscreen = false; /* is alternative screen active */
|
||||
static bool doredraw = false; /* redraw upon sigwinch */
|
||||
|
||||
struct rule {
|
||||
const char *seq;
|
||||
enum {SCROLL_UP, SCROLL_DOWN} event;
|
||||
short lines;
|
||||
};
|
||||
|
||||
#include "config.h"
|
||||
|
||||
void
|
||||
die(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vfprintf(stderr, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
if (fmt[0] && fmt[strlen(fmt)-1] == ':') {
|
||||
fputc(' ', stderr);
|
||||
perror(NULL);
|
||||
} else {
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
void
|
||||
sigwinch(int sig)
|
||||
{
|
||||
assert(sig == SIGWINCH);
|
||||
|
||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1)
|
||||
die("ioctl:");
|
||||
if (ioctl(mfd, TIOCSWINSZ, &ws) == -1) {
|
||||
if (errno == EBADF) /* child already exited */
|
||||
return;
|
||||
die("ioctl:");
|
||||
}
|
||||
kill(-child, SIGWINCH);
|
||||
doredraw = true;
|
||||
}
|
||||
|
||||
void
|
||||
reset(void)
|
||||
{
|
||||
if (tcsetattr(STDIN_FILENO, TCSANOW, &dfl) == -1)
|
||||
die("tcsetattr:");
|
||||
}
|
||||
|
||||
/* error avoiding remalloc */
|
||||
void *
|
||||
earealloc(void *ptr, size_t size)
|
||||
{
|
||||
void *mem;
|
||||
|
||||
while ((mem = realloc(ptr, size)) == NULL) {
|
||||
struct line *line = TAILQ_LAST(&head, tailhead);
|
||||
|
||||
if (line == NULL)
|
||||
die("realloc:");
|
||||
|
||||
TAILQ_REMOVE(&head, line, entries);
|
||||
free(line->buf);
|
||||
free(line);
|
||||
}
|
||||
|
||||
return mem;
|
||||
}
|
||||
|
||||
/* Count string length w/o ansi esc sequences. */
|
||||
size_t
|
||||
strelen(const char *buf, size_t size)
|
||||
{
|
||||
enum {CHAR, BREK, ESC} state = CHAR;
|
||||
size_t len = 0;
|
||||
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
char c = buf[i];
|
||||
|
||||
switch (state) {
|
||||
case CHAR:
|
||||
if (c == '\033')
|
||||
state = BREK;
|
||||
else
|
||||
len++;
|
||||
break;
|
||||
case BREK:
|
||||
if (c == '[') {
|
||||
state = ESC;
|
||||
} else {
|
||||
state = CHAR;
|
||||
len++;
|
||||
}
|
||||
break;
|
||||
case ESC:
|
||||
if (c >= 64 && c <= 126)
|
||||
state = CHAR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
/* detect alternative screen switching and clear screen */
|
||||
bool
|
||||
skipesc(char c)
|
||||
{
|
||||
static enum {CHAR, BREK, ESC} state = CHAR;
|
||||
static char buf[BUFSIZ];
|
||||
static size_t i = 0;
|
||||
|
||||
switch (state) {
|
||||
case CHAR:
|
||||
if (c == '\033')
|
||||
state = BREK;
|
||||
break;
|
||||
case BREK:
|
||||
if (c == '[')
|
||||
state = ESC;
|
||||
else
|
||||
state = CHAR;
|
||||
break;
|
||||
case ESC:
|
||||
buf[i++] = c;
|
||||
if (i == sizeof buf) {
|
||||
/* TODO: find a better way to handle this situation */
|
||||
state = CHAR;
|
||||
i = 0;
|
||||
} else if (c >= 64 && c <= 126) {
|
||||
state = CHAR;
|
||||
buf[i] = '\0';
|
||||
i = 0;
|
||||
|
||||
/* esc seq. enable alternative screen */
|
||||
if (strcmp(buf, "?1049h") == 0 ||
|
||||
strcmp(buf, "?1047h") == 0 ||
|
||||
strcmp(buf, "?47h" ) == 0)
|
||||
altscreen = true;
|
||||
|
||||
/* esc seq. disable alternative screen */
|
||||
if (strcmp(buf, "?1049l") == 0 ||
|
||||
strcmp(buf, "?1047l") == 0 ||
|
||||
strcmp(buf, "?47l" ) == 0)
|
||||
altscreen = false;
|
||||
|
||||
/* don't save cursor move or clear screen */
|
||||
/* esc sequences to log */
|
||||
switch (c) {
|
||||
case 'A':
|
||||
case 'B':
|
||||
case 'C':
|
||||
case 'D':
|
||||
case 'H':
|
||||
case 'J':
|
||||
case 'K':
|
||||
case 'f':
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return altscreen;
|
||||
}
|
||||
|
||||
void
|
||||
getcursorposition(int *x, int *y)
|
||||
{
|
||||
char input[BUFSIZ];
|
||||
ssize_t n;
|
||||
|
||||
if (write(STDOUT_FILENO, "\033[6n", 4) == -1)
|
||||
die("requesting cursor position");
|
||||
|
||||
do {
|
||||
if ((n = read(STDIN_FILENO, input, sizeof(input)-1)) == -1)
|
||||
die("reading cursor position");
|
||||
input[n] = '\0';
|
||||
} while (sscanf(input, "\033[%d;%dR", y, x) != 2);
|
||||
|
||||
if (*x <= 0 || *y <= 0)
|
||||
die("invalid cursor position: x=%d y=%d", *x, *y);
|
||||
}
|
||||
|
||||
void
|
||||
addline(char *buf, size_t size)
|
||||
{
|
||||
struct line *line = earealloc(NULL, sizeof *line);
|
||||
|
||||
line->size = size;
|
||||
line->len = strelen(buf, size);
|
||||
line->buf = earealloc(NULL, size);
|
||||
memcpy(line->buf, buf, size);
|
||||
|
||||
TAILQ_INSERT_HEAD(&head, line, entries);
|
||||
}
|
||||
|
||||
void
|
||||
redraw()
|
||||
{
|
||||
int rows = 0, x, y;
|
||||
|
||||
if (bottom == NULL)
|
||||
return;
|
||||
|
||||
getcursorposition(&x, &y);
|
||||
|
||||
if (y < ws.ws_row-1)
|
||||
y--;
|
||||
|
||||
/* wind back bottom pointer by shown history */
|
||||
for (; bottom != NULL && TAILQ_NEXT(bottom, entries) != NULL &&
|
||||
rows < y - 1; rows++)
|
||||
bottom = TAILQ_NEXT(bottom, entries);
|
||||
|
||||
/* clear screen */
|
||||
dprintf(STDOUT_FILENO, "\033[2J");
|
||||
/* set cursor position to upper left corner */
|
||||
write(STDOUT_FILENO, "\033[0;0H", 6);
|
||||
|
||||
/* remove newline of first line as we are at 0,0 already */
|
||||
if (bottom->size > 0 && bottom->buf[0] == '\n')
|
||||
write(STDOUT_FILENO, bottom->buf + 1, bottom->size - 1);
|
||||
else
|
||||
write(STDOUT_FILENO, bottom->buf, bottom->size);
|
||||
|
||||
for (rows = ws.ws_row; rows > 0 &&
|
||||
TAILQ_PREV(bottom, tailhead, entries) != NULL; rows--) {
|
||||
bottom = TAILQ_PREV(bottom, tailhead, entries);
|
||||
write(STDOUT_FILENO, bottom->buf, bottom->size);
|
||||
}
|
||||
|
||||
if (bottom == TAILQ_FIRST(&head)) {
|
||||
/* add new line in front of the shell prompt */
|
||||
write(STDOUT_FILENO, "\n", 1);
|
||||
write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */
|
||||
} else
|
||||
bottom = TAILQ_NEXT(bottom, entries);
|
||||
}
|
||||
|
||||
void
|
||||
scrollup(int n)
|
||||
{
|
||||
int rows = 2, x, y, extra = 0;
|
||||
struct line *scrollend = bottom;
|
||||
|
||||
if (bottom == NULL)
|
||||
return;
|
||||
|
||||
getcursorposition(&x, &y);
|
||||
|
||||
if (n < 0) /* scroll by fraction of ws.ws_row, but at least one line */
|
||||
n = ws.ws_row > (-n) ? ws.ws_row / (-n) : 1;
|
||||
|
||||
/* wind back scrollend pointer by the current screen */
|
||||
while (rows < y && TAILQ_NEXT(scrollend, entries) != NULL) {
|
||||
scrollend = TAILQ_NEXT(scrollend, entries);
|
||||
rows += (scrollend->len - 1) / ws.ws_col + 1;
|
||||
}
|
||||
|
||||
if (rows <= 0)
|
||||
return;
|
||||
|
||||
/* wind back scrollend pointer n lines */
|
||||
for (rows = 0; rows + extra < n &&
|
||||
TAILQ_NEXT(scrollend, entries) != NULL; rows++) {
|
||||
scrollend = TAILQ_NEXT(scrollend, entries);
|
||||
extra += (scrollend->len - 1) / ws.ws_col;
|
||||
}
|
||||
|
||||
/* move the text in terminal rows lines down */
|
||||
dprintf(STDOUT_FILENO, "\033[%dT", n);
|
||||
/* set cursor position to upper left corner */
|
||||
write(STDOUT_FILENO, "\033[0;0H", 6);
|
||||
/* hide cursor */
|
||||
write(STDOUT_FILENO, "\033[?25l", 6);
|
||||
|
||||
/* remove newline of first line as we are at 0,0 already */
|
||||
if (scrollend->size > 0 && scrollend->buf[0] == '\n')
|
||||
write(STDOUT_FILENO, scrollend->buf + 1, scrollend->size - 1);
|
||||
else
|
||||
write(STDOUT_FILENO, scrollend->buf, scrollend->size);
|
||||
if (y + n >= ws.ws_row)
|
||||
bottom = TAILQ_NEXT(bottom, entries);
|
||||
|
||||
/* print rows lines and move bottom forward to the new screen bottom */
|
||||
for (; rows > 1; rows--) {
|
||||
scrollend = TAILQ_PREV(scrollend, tailhead, entries);
|
||||
if (y + n >= ws.ws_row)
|
||||
bottom = TAILQ_NEXT(bottom, entries);
|
||||
write(STDOUT_FILENO, scrollend->buf, scrollend->size);
|
||||
}
|
||||
/* move cursor from line n to the old bottom position */
|
||||
if (y + n < ws.ws_row) {
|
||||
dprintf(STDOUT_FILENO, "\033[%d;%dH", y + n, x);
|
||||
write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */
|
||||
} else
|
||||
dprintf(STDOUT_FILENO, "\033[%d;0H", ws.ws_row);
|
||||
}
|
||||
|
||||
void
|
||||
scrolldown(char *buf, size_t size, int n)
|
||||
{
|
||||
if (bottom == NULL || bottom == TAILQ_FIRST(&head))
|
||||
return;
|
||||
|
||||
if (n < 0) /* scroll by fraction of ws.ws_row, but at least one line */
|
||||
n = ws.ws_row > (-n) ? ws.ws_row / (-n) : 1;
|
||||
|
||||
bottom = TAILQ_PREV(bottom, tailhead, entries);
|
||||
/* print n lines */
|
||||
while (n > 0 && bottom != NULL && bottom != TAILQ_FIRST(&head)) {
|
||||
bottom = TAILQ_PREV(bottom, tailhead, entries);
|
||||
write(STDOUT_FILENO, bottom->buf, bottom->size);
|
||||
n -= (bottom->len - 1) / ws.ws_col + 1;
|
||||
}
|
||||
if (n > 0 && bottom == TAILQ_FIRST(&head)) {
|
||||
write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */
|
||||
write(STDOUT_FILENO, buf, size);
|
||||
} else if (bottom != NULL)
|
||||
bottom = TAILQ_NEXT(bottom, entries);
|
||||
}
|
||||
|
||||
void
|
||||
jumpdown(char *buf, size_t size)
|
||||
{
|
||||
int rows = ws.ws_row;
|
||||
|
||||
/* wind back by one page starting from the latest line */
|
||||
bottom = TAILQ_FIRST(&head);
|
||||
for (; TAILQ_NEXT(bottom, entries) != NULL && rows > 0; rows--)
|
||||
bottom = TAILQ_NEXT(bottom, entries);
|
||||
|
||||
scrolldown(buf, size, ws.ws_row);
|
||||
}
|
||||
|
||||
void
|
||||
usage(void) {
|
||||
die("usage: %s [-Mvh] [-m mem] [program]", argv0);
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
int ch;
|
||||
struct rlimit rlimit;
|
||||
|
||||
argv0 = argv[0];
|
||||
|
||||
if (getrlimit(RLIMIT_DATA, &rlimit) == -1)
|
||||
die("getrlimit");
|
||||
|
||||
const char *optstring = "Mm:vh";
|
||||
while ((ch = getopt(argc, argv, optstring)) != -1) {
|
||||
switch (ch) {
|
||||
case 'M':
|
||||
rlimit.rlim_cur = rlimit.rlim_max;
|
||||
break;
|
||||
case 'm':
|
||||
rlimit.rlim_cur = strtoull(optarg, NULL, 0);
|
||||
if (errno != 0)
|
||||
die("strtoull: %s", optarg);
|
||||
break;
|
||||
case 'v':
|
||||
die("%s " VERSION, argv0);
|
||||
break;
|
||||
case 'h':
|
||||
default:
|
||||
usage();
|
||||
}
|
||||
}
|
||||
argc -= optind;
|
||||
argv += optind;
|
||||
|
||||
TAILQ_INIT(&head);
|
||||
|
||||
if (isatty(STDIN_FILENO) == 0 || isatty(STDOUT_FILENO) == 0)
|
||||
die("parent it not a tty");
|
||||
|
||||
/* save terminal settings for resetting after exit */
|
||||
if (tcgetattr(STDIN_FILENO, &dfl) == -1)
|
||||
die("tcgetattr:");
|
||||
if (atexit(reset))
|
||||
die("atexit:");
|
||||
|
||||
/* get window size of the terminal */
|
||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1)
|
||||
die("ioctl:");
|
||||
|
||||
child = forkpty(&mfd, NULL, &dfl, &ws);
|
||||
if (child == -1)
|
||||
die("forkpty:");
|
||||
if (child == 0) { /* child */
|
||||
if (argc >= 1) {
|
||||
execvp(argv[0], argv);
|
||||
} else {
|
||||
struct passwd *passwd = getpwuid(getuid());
|
||||
if (passwd == NULL)
|
||||
die("getpwid:");
|
||||
execlp(passwd->pw_shell, passwd->pw_shell, NULL);
|
||||
}
|
||||
|
||||
perror("execvp");
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
/* set maximum memory size for scrollback buffer */
|
||||
if (setrlimit(RLIMIT_DATA, &rlimit) == -1)
|
||||
die("setrlimit:");
|
||||
|
||||
#ifdef __OpenBSD__
|
||||
if (pledge("stdio tty proc", NULL) == -1)
|
||||
die("pledge:");
|
||||
#endif
|
||||
|
||||
if (signal(SIGWINCH, sigwinch) == SIG_ERR)
|
||||
die("signal:");
|
||||
|
||||
struct termios new = dfl;
|
||||
cfmakeraw(&new);
|
||||
new.c_cc[VMIN ] = 1; /* return read if at least one byte in buffer */
|
||||
new.c_cc[VTIME] = 0; /* no polling time for read from terminal */
|
||||
if (tcsetattr(STDIN_FILENO, TCSANOW, &new) == -1)
|
||||
die("tcsetattr:");
|
||||
|
||||
size_t size = BUFSIZ, len = 0, pos = 0;
|
||||
char *buf = calloc(size, sizeof *buf);
|
||||
if (buf == NULL)
|
||||
die("calloc:");
|
||||
|
||||
struct pollfd pfd[2] = {
|
||||
{STDIN_FILENO, POLLIN, 0},
|
||||
{mfd, POLLIN, 0}
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
char input[BUFSIZ];
|
||||
|
||||
if (poll(pfd, LENGTH(pfd), -1) == -1 && errno != EINTR)
|
||||
die("poll:");
|
||||
|
||||
if (doredraw) {
|
||||
redraw();
|
||||
doredraw = false;
|
||||
}
|
||||
|
||||
if (pfd[0].revents & POLLHUP || pfd[1].revents & POLLHUP)
|
||||
break;
|
||||
|
||||
if (pfd[0].revents & POLLIN) {
|
||||
ssize_t n = read(STDIN_FILENO, input, sizeof(input)-1);
|
||||
|
||||
if (n == -1 && errno != EINTR)
|
||||
die("read:");
|
||||
if (n == 0)
|
||||
break;
|
||||
|
||||
input[n] = '\0';
|
||||
|
||||
if (altscreen)
|
||||
goto noevent;
|
||||
|
||||
for (size_t i = 0; i < LENGTH(rules); i++) {
|
||||
if (strncmp(rules[i].seq, input,
|
||||
strlen(rules[i].seq)) == 0) {
|
||||
if (rules[i].event == SCROLL_UP)
|
||||
scrollup(rules[i].lines);
|
||||
if (rules[i].event == SCROLL_DOWN)
|
||||
scrolldown(buf, len,
|
||||
rules[i].lines);
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
noevent:
|
||||
if (write(mfd, input, n) == -1)
|
||||
die("write:");
|
||||
|
||||
if (bottom != TAILQ_FIRST(&head))
|
||||
jumpdown(buf, len);
|
||||
}
|
||||
out:
|
||||
if (pfd[1].revents & POLLIN) {
|
||||
ssize_t n = read(mfd, input, sizeof(input)-1);
|
||||
|
||||
if (n == -1 && errno != EINTR)
|
||||
die("read:");
|
||||
if (n == 0) /* on exit of child we continue here */
|
||||
continue; /* let signal handler catch SIGCHLD */
|
||||
|
||||
input[n] = '\0';
|
||||
|
||||
/* don't print child output while scrolling */
|
||||
if (bottom == TAILQ_FIRST(&head))
|
||||
if (write(STDOUT_FILENO, input, n) == -1)
|
||||
die("write:");
|
||||
|
||||
/* iterate over the input buffer */
|
||||
for (char *c = input; n-- > 0; c++) {
|
||||
/* don't save alternative screen and */
|
||||
/* clear screen esc sequences to scrollback */
|
||||
if (skipesc(*c))
|
||||
continue;
|
||||
|
||||
if (*c == '\n') {
|
||||
addline(buf, len);
|
||||
/* only advance bottom if scroll is */
|
||||
/* at the end of the scroll back */
|
||||
if (bottom == NULL ||
|
||||
TAILQ_PREV(bottom, tailhead,
|
||||
entries) == TAILQ_FIRST(&head))
|
||||
bottom = TAILQ_FIRST(&head);
|
||||
|
||||
memset(buf, 0, size);
|
||||
len = pos = 0;
|
||||
buf[pos++] = '\r';
|
||||
} else if (*c == '\r') {
|
||||
pos = 0;
|
||||
continue;
|
||||
}
|
||||
buf[pos++] = *c;
|
||||
if (pos > len)
|
||||
len = pos;
|
||||
if (len == size) {
|
||||
size *= 2;
|
||||
buf = earealloc(buf, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (close(mfd) == -1)
|
||||
die("close:");
|
||||
|
||||
int status;
|
||||
if (waitpid(child, &status, 0) == -1)
|
||||
die("waitpid:");
|
||||
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
74
suckless/scroll/up.log
Normal file
74
suckless/scroll/up.log
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
16
|
||||
17
|
||||
18
|
||||
19
|
||||
20
|
||||
21
|
||||
22
|
||||
23
|
||||
24
|
||||
25
|
||||
26
|
||||
27
|
||||
28
|
||||
29
|
||||
30
|
||||
31
|
||||
32
|
||||
33
|
||||
34
|
||||
35
|
||||
36
|
||||
37
|
||||
38
|
||||
39
|
||||
40
|
||||
41
|
||||
42
|
||||
43
|
||||
44
|
||||
45
|
||||
46
|
||||
47
|
||||
48
|
||||
49
|
||||
[25T[0;0H[?25l1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
16
|
||||
17
|
||||
18
|
||||
19
|
||||
20
|
||||
21
|
||||
22
|
||||
23
|
||||
24
|
||||
25[25;0H
|
||||
15
suckless/scroll/up.sh
Executable file
15
suckless/scroll/up.sh
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
export POSIXLY_CORRECT=1
|
||||
|
||||
i=1
|
||||
while test "$i" -lt 50; do
|
||||
echo "$i"
|
||||
i=$((i + 1))
|
||||
done > tmp.log
|
||||
|
||||
(sleep 1; printf '\033[5;2~'; sleep 1; ) \
|
||||
| ./ptty ./scroll tail -fn 50 tmp.log > out.log
|
||||
|
||||
cmp out.log up.log
|
||||
25
suckless/slock/LICENSE
Normal file
25
suckless/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
suckless/slock/Makefile
Normal file
46
suckless/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
suckless/slock/README
Normal file
24
suckless/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.
|
||||
28
suckless/slock/config.def.h
Normal file
28
suckless/slock/config.def.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* 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 */
|
||||
};
|
||||
|
||||
/* lock screen */
|
||||
static const char *bgcolor = "black";
|
||||
static const char *fgcolor = "#eeeeee";
|
||||
static const char *dimcolor = "#888888";
|
||||
|
||||
static const char *clockfont = "monospace:bold:pixelsize=160";
|
||||
static const char *datefont = "monospace:pixelsize=34";
|
||||
static const char *textfont = "monospace:pixelsize=22";
|
||||
static const char *iconfont = "Symbols Nerd Font Mono:pixelsize=24";
|
||||
|
||||
static const int barheight = 10; /* height of the bottom bar */
|
||||
static const unsigned int maxdots = 24; /* max dots shown while typing */
|
||||
|
||||
static const char *battery = "BAT0"; /* /sys/class/power_supply/... */
|
||||
static const int batterylow = 20; /* percent shown as low */
|
||||
|
||||
/* treat a cleared input like a wrong password (color) */
|
||||
static const int failonclear = 1;
|
||||
29
suckless/slock/config.h
Normal file
29
suckless/slock/config.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* user and group to drop privileges to */
|
||||
static const char *user = "nobody";
|
||||
static const char *group = "nogroup";
|
||||
|
||||
/* colores de la barra inferior según el estado */
|
||||
static const char *colorname[NUMCOLS] = {
|
||||
[INIT] = "#475258", /* after initialization (gris Everforest) */
|
||||
[INPUT] = "#a7c080", /* during input (verde acento) */
|
||||
[FAILED] = "#e67e80", /* wrong password (rojo Everforest) */
|
||||
};
|
||||
|
||||
/* pantalla de bloqueo (paleta Everforest) */
|
||||
static const char *bgcolor = "#2d353b"; /* fondo */
|
||||
static const char *fgcolor = "#d3c6aa"; /* reloj y texto */
|
||||
static const char *dimcolor = "#859289"; /* fecha y avisos */
|
||||
|
||||
static const char *clockfont = "Fira Code:style=Bold:pixelsize=160:antialias=true";
|
||||
static const char *datefont = "Fira Code:pixelsize=34:antialias=true";
|
||||
static const char *textfont = "Fira Code:pixelsize=22:antialias=true";
|
||||
static const char *iconfont = "Symbols Nerd Font Mono:pixelsize=24:antialias=true";
|
||||
|
||||
static const int barheight = 10; /* alto de la barra inferior */
|
||||
static const unsigned int maxdots = 24; /* puntos máximos al teclear */
|
||||
|
||||
static const char *battery = "BAT1"; /* /sys/class/power_supply/... */
|
||||
static const int batterylow = 20; /* % para mostrarla en rojo */
|
||||
|
||||
/* treat a cleared input like a wrong password (color) */
|
||||
static const int failonclear = 1;
|
||||
29
suckless/slock/config.mk
Normal file
29
suckless/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} -I/usr/include/freetype2
|
||||
LIBS = -L/usr/lib -lc -lcrypt -L${X11LIB} -lX11 -lXext -lXrandr -lXft -lfontconfig
|
||||
|
||||
# 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 -lXft -lfontconfig
|
||||
# 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
suckless/slock/explicit_bzero.c
Normal file
19
suckless/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
suckless/slock/slock.1
Normal file
36
suckless/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.
|
||||
697
suckless/slock/slock.c
Normal file
697
suckless/slock/slock.c
Normal file
|
|
@ -0,0 +1,697 @@
|
|||
/* 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 <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/types.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/Xft/Xft.h>
|
||||
|
||||
#include "util.h"
|
||||
|
||||
char *argv0;
|
||||
|
||||
enum {
|
||||
INIT,
|
||||
INPUT,
|
||||
FAILED,
|
||||
NUMCOLS
|
||||
};
|
||||
|
||||
enum {
|
||||
COLBG,
|
||||
COLFG,
|
||||
COLDIM,
|
||||
COLCHARGE,
|
||||
COLLOW,
|
||||
NUMUICOLS
|
||||
};
|
||||
|
||||
struct lock {
|
||||
int screen;
|
||||
Window root, win;
|
||||
Pixmap pmap;
|
||||
unsigned long colors[NUMCOLS];
|
||||
/* dibujado: doble búfer del tamaño de la pantalla */
|
||||
int w, h;
|
||||
Pixmap buf;
|
||||
XftDraw *draw;
|
||||
XftColor bar[NUMCOLS];
|
||||
XftColor ui[NUMUICOLS];
|
||||
XftFont *fclock, *fdate, *ftext, *ficon;
|
||||
};
|
||||
|
||||
/* estado que se muestra en la pantalla de bloqueo */
|
||||
struct state {
|
||||
unsigned int len; /* caracteres tecleados */
|
||||
int color; /* INIT, INPUT o FAILED */
|
||||
int failure; /* ha habido una contraseña incorrecta */
|
||||
char clock[16], date[64], bat[16];
|
||||
int batlevel, charging;
|
||||
};
|
||||
|
||||
static char username[64];
|
||||
|
||||
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 XftFont *
|
||||
loadfont(Display *dpy, int screen, const char *name)
|
||||
{
|
||||
XftFont *f;
|
||||
|
||||
if (!(f = XftFontOpenName(dpy, screen, name)) &&
|
||||
!(f = XftFontOpenName(dpy, screen, "monospace")))
|
||||
die("slock: cannot load font %s\n", name);
|
||||
return f;
|
||||
}
|
||||
|
||||
static void
|
||||
setupdraw(Display *dpy, struct lock *lock)
|
||||
{
|
||||
Visual *vis = DefaultVisual(dpy, lock->screen);
|
||||
Colormap cmap = DefaultColormap(dpy, lock->screen);
|
||||
const char *uinames[NUMUICOLS] = {
|
||||
[COLBG] = bgcolor, [COLFG] = fgcolor, [COLDIM] = dimcolor,
|
||||
[COLCHARGE] = colorname[INPUT], [COLLOW] = colorname[FAILED],
|
||||
};
|
||||
int i;
|
||||
|
||||
for (i = 0; i < NUMCOLS; i++)
|
||||
if (!XftColorAllocName(dpy, vis, cmap, colorname[i], &lock->bar[i]))
|
||||
die("slock: cannot allocate color %s\n", colorname[i]);
|
||||
for (i = 0; i < NUMUICOLS; i++)
|
||||
if (!XftColorAllocName(dpy, vis, cmap, uinames[i], &lock->ui[i]))
|
||||
die("slock: cannot allocate color %s\n", uinames[i]);
|
||||
|
||||
lock->fclock = loadfont(dpy, lock->screen, clockfont);
|
||||
lock->fdate = loadfont(dpy, lock->screen, datefont);
|
||||
lock->ftext = loadfont(dpy, lock->screen, textfont);
|
||||
lock->ficon = loadfont(dpy, lock->screen, iconfont);
|
||||
|
||||
lock->w = DisplayWidth(dpy, lock->screen);
|
||||
lock->h = DisplayHeight(dpy, lock->screen);
|
||||
lock->buf = XCreatePixmap(dpy, lock->root, lock->w, lock->h,
|
||||
DefaultDepth(dpy, lock->screen));
|
||||
lock->draw = XftDrawCreate(dpy, lock->buf, vis, cmap);
|
||||
}
|
||||
|
||||
static void
|
||||
resizedraw(Display *dpy, struct lock *lock, int w, int h)
|
||||
{
|
||||
if (w == lock->w && h == lock->h)
|
||||
return;
|
||||
lock->w = w;
|
||||
lock->h = h;
|
||||
XFreePixmap(dpy, lock->buf);
|
||||
lock->buf = XCreatePixmap(dpy, lock->root, w, h,
|
||||
DefaultDepth(dpy, lock->screen));
|
||||
XftDrawChange(lock->draw, lock->buf);
|
||||
}
|
||||
|
||||
static int
|
||||
textw(Display *dpy, XftFont *f, const char *s)
|
||||
{
|
||||
XGlyphInfo ext;
|
||||
|
||||
XftTextExtentsUtf8(dpy, f, (const FcChar8 *)s, strlen(s), &ext);
|
||||
return ext.xOff;
|
||||
}
|
||||
|
||||
static int
|
||||
iconw(Display *dpy, XftFont *f, FcChar32 icon)
|
||||
{
|
||||
XGlyphInfo ext;
|
||||
|
||||
if (!icon || !XftCharExists(dpy, f, icon))
|
||||
return 0;
|
||||
XftTextExtents32(dpy, f, &icon, 1, &ext);
|
||||
return ext.xOff;
|
||||
}
|
||||
|
||||
/* icono (opcional) + separación + texto, con la línea base en y */
|
||||
static int
|
||||
drawlabel(Display *dpy, struct lock *lock, XftFont *f, XftColor *c,
|
||||
int x, int y, FcChar32 icon, const char *s)
|
||||
{
|
||||
int iw = iconw(dpy, lock->ficon, icon), gap = iw ? f->height / 2 : 0;
|
||||
|
||||
if (iw)
|
||||
XftDrawString32(lock->draw, c, lock->ficon, x, y, &icon, 1);
|
||||
XftDrawStringUtf8(lock->draw, c, f, x + iw + gap, y,
|
||||
(const FcChar8 *)s, strlen(s));
|
||||
return iw + gap + textw(dpy, f, s);
|
||||
}
|
||||
|
||||
static int
|
||||
labelw(Display *dpy, struct lock *lock, XftFont *f, FcChar32 icon,
|
||||
const char *s)
|
||||
{
|
||||
int iw = iconw(dpy, lock->ficon, icon);
|
||||
|
||||
return iw + (iw ? f->height / 2 : 0) + textw(dpy, f, s);
|
||||
}
|
||||
|
||||
/* Actualiza hora, fecha y batería. Devuelve 1 si algo ha cambiado. */
|
||||
static int
|
||||
updatestate(struct state *st)
|
||||
{
|
||||
static const char *wdays[] = { "domingo", "lunes", "martes",
|
||||
"miércoles", "jueves", "viernes", "sábado" };
|
||||
static const char *months[] = { "enero", "febrero", "marzo", "abril",
|
||||
"mayo", "junio", "julio", "agosto", "septiembre", "octubre",
|
||||
"noviembre", "diciembre" };
|
||||
char clk[sizeof(st->clock)], date[sizeof(st->date)], bat[sizeof(st->bat)];
|
||||
char path[128], status[32] = "";
|
||||
int level = -1, charging = 0, changed;
|
||||
time_t t = time(NULL);
|
||||
struct tm *tm = localtime(&t);
|
||||
FILE *f;
|
||||
|
||||
strftime(clk, sizeof(clk), "%H:%M", tm);
|
||||
snprintf(date, sizeof(date), "%s, %d de %s", wdays[tm->tm_wday],
|
||||
tm->tm_mday, months[tm->tm_mon]);
|
||||
date[0] = toupper((unsigned char)date[0]);
|
||||
|
||||
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/capacity",
|
||||
battery);
|
||||
if ((f = fopen(path, "r"))) {
|
||||
if (fscanf(f, "%d", &level) != 1)
|
||||
level = -1;
|
||||
fclose(f);
|
||||
}
|
||||
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/status",
|
||||
battery);
|
||||
if ((f = fopen(path, "r"))) {
|
||||
if (fscanf(f, "%31s", status) != 1)
|
||||
status[0] = '\0';
|
||||
fclose(f);
|
||||
}
|
||||
charging = !strcmp(status, "Charging") || !strcmp(status, "Full");
|
||||
if (level >= 0)
|
||||
snprintf(bat, sizeof(bat), "%d%%", level);
|
||||
else
|
||||
bat[0] = '\0';
|
||||
|
||||
changed = strcmp(clk, st->clock) || strcmp(date, st->date) ||
|
||||
strcmp(bat, st->bat) || charging != st->charging;
|
||||
memcpy(st->clock, clk, sizeof(clk));
|
||||
memcpy(st->date, date, sizeof(date));
|
||||
memcpy(st->bat, bat, sizeof(bat));
|
||||
st->batlevel = level;
|
||||
st->charging = charging;
|
||||
return changed;
|
||||
}
|
||||
|
||||
/* Símbolos de Nerd Font (Material Design) */
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
|
||||
#define ICON_LOCK 0xF033E
|
||||
#define ICON_ALERT 0xF0026
|
||||
#define ICON_USER 0xF0004
|
||||
#define ICON_CHARGING 0xF0084
|
||||
#define ICON_BAT10 0xF007A /* 10 % .. 90 % seguidos; 100 % es 0xF0079 */
|
||||
#define ICON_BAT100 0xF0079
|
||||
|
||||
static FcChar32
|
||||
baticon(const struct state *st)
|
||||
{
|
||||
if (st->charging)
|
||||
return ICON_CHARGING;
|
||||
if (st->batlevel >= 95)
|
||||
return ICON_BAT100;
|
||||
if (st->batlevel < 10)
|
||||
return ICON_ALERT;
|
||||
return ICON_BAT10 + st->batlevel / 10 - 1;
|
||||
}
|
||||
|
||||
/* Dibuja la pantalla de bloqueo en el rectángulo de un monitor */
|
||||
static void
|
||||
drawmon(Display *dpy, struct lock *lock, const struct state *st,
|
||||
int mx, int my, int mw, int mh)
|
||||
{
|
||||
XftColor *fg = &lock->ui[COLFG], *dim = &lock->ui[COLDIM], *msgc;
|
||||
char msg[128];
|
||||
FcChar32 icon = 0;
|
||||
int x, y, w, pad = mh / 24, low;
|
||||
unsigned int i, n;
|
||||
|
||||
/* reloj y fecha, algo por encima del centro */
|
||||
y = my + mh * 2 / 5;
|
||||
x = mx + (mw - textw(dpy, lock->fclock, st->clock)) / 2;
|
||||
XftDrawStringUtf8(lock->draw, fg, lock->fclock, x, y,
|
||||
(const FcChar8 *)st->clock, strlen(st->clock));
|
||||
y += lock->fdate->height * 3 / 2;
|
||||
x = mx + (mw - textw(dpy, lock->fdate, st->date)) / 2;
|
||||
XftDrawStringUtf8(lock->draw, dim, lock->fdate, x, y,
|
||||
(const FcChar8 *)st->date, strlen(st->date));
|
||||
|
||||
/* mensaje de estado: indicación, puntos o error */
|
||||
msgc = dim;
|
||||
if (st->color == INPUT) {
|
||||
n = MIN(st->len, maxdots);
|
||||
for (i = 0, msg[0] = '\0'; i < n; i++)
|
||||
strcat(msg, i ? " ●" : "●");
|
||||
msgc = &lock->bar[INPUT];
|
||||
} else if (st->color == FAILED && st->failure) {
|
||||
icon = ICON_ALERT;
|
||||
snprintf(msg, sizeof(msg), "Contraseña incorrecta");
|
||||
msgc = &lock->bar[FAILED];
|
||||
} else {
|
||||
icon = ICON_LOCK;
|
||||
snprintf(msg, sizeof(msg), "Escribe la contraseña");
|
||||
}
|
||||
y = my + mh * 3 / 5 + lock->ftext->ascent;
|
||||
x = mx + (mw - labelw(dpy, lock, lock->ftext, icon, msg)) / 2;
|
||||
drawlabel(dpy, lock, lock->ftext, msgc, x, y, icon, msg);
|
||||
|
||||
/* abajo: usuario a la izquierda, batería a la derecha */
|
||||
y = my + mh - barheight - pad;
|
||||
drawlabel(dpy, lock, lock->ftext, dim, mx + pad, y, ICON_USER, username);
|
||||
if (st->bat[0]) {
|
||||
low = !st->charging && st->batlevel <= batterylow;
|
||||
w = labelw(dpy, lock, lock->ftext, baticon(st), st->bat);
|
||||
drawlabel(dpy, lock, lock->ftext,
|
||||
low ? &lock->ui[COLLOW] :
|
||||
st->charging ? &lock->ui[COLCHARGE] : fg,
|
||||
mx + mw - pad - w, y, baticon(st), st->bat);
|
||||
}
|
||||
|
||||
/* barra con el color del estado */
|
||||
XftDrawRect(lock->draw, &lock->bar[st->color], mx, my + mh - barheight,
|
||||
mw, barheight);
|
||||
}
|
||||
|
||||
static void
|
||||
drawlock(Display *dpy, struct lock *lock, const struct state *st)
|
||||
{
|
||||
XRRMonitorInfo *mons;
|
||||
int i, n = 0;
|
||||
|
||||
XftDrawRect(lock->draw, &lock->ui[COLBG], 0, 0, lock->w, lock->h);
|
||||
mons = XRRGetMonitors(dpy, lock->root, True, &n);
|
||||
for (i = 0; i < n; i++)
|
||||
drawmon(dpy, lock, st, mons[i].x, mons[i].y,
|
||||
mons[i].width, mons[i].height);
|
||||
if (mons)
|
||||
XRRFreeMonitors(mons);
|
||||
if (n <= 0)
|
||||
drawmon(dpy, lock, st, 0, 0, lock->w, lock->h);
|
||||
XCopyArea(dpy, lock->buf, lock->win, DefaultGC(dpy, lock->screen),
|
||||
0, 0, lock->w, lock->h, 0, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
drawall(Display *dpy, struct lock **locks, int nscreens,
|
||||
const struct state *st)
|
||||
{
|
||||
int screen;
|
||||
|
||||
for (screen = 0; screen < nscreens; screen++)
|
||||
drawlock(dpy, locks[screen], st);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
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, xfd;
|
||||
struct state st = { 0 };
|
||||
struct timeval tv;
|
||||
KeySym ksym;
|
||||
XEvent ev;
|
||||
fd_set fds;
|
||||
|
||||
running = 1;
|
||||
st.color = INIT;
|
||||
xfd = ConnectionNumber(dpy);
|
||||
updatestate(&st);
|
||||
drawall(dpy, locks, nscreens, &st);
|
||||
|
||||
while (running) {
|
||||
/* sin eventos pendientes: esperar como mucho 1 s y refrescar
|
||||
* reloj y batería si han cambiado (también tras suspender) */
|
||||
if (!XPending(dpy)) {
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(xfd, &fds);
|
||||
tv.tv_sec = 1;
|
||||
tv.tv_usec = 0;
|
||||
if (select(xfd + 1, &fds, NULL, NULL, &tv) <= 0 &&
|
||||
updatestate(&st))
|
||||
drawall(dpy, locks, nscreens, &st);
|
||||
continue;
|
||||
}
|
||||
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[st.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);
|
||||
st.failure = 1;
|
||||
}
|
||||
explicit_bzero(&passwd, sizeof(passwd));
|
||||
st.len = 0;
|
||||
break;
|
||||
case XK_Escape:
|
||||
explicit_bzero(&passwd, sizeof(passwd));
|
||||
st.len = 0;
|
||||
break;
|
||||
case XK_BackSpace:
|
||||
if (st.len)
|
||||
passwd[--st.len] = '\0';
|
||||
break;
|
||||
default:
|
||||
if (num && !iscntrl((unsigned char)buf[0]) &&
|
||||
(st.len + num < sizeof(passwd))) {
|
||||
memcpy(passwd + st.len, buf, num);
|
||||
st.len += num;
|
||||
} else if (buf[0] == '\025') { /* ctrl-u clears input */
|
||||
explicit_bzero(&passwd, sizeof(passwd));
|
||||
st.len = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
st.color = st.len ? INPUT :
|
||||
((st.failure || failonclear) ? FAILED : INIT);
|
||||
if (running) {
|
||||
updatestate(&st);
|
||||
drawall(dpy, locks, nscreens, &st);
|
||||
}
|
||||
} else if (ev.type == Expose) {
|
||||
if (ev.xexpose.count == 0)
|
||||
drawall(dpy, locks, nscreens, &st);
|
||||
} 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);
|
||||
resizedraw(dpy, locks[screen],
|
||||
rre->height, rre->width);
|
||||
} else {
|
||||
XResizeWindow(dpy, locks[screen]->win,
|
||||
rre->width, rre->height);
|
||||
resizedraw(dpy, locks[screen],
|
||||
rre->width, rre->height);
|
||||
}
|
||||
drawlock(dpy, locks[screen], &st);
|
||||
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;
|
||||
}
|
||||
|
||||
setupdraw(dpy, lock);
|
||||
|
||||
/* init */
|
||||
wa.override_redirect = 1;
|
||||
wa.background_pixel = lock->ui[COLBG].pixel;
|
||||
wa.event_mask = ExposureMask;
|
||||
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 | CWEventMask,
|
||||
&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
|
||||
|
||||
/* nombre del usuario que bloquea, para mostrarlo */
|
||||
if ((pwd = getpwuid(getuid())))
|
||||
snprintf(username, sizeof(username), "%s", pwd->pw_name);
|
||||
|
||||
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
suckless/slock/util.h
Normal file
2
suckless/slock/util.h
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
#undef explicit_bzero
|
||||
void explicit_bzero(void *, size_t);
|
||||
46
suckless/slstatus/LICENSE
Normal file
46
suckless/slstatus/LICENSE
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
ISC License
|
||||
|
||||
Copyright 2016-2026 Aaron Marcher <me@drkhsh.at>
|
||||
|
||||
Copyright 2016 Roy Freytag <rfreytag@hs-mittweida.de>
|
||||
Copyright 2016 Vincent Loupmon <vincentloupmon@gmail.com>
|
||||
Copyright 2016 Daniel Walter <d.walter@0x90.at>
|
||||
Copyright 2016-2018 Ali H. Fardan <raiz@firemail.cc>
|
||||
Copyright 2016 Jody Leonard <me@jodyleonard.com>
|
||||
Copyright 2016-2018 Quentin Rameau <quinq@fifth.space>
|
||||
Copyright 2016 Mike Coddington <mike@coddington.us>
|
||||
Copyright 2016-2018 Ivan J. <parazyd@dyne.org>
|
||||
Copyright 2017 Tobias Stoeckmann <tobias@stoeckmann.org>
|
||||
Copyright 2017-2018 Laslo Hunhold <dev@frign.de>
|
||||
Copyright 2018 Darron Anderson <darronanderson@protonmail.com>
|
||||
Copyright 2018 Josuah Demangeon <mail@josuah.net>
|
||||
Copyright 2018 Tobias Tschinkowitz <tobias@he4d.net>
|
||||
Copyright 2018 David Demelier <markand@malikania.fr>
|
||||
Copyright 2018-2012 Michael Buch <michaelbuch12@gmail.com>
|
||||
Copyright 2018 Ian Remmler <ian@remmler.org>
|
||||
Copyright 2016-2019 Joerg Jung <jung@openbsd.org>
|
||||
Copyright 2019 Ryan Kes <alrayyes@gmail.com>
|
||||
Copyright 2019 Cem Keylan <cem@ckyln.com>
|
||||
Copyright 2019 Spiros Thanasoulas <dsp@2f30.org>
|
||||
Copyright 2019-2022 Ingo Feinerer <feinerer@logic.at>
|
||||
Copyright 2020 Alexandre Ratchov <alex@caoua.org>
|
||||
Copyright 2020 Mart Lubbers <mart@martlubbers.net>
|
||||
Copyright 2020 Daniel Moch <daniel@danielmoch.com>
|
||||
Copyright 2022 Nickolas Raymond Kaczynski <nrk@disroot.org>
|
||||
Copyright 2022 Patrick Iacob <iacobp@oregonstate.edu>
|
||||
Copyright 2021-2022 Steven Ward <planet36@gmail.com>
|
||||
Copyright 2025 Joakim Sindholt <opensource@zhasha.com>
|
||||
Copyright 2025 Al <eirann@disroot.org>
|
||||
Copyright 2025 sewn <sewn@disroot.org>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
69
suckless/slstatus/Makefile
Normal file
69
suckless/slstatus/Makefile
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# See LICENSE file for copyright and license details
|
||||
# slstatus - suckless status monitor
|
||||
.POSIX:
|
||||
|
||||
include config.mk
|
||||
|
||||
REQ = util
|
||||
COM =\
|
||||
components/battery\
|
||||
components/cat\
|
||||
components/cpu\
|
||||
components/datetime\
|
||||
components/disk\
|
||||
components/entropy\
|
||||
components/hostname\
|
||||
components/ip\
|
||||
components/kernel_release\
|
||||
components/keyboard_indicators\
|
||||
components/keymap\
|
||||
components/load_avg\
|
||||
components/netspeeds\
|
||||
components/num_files\
|
||||
components/ram\
|
||||
components/run_command\
|
||||
components/swap\
|
||||
components/temperature\
|
||||
components/uptime\
|
||||
components/user\
|
||||
components/volume\
|
||||
components/wifi
|
||||
|
||||
all: slstatus
|
||||
|
||||
$(COM:=.o): config.mk $(REQ:=.h) slstatus.h
|
||||
slstatus.o: slstatus.c slstatus.h arg.h config.h config.mk $(REQ:=.h)
|
||||
|
||||
.c.o:
|
||||
$(CC) -o $@ -c $(CPPFLAGS) $(CFLAGS) $<
|
||||
|
||||
config.h:
|
||||
cp config.def.h $@
|
||||
|
||||
slstatus: slstatus.o $(COM:=.o) $(REQ:=.o)
|
||||
$(CC) -o $@ $(LDFLAGS) $(COM:=.o) $(REQ:=.o) slstatus.o $(LDLIBS)
|
||||
|
||||
clean:
|
||||
rm -f slstatus slstatus.o $(COM:=.o) $(REQ:=.o) slstatus-${VERSION}.tar.gz
|
||||
|
||||
dist:
|
||||
rm -rf "slstatus-$(VERSION)"
|
||||
mkdir -p "slstatus-$(VERSION)/components"
|
||||
cp -R LICENSE Makefile README config.mk config.def.h \
|
||||
arg.h slstatus.h slstatus.c $(REQ:=.c) $(REQ:=.h) \
|
||||
slstatus.1 "slstatus-$(VERSION)"
|
||||
cp -R $(COM:=.c) "slstatus-$(VERSION)/components"
|
||||
tar -cf - "slstatus-$(VERSION)" | gzip -c > "slstatus-$(VERSION).tar.gz"
|
||||
rm -rf "slstatus-$(VERSION)"
|
||||
|
||||
install: all
|
||||
mkdir -p "$(DESTDIR)$(PREFIX)/bin"
|
||||
cp -f slstatus "$(DESTDIR)$(PREFIX)/bin"
|
||||
chmod 755 "$(DESTDIR)$(PREFIX)/bin/slstatus"
|
||||
mkdir -p "$(DESTDIR)$(MANPREFIX)/man1"
|
||||
cp -f slstatus.1 "$(DESTDIR)$(MANPREFIX)/man1"
|
||||
chmod 644 "$(DESTDIR)$(MANPREFIX)/man1/slstatus.1"
|
||||
|
||||
uninstall:
|
||||
rm -f "$(DESTDIR)$(PREFIX)/bin/slstatus"
|
||||
rm -f "$(DESTDIR)$(MANPREFIX)/man1/slstatus.1"
|
||||
65
suckless/slstatus/README
Normal file
65
suckless/slstatus/README
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
slstatus - suckless status
|
||||
==========================
|
||||
slstatus is a small tool for providing system status information to other
|
||||
programs over the EWMH property of the root window (used by dwm(1)) or
|
||||
standard input/output. It is designed to be as efficient as possible by
|
||||
only issuing the minimum of system calls required.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
- Battery percentage/state/time left
|
||||
- Cat (read file)
|
||||
- CPU usage
|
||||
- CPU frequency
|
||||
- Custom shell commands
|
||||
- Date and time
|
||||
- Disk status (free storage, percentage, total storage and used storage)
|
||||
- Available entropy
|
||||
- Username/GID/UID
|
||||
- Hostname
|
||||
- IP address (IPv4 and IPv6), interface status
|
||||
- Kernel version
|
||||
- Keyboard indicators
|
||||
- Keymap
|
||||
- Load average
|
||||
- Network speeds (RX and TX)
|
||||
- Number of files in a directory (hint: Maildir)
|
||||
- Memory status (free memory, percentage, total memory and used memory)
|
||||
- Swap status (free swap, percentage, total swap and used swap)
|
||||
- Temperature
|
||||
- Uptime
|
||||
- Volume percentage
|
||||
- WiFi signal percentage and ESSID
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
Currently slstatus works on FreeBSD, Linux and OpenBSD.
|
||||
In order to build slstatus you need the Xlib header files.
|
||||
|
||||
- For volume percentage on Linux the kernel module `snd-mixer-oss` must be
|
||||
loaded.
|
||||
- For volume percentage on FreeBSD, `sndio` must be installed.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
Edit config.mk to match your local setup (slstatus is installed into the
|
||||
/usr/local namespace by default).
|
||||
|
||||
Afterwards enter the following command to build and install slstatus (if
|
||||
necessary as root):
|
||||
|
||||
make clean install
|
||||
|
||||
|
||||
Running slstatus
|
||||
----------------
|
||||
See the man page for details.
|
||||
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
slstatus can be customized by creating a custom config.h and (re)compiling the
|
||||
source code. This keeps it fast, secure and simple.
|
||||
33
suckless/slstatus/arg.h
Normal file
33
suckless/slstatus/arg.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#ifndef ARG_H
|
||||
#define ARG_H
|
||||
|
||||
extern char *argv0;
|
||||
|
||||
/* int main(int argc, char *argv[]) */
|
||||
#define ARGBEGIN for (argv0 = *argv, *argv ? (argc--, argv++) : ((void *)0); \
|
||||
*argv && (*argv)[0] == '-' && (*argv)[1]; argc--, argv++) { \
|
||||
int i_, argused_; \
|
||||
if ((*argv)[1] == '-' && !(*argv)[2]) { \
|
||||
argc--, argv++; \
|
||||
break; \
|
||||
} \
|
||||
for (i_ = 1, argused_ = 0; (*argv)[i_]; i_++) { \
|
||||
switch ((*argv)[i_])
|
||||
#define ARGEND if (argused_) { \
|
||||
if ((*argv)[i_ + 1]) { \
|
||||
break; \
|
||||
} else { \
|
||||
argc--, argv++; \
|
||||
break; \
|
||||
} \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
#define ARGC() ((*argv)[i_])
|
||||
#define ARGF_(x) (((*argv)[i_ + 1]) ? (argused_ = 1, &((*argv)[i_ + 1])) : \
|
||||
(*(argv + 1)) ? (argused_ = 1, *(argv + 1)) : (x))
|
||||
#define EARGF(x) ARGF_(((x), exit(1), (char *)0))
|
||||
#define ARGF() ARGF_((char *)0)
|
||||
|
||||
#endif
|
||||
247
suckless/slstatus/components/battery.c
Normal file
247
suckless/slstatus/components/battery.c
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../slstatus.h"
|
||||
#include "../util.h"
|
||||
|
||||
#if defined(__linux__)
|
||||
/*
|
||||
* https://www.kernel.org/doc/html/latest/power/power_supply_class.html
|
||||
*/
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define POWER_SUPPLY_CAPACITY "/sys/class/power_supply/%s/capacity"
|
||||
#define POWER_SUPPLY_STATUS "/sys/class/power_supply/%s/status"
|
||||
#define POWER_SUPPLY_CHARGE "/sys/class/power_supply/%s/charge_now"
|
||||
#define POWER_SUPPLY_ENERGY "/sys/class/power_supply/%s/energy_now"
|
||||
#define POWER_SUPPLY_CURRENT "/sys/class/power_supply/%s/current_now"
|
||||
#define POWER_SUPPLY_POWER "/sys/class/power_supply/%s/power_now"
|
||||
|
||||
static const char *
|
||||
pick(const char *bat, const char *f1, const char *f2, char *path,
|
||||
size_t length)
|
||||
{
|
||||
if (esnprintf(path, length, f1, bat) > 0 &&
|
||||
access(path, R_OK) == 0)
|
||||
return f1;
|
||||
|
||||
if (esnprintf(path, length, f2, bat) > 0 &&
|
||||
access(path, R_OK) == 0)
|
||||
return f2;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *
|
||||
battery_perc(const char *bat)
|
||||
{
|
||||
int cap_perc;
|
||||
char path[PATH_MAX];
|
||||
|
||||
if (esnprintf(path, sizeof(path), POWER_SUPPLY_CAPACITY, bat) < 0)
|
||||
return NULL;
|
||||
if (pscanf(path, "%d", &cap_perc) != 1)
|
||||
return NULL;
|
||||
|
||||
return bprintf("%d", cap_perc);
|
||||
}
|
||||
|
||||
const char *
|
||||
battery_state(const char *bat)
|
||||
{
|
||||
static struct {
|
||||
char *state;
|
||||
char *symbol;
|
||||
} map[] = {
|
||||
{ "Charging", "+" },
|
||||
{ "Discharging", "-" },
|
||||
{ "Full", "o" },
|
||||
{ "Not charging", "o" },
|
||||
};
|
||||
size_t i;
|
||||
char path[PATH_MAX], state[13];
|
||||
|
||||
if (esnprintf(path, sizeof(path), POWER_SUPPLY_STATUS, bat) < 0)
|
||||
return NULL;
|
||||
if (pscanf(path, "%12[a-zA-Z ]", state) != 1)
|
||||
return NULL;
|
||||
|
||||
for (i = 0; i < LEN(map); i++)
|
||||
if (!strcmp(map[i].state, state))
|
||||
break;
|
||||
|
||||
return (i == LEN(map)) ? "?" : map[i].symbol;
|
||||
}
|
||||
|
||||
const char *
|
||||
battery_remaining(const char *bat)
|
||||
{
|
||||
uintmax_t charge_now, current_now, m, h;
|
||||
double timeleft;
|
||||
char path[PATH_MAX], state[13];
|
||||
|
||||
if (esnprintf(path, sizeof(path), POWER_SUPPLY_STATUS, bat) < 0)
|
||||
return NULL;
|
||||
if (pscanf(path, "%12[a-zA-Z ]", state) != 1)
|
||||
return NULL;
|
||||
|
||||
if (!pick(bat, POWER_SUPPLY_CHARGE, POWER_SUPPLY_ENERGY, path,
|
||||
sizeof(path)) ||
|
||||
pscanf(path, "%ju", &charge_now) < 0)
|
||||
return NULL;
|
||||
|
||||
if (!strcmp(state, "Discharging")) {
|
||||
if (!pick(bat, POWER_SUPPLY_CURRENT, POWER_SUPPLY_POWER, path,
|
||||
sizeof(path)) ||
|
||||
pscanf(path, "%ju", ¤t_now) < 0)
|
||||
return NULL;
|
||||
|
||||
if (current_now == 0)
|
||||
return NULL;
|
||||
|
||||
timeleft = (double)charge_now / (double)current_now;
|
||||
h = timeleft;
|
||||
m = (timeleft - (double)h) * 60;
|
||||
|
||||
return bprintf("%juh %jum", h, m);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
#elif defined(__OpenBSD__)
|
||||
#include <fcntl.h>
|
||||
#include <machine/apmvar.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int
|
||||
load_apm_power_info(struct apm_power_info *apm_info)
|
||||
{
|
||||
int fd;
|
||||
|
||||
fd = open("/dev/apm", O_RDONLY);
|
||||
if (fd < 0) {
|
||||
warn("open '/dev/apm':");
|
||||
return 0;
|
||||
}
|
||||
|
||||
memset(apm_info, 0, sizeof(struct apm_power_info));
|
||||
if (ioctl(fd, APM_IOC_GETPOWER, apm_info) < 0) {
|
||||
warn("ioctl 'APM_IOC_GETPOWER':");
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
return close(fd), 1;
|
||||
}
|
||||
|
||||
const char *
|
||||
battery_perc(const char *unused)
|
||||
{
|
||||
struct apm_power_info apm_info;
|
||||
|
||||
if (load_apm_power_info(&apm_info))
|
||||
return bprintf("%d", apm_info.battery_life);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *
|
||||
battery_state(const char *unused)
|
||||
{
|
||||
struct {
|
||||
unsigned int state;
|
||||
char *symbol;
|
||||
} map[] = {
|
||||
{ APM_AC_ON, "+" },
|
||||
{ APM_AC_OFF, "-" },
|
||||
};
|
||||
struct apm_power_info apm_info;
|
||||
size_t i;
|
||||
|
||||
if (load_apm_power_info(&apm_info)) {
|
||||
for (i = 0; i < LEN(map); i++)
|
||||
if (map[i].state == apm_info.ac_state)
|
||||
break;
|
||||
|
||||
return (i == LEN(map)) ? "?" : map[i].symbol;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *
|
||||
battery_remaining(const char *unused)
|
||||
{
|
||||
struct apm_power_info apm_info;
|
||||
unsigned int h, m;
|
||||
|
||||
if (load_apm_power_info(&apm_info)) {
|
||||
if (apm_info.ac_state != APM_AC_ON) {
|
||||
h = apm_info.minutes_left / 60;
|
||||
m = apm_info.minutes_left % 60;
|
||||
return bprintf("%uh %02um", h, m);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
#elif defined(__FreeBSD__)
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
#define BATTERY_LIFE "hw.acpi.battery.life"
|
||||
#define BATTERY_STATE "hw.acpi.battery.state"
|
||||
#define BATTERY_TIME "hw.acpi.battery.time"
|
||||
|
||||
const char *
|
||||
battery_perc(const char *unused)
|
||||
{
|
||||
int cap_perc;
|
||||
size_t len;
|
||||
|
||||
len = sizeof(cap_perc);
|
||||
if (sysctlbyname(BATTERY_LIFE, &cap_perc, &len, NULL, 0) < 0 || !len)
|
||||
return NULL;
|
||||
|
||||
return bprintf("%d", cap_perc);
|
||||
}
|
||||
|
||||
const char *
|
||||
battery_state(const char *unused)
|
||||
{
|
||||
int state;
|
||||
size_t len;
|
||||
|
||||
len = sizeof(state);
|
||||
if (sysctlbyname(BATTERY_STATE, &state, &len, NULL, 0) < 0 || !len)
|
||||
return NULL;
|
||||
|
||||
switch (state) {
|
||||
case 0: /* FALLTHROUGH */
|
||||
case 2:
|
||||
return "+";
|
||||
case 1:
|
||||
return "-";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
const char *
|
||||
battery_remaining(const char *unused)
|
||||
{
|
||||
int rem;
|
||||
size_t len;
|
||||
|
||||
len = sizeof(rem);
|
||||
if (sysctlbyname(BATTERY_TIME, &rem, &len, NULL, 0) < 0 || !len
|
||||
|| rem < 0)
|
||||
return NULL;
|
||||
|
||||
return bprintf("%uh %02um", rem / 60, rem % 60);
|
||||
}
|
||||
#endif
|
||||
32
suckless/slstatus/components/cat.c
Normal file
32
suckless/slstatus/components/cat.c
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../slstatus.h"
|
||||
#include "../util.h"
|
||||
|
||||
const char *
|
||||
cat(const char *path)
|
||||
{
|
||||
char *f;
|
||||
FILE *fp;
|
||||
|
||||
if (!(fp = fopen(path, "r"))) {
|
||||
warn("fopen '%s':", path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
f = fgets(buf, sizeof(buf) - 1, fp);
|
||||
if (fclose(fp) < 0) {
|
||||
warn("fclose '%s':", path);
|
||||
return NULL;
|
||||
}
|
||||
if (!f)
|
||||
return NULL;
|
||||
|
||||
if ((f = strrchr(buf, '\n')))
|
||||
f[0] = '\0';
|
||||
|
||||
return buf[0] ? buf : NULL;
|
||||
}
|
||||
|
||||
157
suckless/slstatus/components/cpu.c
Normal file
157
suckless/slstatus/components/cpu.c
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../slstatus.h"
|
||||
#include "../util.h"
|
||||
|
||||
#if defined(__linux__)
|
||||
#define CPU_FREQ "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"
|
||||
|
||||
const char *
|
||||
cpu_freq(const char *unused)
|
||||
{
|
||||
uintmax_t freq;
|
||||
|
||||
/* in kHz */
|
||||
if (pscanf(CPU_FREQ, "%ju", &freq) != 1)
|
||||
return NULL;
|
||||
|
||||
return fmt_human(freq * 1000, 1000);
|
||||
}
|
||||
|
||||
const char *
|
||||
cpu_perc(const char *unused)
|
||||
{
|
||||
static long double a[7];
|
||||
long double b[7], sum;
|
||||
|
||||
memcpy(b, a, sizeof(b));
|
||||
/* cpu user nice system idle iowait irq softirq */
|
||||
if (pscanf("/proc/stat", "%*s %Lf %Lf %Lf %Lf %Lf %Lf %Lf",
|
||||
&a[0], &a[1], &a[2], &a[3], &a[4], &a[5], &a[6])
|
||||
!= 7)
|
||||
return NULL;
|
||||
|
||||
if (b[0] == 0)
|
||||
return NULL;
|
||||
|
||||
sum = (b[0] + b[1] + b[2] + b[3] + b[4] + b[5] + b[6]) -
|
||||
(a[0] + a[1] + a[2] + a[3] + a[4] + a[5] + a[6]);
|
||||
|
||||
if (sum == 0)
|
||||
return NULL;
|
||||
|
||||
return bprintf("%d", (int)(100 *
|
||||
((b[0] + b[1] + b[2] + b[5] + b[6]) -
|
||||
(a[0] + a[1] + a[2] + a[5] + a[6])) / sum));
|
||||
}
|
||||
#elif defined(__OpenBSD__)
|
||||
#include <sys/param.h>
|
||||
#include <sys/sched.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
const char *
|
||||
cpu_freq(const char *unused)
|
||||
{
|
||||
int freq, mib[2];
|
||||
size_t size;
|
||||
|
||||
mib[0] = CTL_HW;
|
||||
mib[1] = HW_CPUSPEED;
|
||||
|
||||
size = sizeof(freq);
|
||||
|
||||
/* in MHz */
|
||||
if (sysctl(mib, 2, &freq, &size, NULL, 0) < 0) {
|
||||
warn("sysctl 'HW_CPUSPEED':");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return fmt_human(freq * 1E6, 1000);
|
||||
}
|
||||
|
||||
const char *
|
||||
cpu_perc(const char *unused)
|
||||
{
|
||||
int mib[2];
|
||||
static uintmax_t a[CPUSTATES];
|
||||
uintmax_t b[CPUSTATES], sum;
|
||||
size_t size;
|
||||
|
||||
mib[0] = CTL_KERN;
|
||||
mib[1] = KERN_CPTIME;
|
||||
|
||||
size = sizeof(a);
|
||||
|
||||
memcpy(b, a, sizeof(b));
|
||||
if (sysctl(mib, 2, &a, &size, NULL, 0) < 0) {
|
||||
warn("sysctl 'KERN_CPTIME':");
|
||||
return NULL;
|
||||
}
|
||||
if (b[0] == 0)
|
||||
return NULL;
|
||||
|
||||
sum = (a[CP_USER] + a[CP_NICE] + a[CP_SYS] + a[CP_INTR] + a[CP_IDLE]) -
|
||||
(b[CP_USER] + b[CP_NICE] + b[CP_SYS] + b[CP_INTR] + b[CP_IDLE]);
|
||||
|
||||
if (sum == 0)
|
||||
return NULL;
|
||||
|
||||
return bprintf("%d", 100 *
|
||||
((a[CP_USER] + a[CP_NICE] + a[CP_SYS] +
|
||||
a[CP_INTR]) -
|
||||
(b[CP_USER] + b[CP_NICE] + b[CP_SYS] +
|
||||
b[CP_INTR])) / sum);
|
||||
}
|
||||
#elif defined(__FreeBSD__)
|
||||
#include <devstat.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
const char *
|
||||
cpu_freq(const char *unused)
|
||||
{
|
||||
int freq;
|
||||
size_t size;
|
||||
|
||||
size = sizeof(freq);
|
||||
/* in MHz */
|
||||
if (sysctlbyname("hw.clockrate", &freq, &size, NULL, 0) < 0 || !size) {
|
||||
warn("sysctlbyname 'hw.clockrate':");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return fmt_human(freq * 1E6, 1000);
|
||||
}
|
||||
|
||||
const char *
|
||||
cpu_perc(const char *unused)
|
||||
{
|
||||
size_t size;
|
||||
static long a[CPUSTATES];
|
||||
long b[CPUSTATES], sum;
|
||||
|
||||
size = sizeof(a);
|
||||
memcpy(b, a, sizeof(b));
|
||||
if (sysctlbyname("kern.cp_time", &a, &size, NULL, 0) < 0 || !size) {
|
||||
warn("sysctlbyname 'kern.cp_time':");
|
||||
return NULL;
|
||||
}
|
||||
if (b[0] == 0)
|
||||
return NULL;
|
||||
|
||||
sum = (a[CP_USER] + a[CP_NICE] + a[CP_SYS] + a[CP_INTR] + a[CP_IDLE]) -
|
||||
(b[CP_USER] + b[CP_NICE] + b[CP_SYS] + b[CP_INTR] + b[CP_IDLE]);
|
||||
|
||||
if (sum == 0)
|
||||
return NULL;
|
||||
|
||||
return bprintf("%d", 100 *
|
||||
((a[CP_USER] + a[CP_NICE] + a[CP_SYS] +
|
||||
a[CP_INTR]) -
|
||||
(b[CP_USER] + b[CP_NICE] + b[CP_SYS] +
|
||||
b[CP_INTR])) / sum);
|
||||
}
|
||||
#endif
|
||||
20
suckless/slstatus/components/datetime.c
Normal file
20
suckless/slstatus/components/datetime.c
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* See LICENSE file for copyright and license details. */
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "../slstatus.h"
|
||||
#include "../util.h"
|
||||
|
||||
const char *
|
||||
datetime(const char *fmt)
|
||||
{
|
||||
time_t t;
|
||||
|
||||
t = time(NULL);
|
||||
if (!strftime(buf, sizeof(buf), fmt, localtime(&t))) {
|
||||
warn("strftime: Result string exceeds buffer size");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue