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:
Alejandro Guerrero 2026-09-27 17:51:52 +02:00
parent ce3a0db5bf
commit fbda573897
148 changed files with 14 additions and 14 deletions

View 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

View 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
View 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
View 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
View 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

View file

@ -0,0 +1,97 @@
clipmenu is a simple clipboard manager using [dmenu][], [rofi][] or similar.
# Demo
![Demo](https://cloud.githubusercontent.com/assets/660663/24079784/6f76da94-0c88-11e7-8251-40b1f02ebf3c.gif)
# 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

View 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

View 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 .

View 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 .

View 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 .

View 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 .

View 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 .

View 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 .

View 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 .

View 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);
}

View 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;
}

View 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);
}

View 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);
}

View 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)&timestamp_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

View 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;
}

View 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();
}
}
}

View 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

View 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;
}

View 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

View 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;
}

View 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

View 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;
}

View 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
View 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
View 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

View 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;
}

View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
#!/bin/sh
dmenu_path | dmenu "$@" | ${SHELL:-"/bin/sh"} &

471
suckless/dmenu/drw.c Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

BIN
suckless/dwm/dwm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

View 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;
}
}

View 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

View 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

View file

@ -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

View 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
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
scroll
ptty
*.swp
*.core
config.h

16
suckless/scroll/LICENSE Normal file
View 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
View 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
View 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
View 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)

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
[?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

15
suckless/scroll/up.sh Executable file
View 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
View 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
View 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
View 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.

View 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
View 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
View 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 =

View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
#undef explicit_bzero
void explicit_bzero(void *, size_t);

46
suckless/slstatus/LICENSE Normal file
View 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.

View 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
View 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
View 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

View 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", &current_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

View 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;
}

View 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

View 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