From cb051ead76619089a2214c1ed8bffb91bdfbe642 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 1/7] Squashed 'suckless/clipmenu/' content from commit e0ec3bc6 git-subtree-dir: suckless/clipmenu git-subtree-split: e0ec3bc6608ecc70ed17e8da76e27bf90b1e8e90 --- .clang-format | 11 + .github/workflows/ci.yml | 28 ++ .gitignore | 9 + LICENSE | 21 + Makefile | 135 ++++++ README.md | 97 +++++ init/clipmenud.service.in | 21 + man/clipctl.1 | 56 +++ man/clipdel.1 | 53 +++ man/clipdelmenu.1 | 43 ++ man/clipmenu.1 | 42 ++ man/clipmenu.conf.5 | 124 ++++++ man/clipmenud.1 | 32 ++ man/clipserve.1 | 44 ++ src/clipctl.c | 152 +++++++ src/clipdel.c | 162 ++++++++ src/clipdelmenu.c | 45 ++ src/clipmenu.c | 42 ++ src/clipmenud.c | 765 ++++++++++++++++++++++++++++++++++ src/clipserve.c | 286 +++++++++++++ src/config.c | 500 ++++++++++++++++++++++ src/config.h | 104 +++++ src/menu_util.c | 302 ++++++++++++++ src/menu_util.h | 18 + src/store.c | 847 +++++++++++++++++++++++++++++++++++++ src/store.h | 192 +++++++++ src/util.c | 165 ++++++++ src/util.h | 111 +++++ src/x.c | 121 ++++++ src/x.h | 34 ++ tests/test_store.c | 848 ++++++++++++++++++++++++++++++++++++++ tests/x_integration_tests | 350 ++++++++++++++++ 32 files changed, 5760 insertions(+) create mode 100644 .clang-format create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 init/clipmenud.service.in create mode 100644 man/clipctl.1 create mode 100644 man/clipdel.1 create mode 100644 man/clipdelmenu.1 create mode 100644 man/clipmenu.1 create mode 100644 man/clipmenu.conf.5 create mode 100644 man/clipmenud.1 create mode 100644 man/clipserve.1 create mode 100644 src/clipctl.c create mode 100644 src/clipdel.c create mode 100644 src/clipdelmenu.c create mode 100644 src/clipmenu.c create mode 100644 src/clipmenud.c create mode 100644 src/clipserve.c create mode 100644 src/config.c create mode 100644 src/config.h create mode 100644 src/menu_util.c create mode 100644 src/menu_util.h create mode 100644 src/store.c create mode 100644 src/store.h create mode 100644 src/util.c create mode 100644 src/util.h create mode 100644 src/x.c create mode 100644 src/x.h create mode 100644 tests/test_store.c create mode 100755 tests/x_integration_tests diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..076d317 --- /dev/null +++ b/.clang-format @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b412fc6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3668d31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +src/clipmenud +src/clipctl +src/clipdel +src/clipdelmenu +src/clipmenu +src/clipserve +tests/test +tests/test_store +*.o diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9019292 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..934c0d1 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf29fac --- /dev/null +++ b/README.md @@ -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 diff --git a/init/clipmenud.service.in b/init/clipmenud.service.in new file mode 100644 index 0000000..f68267b --- /dev/null +++ b/init/clipmenud.service.in @@ -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 diff --git a/man/clipctl.1 b/man/clipctl.1 new file mode 100644 index 0000000..1b95e05 --- /dev/null +++ b/man/clipctl.1 @@ -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 . diff --git a/man/clipdel.1 b/man/clipdel.1 new file mode 100644 index 0000000..1d93acd --- /dev/null +++ b/man/clipdel.1 @@ -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 . diff --git a/man/clipdelmenu.1 b/man/clipdelmenu.1 new file mode 100644 index 0000000..72f76ee --- /dev/null +++ b/man/clipdelmenu.1 @@ -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 . diff --git a/man/clipmenu.1 b/man/clipmenu.1 new file mode 100644 index 0000000..00cfd00 --- /dev/null +++ b/man/clipmenu.1 @@ -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 . diff --git a/man/clipmenu.conf.5 b/man/clipmenu.conf.5 new file mode 100644 index 0000000..2c93bed --- /dev/null +++ b/man/clipmenu.conf.5 @@ -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.. 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 . diff --git a/man/clipmenud.1 b/man/clipmenud.1 new file mode 100644 index 0000000..65f367a --- /dev/null +++ b/man/clipmenud.1 @@ -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 . diff --git a/man/clipserve.1 b/man/clipserve.1 new file mode 100644 index 0000000..72f6d9b --- /dev/null +++ b/man/clipserve.1 @@ -0,0 +1,44 @@ +.TH CLIPSERVE 1 +.SH NAME +clipserve \- serve a selected clipboard entry to X11 selections +.SH SYNOPSIS +.B clipserve +[\-s selection]... +.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 . diff --git a/src/clipctl.c b/src/clipctl.c new file mode 100644 index 0000000..7f5c961 --- /dev/null +++ b/src/clipctl.c @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 \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); +} diff --git a/src/clipdel.c b/src/clipdel.c new file mode 100644 index 0000000..30fd0b7 --- /dev/null +++ b/src/clipdel.c @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/src/clipdelmenu.c b/src/clipdelmenu.c new file mode 100644 index 0000000..255c7bf --- /dev/null +++ b/src/clipdelmenu.c @@ -0,0 +1,45 @@ +#include +#include +#include + +#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); +} diff --git a/src/clipmenu.c b/src/clipmenu.c new file mode 100644 index 0000000..a7e1982 --- /dev/null +++ b/src/clipmenu.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include +#include + +#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); +} diff --git a/src/clipmenud.c b/src/clipmenud.c new file mode 100644 index 0000000..446a2d7 --- /dev/null +++ b/src/clipmenud.c @@ -0,0 +1,765 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "store.h" +#include "util.h" +#include "x.h" + +static Display *dpy; +static struct clip_store cs; +static struct config cfg; +static Window win; + +static int enabled = 1; +static int sig_pipe[2]; + +static Atom timestamp_atom; +static Atom incr_atom; +static struct incr_transfer *it_list; + +static struct cm_selections sels[CM_SEL_MAX]; + +static Time last_disable_time = 0; +static Time last_enable_time = 0; + +static bool has_owned_selections(void) { + for (size_t i = 0; i < CM_SEL_MAX; i++) { + if (cfg.owned_selections[i].active) { + return true; + } + } + return false; +} + +enum clip_text_source { + CLIP_TEXT_SOURCE_X, + CLIP_TEXT_SOURCE_MALLOC, + CLIP_TEXT_SOURCE_INVALID +}; + +struct clip_text { + char *data; + enum clip_text_source source; +}; + +static struct clip_text last_text[CM_SEL_MAX] = { + {NULL, CLIP_TEXT_SOURCE_MALLOC}, + {NULL, CLIP_TEXT_SOURCE_MALLOC}, + {NULL, CLIP_TEXT_SOURCE_MALLOC}, +}; +static struct timespec last_text_time[CM_SEL_MAX]; + +static void free_clip_text(struct clip_text *ct) { + expect(ct->source != CLIP_TEXT_SOURCE_INVALID); + + if (ct->data) { + if (ct->source == CLIP_TEXT_SOURCE_X) { + XFree(ct->data); + } else { + free(ct->data); + } + ct->data = NULL; + } + + ct->source = CLIP_TEXT_SOURCE_INVALID; +} + +static struct timespec get_monotonic_time(void) { + struct timespec ts; + expect(clock_gettime(CLOCK_MONOTONIC, &ts) == 0); + return ts; +} + +static bool within_partial_merge_window(struct timespec current_time, + struct timespec last_time) { + time_t elapsed_secs = current_time.tv_sec - last_time.tv_sec; + + if (elapsed_secs < cfg.partial_merge_secs) { + return true; + } + if (elapsed_secs > cfg.partial_merge_secs) { + return false; + } + return current_time.tv_nsec <= last_time.tv_nsec; +} + +// cppcheck-suppress [constParameterCallback,unmatchedSuppression] +static Bool is_timestamp_event(Display *display _unused_, XEvent *event, + XPointer arg) { + const void *argp = arg; + return event->type == PropertyNotify && + event->xproperty.atom == *(const Atom *)argp; +} + +/** + * Get the current X server time by triggering a PropertyNotify. + */ +static Time get_current_server_time(void) { + XEvent ev; + XChangeProperty(dpy, win, timestamp_atom, XA_INTEGER, 32, PropModeReplace, + NULL, 0); + XIfEvent(dpy, &ev, is_timestamp_event, (XPointer)×tamp_atom); + return ev.xproperty.time; +} + +/** + * Check if a text s1 is a possible partial of s2. + * + * Chromium and some other badly behaved applications spam PRIMARY during + * selection, so if you're selecting the text "abc", you get three clips: "a", + * "ab", and "abc" (or "c", "bc", "abc" if selecting right to left). Attempt to + * detect these. It's possible we were not fast enough to get all of them, so + * unfortunately we can't check for strlen(s1)+1 either. It's also possible the + * user first expands, and then retracts the selection, so we need to handle + * that too. + */ +static bool is_possible_partial(const char *s1, const char *s2) { + size_t len1 = strlen(s1), len2 = strlen(s2); + + // Is one a prefix of the other? + if (strncmp(s1, s2, len1 < len2 ? len1 : len2) == 0) { + return true; + } + + // Is one a suffix of the other? + if (len1 < len2) { + return strcmp(s1, s2 + len2 - len1) == 0; + } else { + return strcmp(s2, s1 + len1 - len2) == 0; + } +} + +/** + * Retrieve the converted text put into our clip atom. In order for this to + * happen a conversion must have been performed in an earlier iteration with + * XConvertSelection. + */ +static struct clip_text get_clipboard_text(Atom clip_atom) { + struct clip_text ct = {NULL, CLIP_TEXT_SOURCE_X}; + unsigned char *cur_text; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + + int res = XGetWindowProperty(dpy, win, clip_atom, 0L, (~0L), False, + AnyPropertyType, &actual_type, &actual_format, + &nitems, &bytes_after, &cur_text); + if (res != Success) { + return ct; + } + + if (actual_type == incr_atom) { + dbg("Unexpected INCR transfer detected\n"); + XFree(cur_text); + return ct; + } + + ct.data = (char *)cur_text; + + return ct; +} + +/** + * Return true if the given string contains any non-whitespace characters. + */ +static bool is_salient_text(const char *str) { + if (!str) { + return false; + } + + for (; *str; str++) { + if (!isspace((unsigned char)*str)) { + return true; + } + } + return false; +} + +/** + * Write the current enabled status to a designated status file. + */ +static void write_status(void) { + _drop_(close) int fd = + open(get_enabled_path(&cfg), O_WRONLY | O_CREAT, 0600); + die_on(fd < 0, "Failed to update status: %s\n", strerror(errno)); + dprintf(fd, "%d", (int)enabled); +} + +/** + * Return true if the given window title matches the title of the clipserve + * window. + */ +static bool is_clipserve(const char *win_title) { + return win_title && streq(win_title, "clipserve"); +} + +/** + * Determine if a window with the given title should be ignored based on user + * configuration. + */ +static bool is_ignored_window(char *win_title) { + if (!win_title || !cfg.ignore_window.set) { + return 0; + } + int ret = regexec(&cfg.ignore_window.rgx, win_title, 0, NULL, 0); + expect(ret == 0 || ret == REG_NOMATCH); + return !ret; +} + +/** + * Disable or enable clip collection based on received signals. + */ +static void handle_signal_event(int signo) { + dbg("Got signal %d\n", signo); + switch (signo) { + case SIGUSR1: + // If we're already disabled, we need to keep the original + // timestamp to properly filter all events that were queued during + // any part of the disabled period. + if (enabled) { + last_disable_time = get_current_server_time(); + } + enabled = 0; + dbg("Clipboard collection disabled by signal at time %lu\n", + (unsigned long)last_disable_time); + break; + case SIGUSR2: + // If we're already enabled, we need to keep the original timestamp + // so we don't mistakenly filter out valid messages. + if (!enabled) { + last_enable_time = get_current_server_time(); + } + enabled = 1; + dbg("Clipboard collection enabled by signal at time %lu\n", + (unsigned long)last_enable_time); + break; + } + write_status(); +} + +static void handle_signal_pipe(void) { + unsigned char sigs[32]; + ssize_t read_sz; + while ((read_sz = read(sig_pipe[0], sigs, sizeof(sigs))) > 0) { + for (ssize_t i = 0; i < read_sz; i++) { + handle_signal_event((int)sigs[i]); + } + } + if (read_sz < 0) { + expect(errno == EAGAIN || errno == EINTR); + } +} + +static void signal_handler(int signo) { + int saved_errno = errno; + unsigned char sig = (unsigned char)signo; + ssize_t written = write(sig_pipe[1], &sig, sizeof(sig)); + (void)written; + errno = saved_errno; +} + +static void set_fd_flags(int fd) { + int flags = fcntl(fd, F_GETFL); + expect(flags >= 0); + expect(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0); + + int fd_flags = fcntl(fd, F_GETFD); + expect(fd_flags >= 0); + expect(fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC) == 0); +} + +/** + * Something changed about the watched selection, consider converting it to our + * desired property type. + */ +static void handle_xfixes_selection_notify(XFixesSelectionNotifyEvent *se) { + if (last_disable_time > 0 && se->timestamp >= last_disable_time && + se->timestamp < last_enable_time) { + dbg("Ignoring selection event from disabled period (event time: %lu, disabled: %lu, enabled: %lu)\n", + (unsigned long)se->timestamp, (unsigned long)last_disable_time, + (unsigned long)last_enable_time); + return; + } + + enum selection_type sel = + selection_atom_to_selection_type(se->selection, sels); + if (sel == CM_SEL_INVALID) { + dbg("Received XFixesSelectionNotify for unknown sel\n"); + return; + } + + _drop_(XFree) char *win_title = get_window_title(dpy, se->owner); + if (is_clipserve(win_title) || is_ignored_window(win_title)) { + dbg("Ignoring clip from window titled '%s'\n", win_title); + return; + } + + dbg("Notified about selection update. Selection: %s, Owner: '%s' (0x%lx)\n", + cfg.selections[sel].name, strnull(win_title), (unsigned long)se->owner); + + struct incr_transfer *it = it_list; + while (it) { + struct incr_transfer *next = it->next; + if (it->property == sels[sel].storage) { + it_dbg(it, "Cleaning up stale INCR transfer\n"); + free(it->data); + it_remove(&it_list, it); + free(it); + } + it = next; + } + + XConvertSelection(dpy, se->selection, + XInternAtom(dpy, "UTF8_STRING", False), sels[sel].storage, + win, CurrentTime); + + return; +} + +/** + * Something changed about the watched selection, but we don't explicitly + * listen for SelectionNotify, so in reality this only happens in response to + * an explicit request to tell us that there is no owner. In that case, return + * -ENOENT. + */ +static int handle_selection_notify(const XSelectionEvent *se) { + if (se->property == None) { + enum selection_type sel = + selection_atom_to_selection_type(se->selection, sels); + if (sel == CM_SEL_INVALID) { + dbg("Received no owner notification for unknown sel\n"); + return 0; + } + dbg("X reports that %s has no current owner\n", + cfg.selections[sel].name); + return -ENOENT; + } + return 0; +} + +/** + * Trims the clip store if the number of clips exceeds the configured batch + * size. + */ +static void maybe_trim(void) { + size_t cur_clips; + expect(cs_len(&cs, &cur_clips) == 0); + if (cur_clips > (size_t)cfg.max_clips + (size_t)cfg.max_clips_batch) { + expect(cs_trim(&cs, CS_ITER_NEWEST_FIRST, (size_t)cfg.max_clips) == 0); + } +} + +/** + * Store the clipboard text for a selection. If the text is a possible partial + * of the last clip on the same selection and it was received shortly + * afterwards, replace instead of adding. + */ +static uint64_t store_clip(enum selection_type sel, struct clip_text *ct) { + dbg("Clipboard text is considered salient, storing\n"); + struct timespec current_time = get_monotonic_time(); + uint64_t hash; + + if (cfg.partial_merge_secs > 0 && last_text[sel].data && + within_partial_merge_window(current_time, last_text_time[sel]) && + is_possible_partial(last_text[sel].data, ct->data)) { + dbg("Possible partial of last clip on %s, replacing\n", + cfg.selections[sel].name); + expect(cs_replace(&cs, CS_ITER_NEWEST_FIRST, 0, ct->data, &hash) == 0); + } else { + expect(cs_add(&cs, ct->data, &hash, + cfg.deduplicate ? CS_DUPE_KEEP_LAST : CS_DUPE_KEEP_ALL) == + 0); + } + + free_clip_text(&last_text[sel]); + last_text[sel] = *ct; + last_text_time[sel] = current_time; + + // The caller no longer owns this data. + ct->data = NULL; + ct->source = CLIP_TEXT_SOURCE_INVALID; + + return hash; +} + +/** + * Process the final data collected during an INCR transfer. + */ +static void incr_receive_finish(struct incr_transfer *it) { + enum selection_type sel = + storage_atom_to_selection_type(it->property, sels); + if (sel == CM_SEL_INVALID) { + it_dbg(it, "Received INCR finish for unknown sel\n"); + return; + } + + it_dbg(it, "Finished (bytes buffered: %zu)\n", it->data_size); + char *text = malloc(it->data_size + 1); + expect(text); + memcpy(text, it->data, it->data_size); + text[it->data_size] = '\0'; + + struct clip_text ct = {text, CLIP_TEXT_SOURCE_MALLOC}; + + char line[CS_SNIP_LINE_SIZE]; + first_line(ct.data, line); + it_dbg(it, "First line: %s\n", line); + + if (is_salient_text(ct.data)) { + uint64_t hash = store_clip(sel, &ct); + maybe_trim(); + if (cfg.own_clipboard && has_owned_selections()) { + run_clipserve(hash, cfg.owned_selections); + } + } else { + it_dbg(it, "Clipboard text is whitespace only, ignoring\n"); + free_clip_text(&ct); + } + + free(it->data); + it_remove(&it_list, it); + free(it); +} + +#define INCR_DATA_START_BYTES 1024 * 1024 + +/** + * Acknowledge and start an INCR transfer. + */ +static void incr_receive_start(const XPropertyEvent *pe) { + struct incr_transfer *it = malloc(sizeof(struct incr_transfer)); + expect(it); + *it = (struct incr_transfer){ + .property = pe->atom, + .requestor = pe->window, + .data_size = 0, + .data_capacity = INCR_DATA_START_BYTES, + .data = malloc(INCR_DATA_START_BYTES), + }; + expect(it->data); + + it_dbg(it, "Starting transfer\n"); + it_add(&it_list, it); + + // Signal readiness for chunks + XDeleteProperty(dpy, win, pe->atom); +} + +/** + * Continue receiving data during an INCR transfer. + */ +static void incr_receive_data(const XPropertyEvent *pe, + struct incr_transfer *it) { + if (pe->state != PropertyNewValue) { + return; + } + + it_dbg(it, "Receiving chunk (bytes buffered: %zu)\n", it->data_size); + + _drop_(XFree) unsigned char *chunk = NULL; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + XGetWindowProperty(dpy, win, pe->atom, 0, LONG_MAX, False, AnyPropertyType, + &actual_type, &actual_format, &nitems, &bytes_after, + &chunk); + + size_t chunk_size = nitems * (actual_format / 8); + + if (chunk_size == 0) { + it_dbg(it, "Transfer complete\n"); + incr_receive_finish(it); + return; + } + + if (it->data_size + chunk_size > it->data_capacity) { + it->data_capacity = (it->data_size + chunk_size) * 2; + it->data = realloc(it->data, it->data_capacity); + expect(it->data); + it_dbg(it, "Expanded data buffer to %zu bytes\n", it->data_capacity); + } + + memcpy(it->data + it->data_size, chunk, chunk_size); + it->data_size += chunk_size; + + // Signal readiness for next chunk + XDeleteProperty(dpy, win, pe->atom); +} + +/** + * Something changed in our clip storage atoms. Work out whether we want to + * store the new content as a clipboard entry. + */ +static int handle_property_notify(const XPropertyEvent *pe) { + if (pe->state != PropertyNewValue && pe->state != PropertyDelete) { + return -EINVAL; + } + + enum selection_type sel = storage_atom_to_selection_type(pe->atom, sels); + if (sel == CM_SEL_INVALID) { + dbg("Received PropertyNotify for unknown sel\n"); + return -EINVAL; + } + + // Check if this property corresponds to an INCR transfer in progress + struct incr_transfer *it = it_list; + while (it) { + if (it->property == pe->atom && it->requestor == pe->window) { + break; + } + it = it->next; + } + + if (it) { + incr_receive_data(pe, it); + return 0; + } + + // Not an INCR transfer in progress. Check if this is an INCR transfer + // starting + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + _drop_(XFree) unsigned char *prop = NULL; + + XGetWindowProperty(dpy, win, pe->atom, 0, 0, False, AnyPropertyType, + &actual_type, &actual_format, &nitems, &bytes_after, + &prop); + + if (actual_type == incr_atom) { + incr_receive_start(pe); + } else { + dbg("Received non-INCR PropertyNotify\n"); + + // store_clip will take care of freeing this later when it's gone from + // last_text. + struct clip_text ct = get_clipboard_text(pe->atom); + if (!ct.data) { + dbg("Failed to get clipboard text\n"); + return -EINVAL; + } + char line[CS_SNIP_LINE_SIZE]; + first_line(ct.data, line); + dbg("First line: %s\n", line); + + if (is_salient_text(ct.data)) { + uint64_t hash = store_clip(sel, &ct); + maybe_trim(); + /* We only own CLIPBOARD because otherwise the behaviour is wonky: + * + * 1. When you select in a browser and press ^V, it repastes what + * you have selected instead of the previous content + * 2. urxvt and some other terminal emulators will unhilight on + * PRIMARY ownership being taken away from them + */ + if (cfg.own_clipboard && has_owned_selections()) { + run_clipserve(hash, cfg.owned_selections); + } + } else { + dbg("Clipboard text is whitespace only, ignoring\n"); + free_clip_text(&ct); + } + } + + return 0; +} + +/** + * Process X11 events, returning when we have either processed one clip, or + * have received an indication that the selection is not owned. + * + * The usual sequence is: + * + * 1. Get an XFixesSelectionNotify that we have a new selection. + * 2. Call XConvertSelection() on it to get a string in our prop. + * 3. Wait for a PropertyNotify that says that's ready. + * 4. When it's ready, store it, and return from the function. + * + * Another possible outcome, especially when trying to get the initial state at + * startup, is that we get a SelectionNotify even with owner == None, which + * means the selection is unowned. At that point we also return, since it's + * clear that an explicit request has been nacked. + */ +static int handle_x11_event(int evt_base) { + while (XPending(dpy)) { + XEvent evt; + XNextEvent(dpy, &evt); + + if (!enabled) { + dbg("Got X event, but ignoring as collection is disabled\n"); + continue; + } + + int ret; + if (evt.type == evt_base + XFixesSelectionNotify) { + handle_xfixes_selection_notify((XFixesSelectionNotifyEvent *)&evt); + } else if (evt.type == PropertyNotify) { + ret = handle_property_notify((XPropertyEvent *)&evt); + if (ret == 0) { + return ret; + } + } else if (evt.type == SelectionNotify) { + ret = handle_selection_notify((XSelectionEvent *)&evt); + if (ret < 0) { + return ret; + } + } + } + + return -EINPROGRESS; +} + +/** + * Continuously wait for and process X11 or signal events until we fully + * process success or failure for a clip. + * + * The usual sequence is: + * + * 1. Get an XFixesSelectionNotify that we have a new selection. + * 2. Call XConvertSelection() on it to get a string in our prop. + * 3. Wait for a PropertyNotify that says that's ready. + * 4. When it's ready, store it, and return from the function. + * + * Another possible outcome, especially when trying to get the initial state at + * startup, is that we get a SelectionNotify even with owner == None, which + * means the selection is unowned. At that point we also return, since it's + * clear that an explicit request has been nacked. + */ +static int get_one_clip(int evt_base) { + while (1) { + // It's possible that we have more X events to process, but because of + // the way the protocol works, we won't get told about them until we + // next get an event if we wait for select(). Check for them first. + if (XPending(dpy)) { + return handle_x11_event(evt_base); + } + + fd_set fds; + int x_fd = ConnectionNumber(dpy); + + FD_ZERO(&fds); + FD_SET(sig_pipe[0], &fds); + FD_SET(x_fd, &fds); + + int max_fd = sig_pipe[0] > x_fd ? sig_pipe[0] : x_fd; + int ret = select(max_fd + 1, &fds, NULL, NULL, NULL); + if (ret < 0) { + if (errno == EINTR) { + return 0; + } + expect(ret > 0); + } + + if (FD_ISSET(sig_pipe[0], &fds)) { + handle_signal_pipe(); + } + + if (FD_ISSET(x_fd, &fds)) { + return handle_x11_event(evt_base); + } + } +} + +static int setup_watches(int evt_base) { + XSelectInput(dpy, win, PropertyChangeMask); + + for (size_t i = 0; i < CM_SEL_MAX; i++) { + struct selection sel = cfg.selections[i]; + if (!sel.active) { + continue; + } + Atom sel_atom = sels[i].selection; + XFixesSelectSelectionInput(dpy, win, sel_atom, + XFixesSetSelectionOwnerNotifyMask); + dbg("Getting initial value for selection %s\n", sel.name); + XConvertSelection(dpy, sel_atom, XInternAtom(dpy, "UTF8_STRING", False), + sels[i].storage, win, CurrentTime); + get_one_clip(evt_base); + } + + return 0; +} + +static int _noreturn_ run(int evt_base) { + while (1) { + get_one_clip(evt_base); + } +} + +#ifndef UNIT_TEST +int main(int argc, char *argv[]) { + int evt_base; + + cfg = setup("clipmenud"); + exec_man_on_help(argc, argv); + die_on(argc != 1, "clipmenud doesn't accept any arguments\n"); + + _drop_(close) int session_fd = + open(get_session_lock_path(&cfg), O_RDWR | O_CREAT | O_CLOEXEC, 0600); + die_on(session_fd < 0, "Failed to open session file: %s\n", + strerror(errno)); + die_on(flock(session_fd, LOCK_EX | LOCK_NB) < 0, + "Failed to lock session file -- is another clipmenud running?\n"); + + expect(ftruncate(session_fd, 0) == 0); + expect(lseek(session_fd, 0, SEEK_SET) == 0); + dprintf(session_fd, "%ld\n", (long)getpid()); + + write_status(); + + _drop_(close) int content_dir_fd = open(get_cache_dir(&cfg), O_RDONLY); + _drop_(close) int snip_fd = + open(get_line_cache_path(&cfg), O_RDWR | O_CREAT, 0600); + expect(content_dir_fd >= 0 && snip_fd >= 0); + + expect(cs_init(&cs, snip_fd, content_dir_fd) == 0); + + die_on(!(dpy = XOpenDisplay(NULL)), "Cannot open display\n"); + win = DefaultRootWindow(dpy); + setup_selections(dpy, sels); + + incr_atom = XInternAtom(dpy, "INCR", False); + timestamp_atom = XInternAtom(dpy, "CLIPMENUD_TIMESTAMP", False); + + expect(pipe(sig_pipe) == 0); + set_fd_flags(sig_pipe[0]); + set_fd_flags(sig_pipe[1]); + + struct sigaction sa = {0}; + sa.sa_handler = signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + expect(sigaction(SIGUSR1, &sa, NULL) == 0); + expect(sigaction(SIGUSR2, &sa, NULL) == 0); + expect(signal(SIGCHLD, SIG_IGN) != SIG_ERR); + + int unused; + die_on(!XFixesQueryExtension(dpy, &evt_base, &unused), "XFixes missing\n"); + + setup_watches(evt_base); + + if (!cfg.oneshot) { + run(evt_base); + } + + expect(cs_destroy(&cs) == 0); + config_free(&cfg); + XCloseDisplay(dpy); + return 0; +} +#endif diff --git a/src/clipserve.c b/src/clipserve.c new file mode 100644 index 0000000..9d490cd --- /dev/null +++ b/src/clipserve.c @@ -0,0 +1,286 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#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]... \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]... \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; +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..10ba62c --- /dev/null +++ b/src/config.c @@ -0,0 +1,500 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#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(); + } + } +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..949ebd8 --- /dev/null +++ b/src/config.h @@ -0,0 +1,104 @@ +#ifndef CM_CONFIG_H +#define CM_CONFIG_H + +#include +#include +#include +#include +#include + +#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 diff --git a/src/menu_util.c b/src/menu_util.c new file mode 100644 index 0000000..3358c43 --- /dev/null +++ b/src/menu_util.c @@ -0,0 +1,302 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/src/menu_util.h b/src/menu_util.h new file mode 100644 index 0000000..3bb15de --- /dev/null +++ b/src/menu_util.h @@ -0,0 +1,18 @@ +#ifndef CM_MENU_UTIL_H +#define CM_MENU_UTIL_H + +#include + +#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 diff --git a/src/store.c b/src/store.c new file mode 100644 index 0000000..fd2d80e --- /dev/null +++ b/src/store.c @@ -0,0 +1,847 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/src/store.h b/src/store.h new file mode 100644 index 0000000..3a0f3bd --- /dev/null +++ b/src/store.h @@ -0,0 +1,192 @@ +#ifndef CM_STORE_H +#define CM_STORE_H + +#include +#include +#include + +#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 diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..e9cf994 --- /dev/null +++ b/src/util.c @@ -0,0 +1,165 @@ +#include +#include +#include +#include + +#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; +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..3aa1592 --- /dev/null +++ b/src/util.h @@ -0,0 +1,111 @@ +#ifndef CM_UTIL_H +#define CM_UTIL_H + +#include +#include +#include +#include +#include +#include + +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 diff --git a/src/x.c b/src/x.c new file mode 100644 index 0000000..18809f3 --- /dev/null +++ b/src/x.c @@ -0,0 +1,121 @@ +#include +#include +#include + +#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; + } +} diff --git a/src/x.h b/src/x.h new file mode 100644 index 0000000..a56f6d1 --- /dev/null +++ b/src/x.h @@ -0,0 +1,34 @@ +#ifndef CM_X_H +#define CM_X_H + +#include + +#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 diff --git a/tests/test_store.c b/tests/test_store.c new file mode 100644 index 0000000..a1d598e --- /dev/null +++ b/tests/test_store.c @@ -0,0 +1,848 @@ +#undef NDEBUG + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/tests/x_integration_tests b/tests/x_integration_tests new file mode 100755 index 0000000..f24de8b --- /dev/null +++ b/tests/x_integration_tests @@ -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 From 112255cfb09cc360a896392fbf6dc796b205c6e5 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 2/7] Squashed 'suckless/scroll/' content from commit 51ee387 git-subtree-dir: suckless/scroll git-subtree-split: 51ee387dd3860b13baaca5ef23392f7282e9bbdc --- .gitignore | 5 + LICENSE | 16 ++ Makefile | 45 ++++ README | 34 +++ TODO | 3 + config.def.h | 16 ++ config.mk | 12 ++ perf.sh | 29 +++ ptty.c | 156 ++++++++++++++ scroll.1 | 68 ++++++ scroll.c | 594 +++++++++++++++++++++++++++++++++++++++++++++++++++ up.log | 74 +++++++ up.sh | 15 ++ 13 files changed, 1067 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 config.def.h create mode 100644 config.mk create mode 100755 perf.sh create mode 100644 ptty.c create mode 100644 scroll.1 create mode 100644 scroll.c create mode 100644 up.log create mode 100755 up.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93b7ec2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +scroll +ptty +*.swp +*.core +config.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2db5bfe --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +ISC License (ISC) + +Copyright (c) 2020 Jan Klemkow +Copyright (c) 2020 Jochen Sprickerhof + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cacccd2 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README b/README new file mode 100644 index 0000000..a1837c2 --- /dev/null +++ b/README @@ -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 diff --git a/TODO b/TODO new file mode 100644 index 0000000..84ffd33 --- /dev/null +++ b/TODO @@ -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) diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..536db70 --- /dev/null +++ b/config.def.h @@ -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 */ +}; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..5676b85 --- /dev/null +++ b/config.mk @@ -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 diff --git a/perf.sh b/perf.sh new file mode 100755 index 0000000..ab1fb21 --- /dev/null +++ b/perf.sh @@ -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 diff --git a/ptty.c b/ptty.c new file mode 100644 index 0000000..bbbb99f --- /dev/null +++ b/ptty.c @@ -0,0 +1,156 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux) + #include +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) + #include +#elif defined(__FreeBSD__) || defined(__DragonFly__) + #include +#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); +} diff --git a/scroll.1 b/scroll.1 new file mode 100644 index 0000000..b5524ab --- /dev/null +++ b/scroll.1 @@ -0,0 +1,68 @@ +.\" +.\" Copyright (c) 2020 Jan Klemkow +.\" +.\" 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 . diff --git a/scroll.c b/scroll.c new file mode 100644 index 0000000..8f66d54 --- /dev/null +++ b/scroll.c @@ -0,0 +1,594 @@ +/* + * Based on an example code from Roberto E. Vargas Caballero. + * + * See LICENSE file for copyright and license details. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux) + #include +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) + #include +#elif defined(__FreeBSD__) || defined(__DragonFly__) + #include +#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); +} diff --git a/up.log b/up.log new file mode 100644 index 0000000..5f40226 --- /dev/null +++ b/up.log @@ -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 \ No newline at end of file diff --git a/up.sh b/up.sh new file mode 100755 index 0000000..fa28a80 --- /dev/null +++ b/up.sh @@ -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 From 9586e3bdb0fa1168f2c467a200d1420f8c47396e Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 3/7] Squashed 'suckless/slock/' content from commit 65cb721 git-subtree-dir: suckless/slock git-subtree-split: 65cb721c9eb7b5d80267378790dee93e9db0223c --- LICENSE | 25 ++++ Makefile | 46 ++++++ README | 24 +++ config.def.h | 12 ++ config.mk | 29 ++++ explicit_bzero.c | 19 +++ slock.1 | 36 +++++ slock.c | 382 +++++++++++++++++++++++++++++++++++++++++++++++ util.h | 2 + 9 files changed, 575 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 explicit_bzero.c create mode 100644 slock.1 create mode 100644 slock.c create mode 100644 util.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8b44e16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +MIT/X Consortium License + +© 2016-2026 Hiltjo Posthuma +© 2015-2016 Markus Teich +© 2014 Dimitris Papastamos +© 2006-2014 Anselm R Garbe +© 2014-2016 Laslo Hunhold + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c41e57e --- /dev/null +++ b/Makefile @@ -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" ${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 diff --git a/README b/README new file mode 100644 index 0000000..dcacd01 --- /dev/null +++ b/README @@ -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. diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..9855e21 --- /dev/null +++ b/config.def.h @@ -0,0 +1,12 @@ +/* user and group to drop privileges to */ +static const char *user = "nobody"; +static const char *group = "nogroup"; + +static const char *colorname[NUMCOLS] = { + [INIT] = "black", /* after initialization */ + [INPUT] = "#005577", /* during input */ + [FAILED] = "#CC3333", /* wrong password */ +}; + +/* treat a cleared input like a wrong password (color) */ +static const int failonclear = 1; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..9fe5bff --- /dev/null +++ b/config.mk @@ -0,0 +1,29 @@ +# slock version +VERSION = 1.7 + +# Customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +# includes and libs +INCS = -I. -I/usr/include -I${X11INC} +LIBS = -L/usr/lib -lc -lcrypt -L${X11LIB} -lX11 -lXext -lXrandr + +# flags +CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE -DHAVE_SHADOW_H +CFLAGS = -std=c99 -pedantic -Wall -Os ${INCS} ${CPPFLAGS} +LDFLAGS = -s ${LIBS} +COMPATSRC = explicit_bzero.c + +# On OpenBSD and Darwin remove -lcrypt from LIBS +#LIBS = -L/usr/lib -lc -L${X11LIB} -lX11 -lXext -lXrandr +# On *BSD remove -DHAVE_SHADOW_H from CPPFLAGS +# On NetBSD add -D_NETBSD_SOURCE to CPPFLAGS +#CPPFLAGS = -DVERSION=\"${VERSION}\" -D_BSD_SOURCE -D_NETBSD_SOURCE +# On OpenBSD set COMPATSRC to empty +#COMPATSRC = diff --git a/explicit_bzero.c b/explicit_bzero.c new file mode 100644 index 0000000..3e33ca8 --- /dev/null +++ b/explicit_bzero.c @@ -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 + +__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); +} diff --git a/slock.1 b/slock.1 new file mode 100644 index 0000000..e485992 --- /dev/null +++ b/slock.1 @@ -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. diff --git a/slock.c b/slock.c new file mode 100644 index 0000000..9c587e1 --- /dev/null +++ b/slock.c @@ -0,0 +1,382 @@ +/* See LICENSE file for license details. */ +#define _XOPEN_SOURCE 500 +#if HAVE_SHADOW_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" + +char *argv0; + +enum { + INIT, + INPUT, + FAILED, + NUMCOLS +}; + +struct lock { + int screen; + Window root, win; + Pixmap pmap; + unsigned long colors[NUMCOLS]; +}; + +struct xrandr { + int active; + int evbase; + int errbase; +}; + +#include "config.h" + +static void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(1); +} + +#ifdef __linux__ +#include +#include + +static void +dontkillme(void) +{ + FILE *f; + const char oomfile[] = "/proc/self/oom_score_adj"; + + if (!(f = fopen(oomfile, "w"))) { + if (errno == ENOENT) + return; + die("slock: fopen %s: %s\n", oomfile, strerror(errno)); + } + fprintf(f, "%d", OOM_SCORE_ADJ_MIN); + if (fclose(f)) { + if (errno == EACCES) + die("slock: unable to disable OOM killer. " + "Make sure to suid or sgid slock.\n"); + else + die("slock: fclose %s: %s\n", oomfile, strerror(errno)); + } +} +#endif + +static const char * +gethash(void) +{ + const char *hash; + struct passwd *pw; + + /* Check if the current user has a password entry */ + errno = 0; + if (!(pw = getpwuid(getuid()))) { + if (errno) + die("slock: getpwuid: %s\n", strerror(errno)); + else + die("slock: cannot retrieve password entry\n"); + } + hash = pw->pw_passwd; + +#if HAVE_SHADOW_H + if (!strcmp(hash, "x")) { + struct spwd *sp; + if (!(sp = getspnam(pw->pw_name))) + die("slock: getspnam: cannot retrieve shadow entry. " + "Make sure to suid or sgid slock.\n"); + hash = sp->sp_pwdp; + } +#else + if (!strcmp(hash, "*")) { +#ifdef __OpenBSD__ + if (!(pw = getpwuid_shadow(getuid()))) + die("slock: getpwnam_shadow: cannot retrieve shadow entry. " + "Make sure to suid or sgid slock.\n"); + hash = pw->pw_passwd; +#else + die("slock: getpwuid: cannot retrieve shadow entry. " + "Make sure to suid or sgid slock.\n"); +#endif /* __OpenBSD__ */ + } +#endif /* HAVE_SHADOW_H */ + + return hash; +} + +static void +readpw(Display *dpy, struct xrandr *rr, struct lock **locks, int nscreens, + const char *hash) +{ + XRRScreenChangeNotifyEvent *rre; + char buf[32], passwd[256], *inputhash; + int num, screen, running, failure, oldc; + unsigned int len, color; + KeySym ksym; + XEvent ev; + + len = 0; + running = 1; + failure = 0; + oldc = INIT; + + while (running && !XNextEvent(dpy, &ev)) { + if (ev.type == KeyPress) { + explicit_bzero(&buf, sizeof(buf)); + num = XLookupString(&ev.xkey, buf, sizeof(buf), &ksym, 0); + if (IsKeypadKey(ksym)) { + if (ksym == XK_KP_Enter) + ksym = XK_Return; + else if (ksym >= XK_KP_0 && ksym <= XK_KP_9) + ksym = (ksym - XK_KP_0) + XK_0; + } + if (IsFunctionKey(ksym) || + IsKeypadKey(ksym) || + IsMiscFunctionKey(ksym) || + IsPFKey(ksym) || + IsPrivateKeypadKey(ksym)) + continue; + switch (ksym) { + case XK_Return: + passwd[len] = '\0'; + errno = 0; + if (!(inputhash = crypt(passwd, hash))) + fprintf(stderr, "slock: crypt: %s\n", strerror(errno)); + else + running = !!strcmp(inputhash, hash); + if (running) { + XBell(dpy, 100); + failure = 1; + } + explicit_bzero(&passwd, sizeof(passwd)); + len = 0; + break; + case XK_Escape: + explicit_bzero(&passwd, sizeof(passwd)); + len = 0; + break; + case XK_BackSpace: + if (len) + passwd[--len] = '\0'; + break; + default: + if (num && !iscntrl((unsigned char)buf[0]) && + (len + num < sizeof(passwd))) { + memcpy(passwd + len, buf, num); + len += num; + } else if (buf[0] == '\025') { /* ctrl-u clears input */ + explicit_bzero(&passwd, sizeof(passwd)); + len = 0; + } + break; + } + color = len ? INPUT : ((failure || failonclear) ? FAILED : INIT); + if (running && oldc != color) { + for (screen = 0; screen < nscreens; screen++) { + XSetWindowBackground(dpy, + locks[screen]->win, + locks[screen]->colors[color]); + XClearWindow(dpy, locks[screen]->win); + } + oldc = color; + } + } else if (rr->active && ev.type == rr->evbase + RRScreenChangeNotify) { + rre = (XRRScreenChangeNotifyEvent*)&ev; + for (screen = 0; screen < nscreens; screen++) { + if (locks[screen]->win == rre->window) { + if (rre->rotation == RR_Rotate_90 || + rre->rotation == RR_Rotate_270) + XResizeWindow(dpy, locks[screen]->win, + rre->height, rre->width); + else + XResizeWindow(dpy, locks[screen]->win, + rre->width, rre->height); + XClearWindow(dpy, locks[screen]->win); + break; + } + } + } else { + for (screen = 0; screen < nscreens; screen++) + XRaiseWindow(dpy, locks[screen]->win); + } + } +} + +static struct lock * +lockscreen(Display *dpy, struct xrandr *rr, int screen) +{ + char curs[] = {0, 0, 0, 0, 0, 0, 0, 0}; + int i, ptgrab, kbgrab; + struct lock *lock; + XColor color, dummy; + XSetWindowAttributes wa; + Cursor invisible; + + if (dpy == NULL || screen < 0 || !(lock = malloc(sizeof(struct lock)))) + return NULL; + + lock->screen = screen; + lock->root = RootWindow(dpy, lock->screen); + + for (i = 0; i < NUMCOLS; i++) { + XAllocNamedColor(dpy, DefaultColormap(dpy, lock->screen), + colorname[i], &color, &dummy); + lock->colors[i] = color.pixel; + } + + /* init */ + wa.override_redirect = 1; + wa.background_pixel = lock->colors[INIT]; + lock->win = XCreateWindow(dpy, lock->root, 0, 0, + DisplayWidth(dpy, lock->screen), + DisplayHeight(dpy, lock->screen), + 0, DefaultDepth(dpy, lock->screen), + CopyFromParent, + DefaultVisual(dpy, lock->screen), + CWOverrideRedirect | CWBackPixel, &wa); + lock->pmap = XCreateBitmapFromData(dpy, lock->win, curs, 8, 8); + invisible = XCreatePixmapCursor(dpy, lock->pmap, lock->pmap, + &color, &color, 0, 0); + XDefineCursor(dpy, lock->win, invisible); + + /* Try to grab mouse pointer *and* keyboard for 600ms, else fail the lock */ + for (i = 0, ptgrab = kbgrab = -1; i < 6; i++) { + if (ptgrab != GrabSuccess) { + ptgrab = XGrabPointer(dpy, lock->root, False, + ButtonPressMask | ButtonReleaseMask | + PointerMotionMask, GrabModeAsync, + GrabModeAsync, None, invisible, CurrentTime); + } + if (kbgrab != GrabSuccess) { + kbgrab = XGrabKeyboard(dpy, lock->root, True, + GrabModeAsync, GrabModeAsync, CurrentTime); + } + + /* input is grabbed: we can lock the screen */ + if (ptgrab == GrabSuccess && kbgrab == GrabSuccess) { + XMapRaised(dpy, lock->win); + if (rr->active) + XRRSelectInput(dpy, lock->win, RRScreenChangeNotifyMask); + + XSelectInput(dpy, lock->root, SubstructureNotifyMask); + return lock; + } + + /* retry on AlreadyGrabbed but fail on other errors */ + if ((ptgrab != AlreadyGrabbed && ptgrab != GrabSuccess) || + (kbgrab != AlreadyGrabbed && kbgrab != GrabSuccess)) + break; + + usleep(100000); + } + + /* we couldn't grab all input: fail out */ + if (ptgrab != GrabSuccess) + fprintf(stderr, "slock: unable to grab mouse pointer for screen %d\n", + screen); + if (kbgrab != GrabSuccess) + fprintf(stderr, "slock: unable to grab keyboard for screen %d\n", + screen); + return NULL; +} + +static void +usage(void) +{ + die("usage: slock [-v]\n"); +} + +int +main(int argc, char **argv) { + struct xrandr rr; + struct lock **locks; + struct passwd *pwd; + struct group *grp; + uid_t duid; + gid_t dgid; + const char *hash; + Display *dpy; + int s, nlocks, nscreens; + + if (argc > 1 && !strcmp(argv[1], "-v")) { + puts("slock-"VERSION); + return 0; + } else if (argc > 1) { + usage(); + } + + /* validate drop-user and -group */ + errno = 0; + if (!(pwd = getpwnam(user))) + die("slock: getpwnam %s: %s\n", user, + errno ? strerror(errno) : "user entry not found"); + duid = pwd->pw_uid; + errno = 0; + if (!(grp = getgrnam(group))) + die("slock: getgrnam %s: %s\n", group, + errno ? strerror(errno) : "group entry not found"); + dgid = grp->gr_gid; + +#ifdef __linux__ + dontkillme(); +#endif + + hash = gethash(); + errno = 0; + if (!crypt("", hash)) + die("slock: crypt: %s\n", strerror(errno)); + + if (!(dpy = XOpenDisplay(NULL))) + die("slock: cannot open display\n"); + + /* drop privileges */ + if (setgroups(0, NULL) < 0) + die("slock: setgroups: %s\n", strerror(errno)); + if (setgid(dgid) < 0) + die("slock: setgid: %s\n", strerror(errno)); + if (setuid(duid) < 0) + die("slock: setuid: %s\n", strerror(errno)); + + /* check for Xrandr support */ + rr.active = XRRQueryExtension(dpy, &rr.evbase, &rr.errbase); + + /* get number of screens in display "dpy" and blank them */ + nscreens = ScreenCount(dpy); + if (!(locks = calloc(nscreens, sizeof(struct lock *)))) + die("slock: out of memory\n"); + for (nlocks = 0, s = 0; s < nscreens; s++) { + if ((locks[s] = lockscreen(dpy, &rr, s)) != NULL) + nlocks++; + else + break; + } + XSync(dpy, 0); + + /* did we manage to lock everything? */ + if (nlocks != nscreens) + return 1; + + /* everything is now blank. Wait for the correct password */ + readpw(dpy, &rr, locks, nscreens, hash); + + return 0; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..6f748b8 --- /dev/null +++ b/util.h @@ -0,0 +1,2 @@ +#undef explicit_bzero +void explicit_bzero(void *, size_t); From 9ae5fa514268cbd673d5ff8740c10c852f28f160 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 4/7] Squashed 'suckless/slstatus/' content from commit 675c839 git-subtree-dir: suckless/slstatus git-subtree-split: 675c83912ea1aca9834448aaa87a040bc9990c52 --- LICENSE | 46 ++++ Makefile | 69 +++++ README | 65 +++++ arg.h | 33 +++ components/battery.c | 247 ++++++++++++++++++ components/cat.c | 32 +++ components/cpu.c | 157 ++++++++++++ components/datetime.c | 20 ++ components/disk.c | 59 +++++ components/entropy.c | 29 +++ components/hostname.c | 17 ++ components/ip.c | 89 +++++++ components/kernel_release.c | 19 ++ components/keyboard_indicators.c | 50 ++++ components/keymap.c | 86 +++++++ components/load_avg.c | 19 ++ components/netspeeds.c | 129 ++++++++++ components/num_files.c | 32 +++ components/ram.c | 227 +++++++++++++++++ components/run_command.c | 31 +++ components/swap.c | 276 ++++++++++++++++++++ components/temperature.c | 73 ++++++ components/uptime.c | 34 +++ components/user.c | 33 +++ components/volume.c | 224 +++++++++++++++++ components/wifi.c | 417 +++++++++++++++++++++++++++++++ config.def.h | 70 ++++++ config.mk | 22 ++ slstatus.1 | 47 ++++ slstatus.c | 135 ++++++++++ slstatus.h | 85 +++++++ util.c | 158 ++++++++++++ util.h | 18 ++ 33 files changed, 3048 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 arg.h create mode 100644 components/battery.c create mode 100644 components/cat.c create mode 100644 components/cpu.c create mode 100644 components/datetime.c create mode 100644 components/disk.c create mode 100644 components/entropy.c create mode 100644 components/hostname.c create mode 100644 components/ip.c create mode 100644 components/kernel_release.c create mode 100644 components/keyboard_indicators.c create mode 100644 components/keymap.c create mode 100644 components/load_avg.c create mode 100644 components/netspeeds.c create mode 100644 components/num_files.c create mode 100644 components/ram.c create mode 100644 components/run_command.c create mode 100644 components/swap.c create mode 100644 components/temperature.c create mode 100644 components/uptime.c create mode 100644 components/user.c create mode 100644 components/volume.c create mode 100644 components/wifi.c create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 slstatus.1 create mode 100644 slstatus.c create mode 100644 slstatus.h create mode 100644 util.c create mode 100644 util.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..859df8b --- /dev/null +++ b/LICENSE @@ -0,0 +1,46 @@ +ISC License + +Copyright 2016-2026 Aaron Marcher + +Copyright 2016 Roy Freytag +Copyright 2016 Vincent Loupmon +Copyright 2016 Daniel Walter +Copyright 2016-2018 Ali H. Fardan +Copyright 2016 Jody Leonard +Copyright 2016-2018 Quentin Rameau +Copyright 2016 Mike Coddington +Copyright 2016-2018 Ivan J. +Copyright 2017 Tobias Stoeckmann +Copyright 2017-2018 Laslo Hunhold +Copyright 2018 Darron Anderson +Copyright 2018 Josuah Demangeon +Copyright 2018 Tobias Tschinkowitz +Copyright 2018 David Demelier +Copyright 2018-2012 Michael Buch +Copyright 2018 Ian Remmler +Copyright 2016-2019 Joerg Jung +Copyright 2019 Ryan Kes +Copyright 2019 Cem Keylan +Copyright 2019 Spiros Thanasoulas +Copyright 2019-2022 Ingo Feinerer +Copyright 2020 Alexandre Ratchov +Copyright 2020 Mart Lubbers +Copyright 2020 Daniel Moch +Copyright 2022 Nickolas Raymond Kaczynski +Copyright 2022 Patrick Iacob +Copyright 2021-2022 Steven Ward +Copyright 2025 Joakim Sindholt +Copyright 2025 Al +Copyright 2025 sewn + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7a18274 --- /dev/null +++ b/Makefile @@ -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" diff --git a/README b/README new file mode 100644 index 0000000..4d592bb --- /dev/null +++ b/README @@ -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. diff --git a/arg.h b/arg.h new file mode 100644 index 0000000..5f1f408 --- /dev/null +++ b/arg.h @@ -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 diff --git a/components/battery.c b/components/battery.c new file mode 100644 index 0000000..f2dc393 --- /dev/null +++ b/components/battery.c @@ -0,0 +1,247 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__linux__) +/* + * https://www.kernel.org/doc/html/latest/power/power_supply_class.html + */ + #include + #include + #include + + #define POWER_SUPPLY_CAPACITY "/sys/class/power_supply/%s/capacity" + #define POWER_SUPPLY_STATUS "/sys/class/power_supply/%s/status" + #define POWER_SUPPLY_CHARGE "/sys/class/power_supply/%s/charge_now" + #define POWER_SUPPLY_ENERGY "/sys/class/power_supply/%s/energy_now" + #define POWER_SUPPLY_CURRENT "/sys/class/power_supply/%s/current_now" + #define POWER_SUPPLY_POWER "/sys/class/power_supply/%s/power_now" + + static const char * + pick(const char *bat, const char *f1, const char *f2, char *path, + size_t length) + { + if (esnprintf(path, length, f1, bat) > 0 && + access(path, R_OK) == 0) + return f1; + + if (esnprintf(path, length, f2, bat) > 0 && + access(path, R_OK) == 0) + return f2; + + return NULL; + } + + const char * + battery_perc(const char *bat) + { + int cap_perc; + char path[PATH_MAX]; + + if (esnprintf(path, sizeof(path), POWER_SUPPLY_CAPACITY, bat) < 0) + return NULL; + if (pscanf(path, "%d", &cap_perc) != 1) + return NULL; + + return bprintf("%d", cap_perc); + } + + const char * + battery_state(const char *bat) + { + static struct { + char *state; + char *symbol; + } map[] = { + { "Charging", "+" }, + { "Discharging", "-" }, + { "Full", "o" }, + { "Not charging", "o" }, + }; + size_t i; + char path[PATH_MAX], state[13]; + + if (esnprintf(path, sizeof(path), POWER_SUPPLY_STATUS, bat) < 0) + return NULL; + if (pscanf(path, "%12[a-zA-Z ]", state) != 1) + return NULL; + + for (i = 0; i < LEN(map); i++) + if (!strcmp(map[i].state, state)) + break; + + return (i == LEN(map)) ? "?" : map[i].symbol; + } + + const char * + battery_remaining(const char *bat) + { + uintmax_t charge_now, current_now, m, h; + double timeleft; + char path[PATH_MAX], state[13]; + + if (esnprintf(path, sizeof(path), POWER_SUPPLY_STATUS, bat) < 0) + return NULL; + if (pscanf(path, "%12[a-zA-Z ]", state) != 1) + return NULL; + + if (!pick(bat, POWER_SUPPLY_CHARGE, POWER_SUPPLY_ENERGY, path, + sizeof(path)) || + pscanf(path, "%ju", &charge_now) < 0) + return NULL; + + if (!strcmp(state, "Discharging")) { + if (!pick(bat, POWER_SUPPLY_CURRENT, POWER_SUPPLY_POWER, path, + sizeof(path)) || + pscanf(path, "%ju", ¤t_now) < 0) + return NULL; + + if (current_now == 0) + return NULL; + + timeleft = (double)charge_now / (double)current_now; + h = timeleft; + m = (timeleft - (double)h) * 60; + + return bprintf("%juh %jum", h, m); + } + + return ""; + } +#elif defined(__OpenBSD__) + #include + #include + #include + #include + + 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 + + #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 diff --git a/components/cat.c b/components/cat.c new file mode 100644 index 0000000..07944cc --- /dev/null +++ b/components/cat.c @@ -0,0 +1,32 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#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; +} + diff --git a/components/cpu.c b/components/cpu.c new file mode 100644 index 0000000..d0d03c7 --- /dev/null +++ b/components/cpu.c @@ -0,0 +1,157 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include + +#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 + #include + #include + + 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 + #include + #include + + 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 diff --git a/components/datetime.c b/components/datetime.c new file mode 100644 index 0000000..5b10daf --- /dev/null +++ b/components/datetime.c @@ -0,0 +1,20 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#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; +} diff --git a/components/disk.c b/components/disk.c new file mode 100644 index 0000000..e19a693 --- /dev/null +++ b/components/disk.c @@ -0,0 +1,59 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +disk_free(const char *path) +{ + struct statvfs fs; + + if (statvfs(path, &fs) < 0) { + warn("statvfs '%s':", path); + return NULL; + } + + return fmt_human(fs.f_frsize * fs.f_bavail, 1024); +} + +const char * +disk_perc(const char *path) +{ + struct statvfs fs; + + if (statvfs(path, &fs) < 0) { + warn("statvfs '%s':", path); + return NULL; + } + + return bprintf("%d", (int)(100 * + (1 - ((double)fs.f_bavail / (double)fs.f_blocks)))); +} + +const char * +disk_total(const char *path) +{ + struct statvfs fs; + + if (statvfs(path, &fs) < 0) { + warn("statvfs '%s':", path); + return NULL; + } + + return fmt_human(fs.f_frsize * fs.f_blocks, 1024); +} + +const char * +disk_used(const char *path) +{ + struct statvfs fs; + + if (statvfs(path, &fs) < 0) { + warn("statvfs '%s':", path); + return NULL; + } + + return fmt_human(fs.f_frsize * (fs.f_blocks - fs.f_bfree), 1024); +} diff --git a/components/entropy.c b/components/entropy.c new file mode 100644 index 0000000..65010b0 --- /dev/null +++ b/components/entropy.c @@ -0,0 +1,29 @@ +/* See LICENSE file for copyright and license details. */ +#include "../slstatus.h" +#if defined(__linux__) + #include + #include + + #include "../util.h" + + #define ENTROPY_AVAIL "/proc/sys/kernel/random/entropy_avail" + + const char * + entropy(const char *unused) + { + uintmax_t num; + + if (pscanf(ENTROPY_AVAIL, "%ju", &num) != 1) + return NULL; + + return bprintf("%ju", num); + } +#elif defined(__OpenBSD__) | defined(__FreeBSD__) + const char * + entropy(const char *unused) + { + // https://www.unicode.org/charts/PDF/U2200.pdf + /* Unicode Character 'INFINITY' (U+221E) */ + return "\u221E"; + } +#endif diff --git a/components/hostname.c b/components/hostname.c new file mode 100644 index 0000000..dab8b63 --- /dev/null +++ b/components/hostname.c @@ -0,0 +1,17 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +hostname(const char *unused) +{ + if (gethostname(buf, sizeof(buf)) < 0) { + warn("gethostbyname:"); + return NULL; + } + + return buf; +} diff --git a/components/ip.c b/components/ip.c new file mode 100644 index 0000000..ac75661 --- /dev/null +++ b/components/ip.c @@ -0,0 +1,89 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#if defined(__OpenBSD__) + #include + #include +#elif defined(__FreeBSD__) + #include + #include +#endif + +#include "../slstatus.h" +#include "../util.h" + +static const char * +ip(const char *interface, unsigned short sa_family) +{ + struct ifaddrs *ifaddr, *ifa; + int s; + char host[NI_MAXHOST]; + + if (getifaddrs(&ifaddr) < 0) { + warn("getifaddrs:"); + return NULL; + } + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) + continue; + if (strcmp(ifa->ifa_name, interface) || + ifa->ifa_addr->sa_family != sa_family) + continue; + + s = getnameinfo(ifa->ifa_addr, + (sa_family == AF_INET) ? sizeof(struct sockaddr_in) + : sizeof(struct sockaddr_in6), + host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST); + freeifaddrs(ifaddr); + if (s != 0) { + warn("getnameinfo: %s", gai_strerror(s)); + return NULL; + } + return bprintf("%s", host); + } + + freeifaddrs(ifaddr); + + return NULL; +} + +const char * +ipv4(const char *interface) +{ + return ip(interface, AF_INET); +} + +const char * +ipv6(const char *interface) +{ + return ip(interface, AF_INET6); +} + +const char * +up(const char *interface) +{ + struct ifaddrs *ifaddr, *ifa; + + if (getifaddrs(&ifaddr) < 0) { + warn("getifaddrs:"); + return NULL; + } + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) + continue; + + if (!strcmp(ifa->ifa_name, interface)) { + freeifaddrs(ifaddr); + return ifa->ifa_flags & IFF_UP ? "up" : "down"; + } + } + + freeifaddrs(ifaddr); + + return NULL; +} diff --git a/components/kernel_release.c b/components/kernel_release.c new file mode 100644 index 0000000..36a6a44 --- /dev/null +++ b/components/kernel_release.c @@ -0,0 +1,19 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +kernel_release(const char *unused) +{ + struct utsname udata; + + if (uname(&udata) < 0) { + warn("uname:"); + return NULL; + } + + return bprintf("%s", udata.release); +} diff --git a/components/keyboard_indicators.c b/components/keyboard_indicators.c new file mode 100644 index 0000000..5f62bb7 --- /dev/null +++ b/components/keyboard_indicators.c @@ -0,0 +1,50 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +/* + * fmt consists of uppercase or lowercase 'c' for caps lock and/or 'n' for num + * lock, each optionally followed by '?', in the order of indicators desired. + * If followed by '?', the letter with case preserved is included in the output + * if the corresponding indicator is on. Otherwise, the letter is always + * included, lowercase when off and uppercase when on. + */ +const char * +keyboard_indicators(const char *fmt) +{ + Display *dpy; + XKeyboardState state; + size_t fmtlen, i, n; + int togglecase, isset; + char key; + + if (!(dpy = XOpenDisplay(NULL))) { + warn("XOpenDisplay: Failed to open display"); + return NULL; + } + XGetKeyboardControl(dpy, &state); + XCloseDisplay(dpy); + + fmtlen = strnlen(fmt, 4); + for (i = n = 0; i < fmtlen; i++) { + key = tolower(fmt[i]); + if (key != 'c' && key != 'n') + continue; + + togglecase = (i + 1 >= fmtlen || fmt[i + 1] != '?'); + isset = (state.led_mask & (1 << (key == 'n'))); + + if (togglecase) + buf[n++] = isset ? toupper(key) : key; + else if (isset) + buf[n++] = fmt[i]; + } + + buf[n] = 0; + return buf; +} diff --git a/components/keymap.c b/components/keymap.c new file mode 100644 index 0000000..22224f3 --- /dev/null +++ b/components/keymap.c @@ -0,0 +1,86 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +static int +valid_layout_or_variant(char *sym) +{ + size_t i; + /* invalid symbols from xkb rules config */ + static const char *invalid[] = { "evdev", "inet", "pc", "base" }; + + for (i = 0; i < LEN(invalid); i++) + if (!strncmp(sym, invalid[i], strlen(invalid[i]))) + return 0; + + return 1; +} + +static char * +get_layout(char *syms, int grp_num) +{ + char *tok, *layout; + int grp; + + layout = NULL; + tok = strtok(syms, "+:_"); + for (grp = 0; tok && grp <= grp_num; tok = strtok(NULL, "+:_")) { + if (!valid_layout_or_variant(tok)) { + continue; + } else if (strlen(tok) == 1 && isdigit(tok[0])) { + /* ignore :2, :3, :4 (additional layout groups) */ + continue; + } + layout = tok; + grp++; + } + + return layout; +} + +const char * +keymap(const char *unused) +{ + Display *dpy; + XkbDescRec *desc; + XkbStateRec state; + char *symbols; + const char *layout; + + layout = NULL; + + if (!(dpy = XOpenDisplay(NULL))) { + warn("XOpenDisplay: Failed to open display"); + return NULL; + } + if (!(desc = XkbAllocKeyboard())) { + warn("XkbAllocKeyboard: Failed to allocate keyboard"); + goto end; + } + if (XkbGetNames(dpy, XkbSymbolsNameMask, desc)) { + warn("XkbGetNames: Failed to retrieve key symbols"); + goto end; + } + if (XkbGetState(dpy, XkbUseCoreKbd, &state)) { + warn("XkbGetState: Failed to retrieve keyboard state"); + goto end; + } + if (!(symbols = XGetAtomName(dpy, desc->names->symbols))) { + warn("XGetAtomName: Failed to get atom name"); + goto end; + } + layout = bprintf("%s", get_layout(symbols, state.group)); + XFree(symbols); +end: + XkbFreeKeyboard(desc, XkbSymbolsNameMask, 1); + if (XCloseDisplay(dpy)) + warn("XCloseDisplay: Failed to close display"); + + return layout; +} diff --git a/components/load_avg.c b/components/load_avg.c new file mode 100644 index 0000000..f278a40 --- /dev/null +++ b/components/load_avg.c @@ -0,0 +1,19 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +load_avg(const char *unused) +{ + double avgs[3]; + + if (getloadavg(avgs, 3) < 0) { + warn("getloadavg: Failed to obtain load average"); + return NULL; + } + + return bprintf("%.2f %.2f %.2f", avgs[0], avgs[1], avgs[2]); +} diff --git a/components/netspeeds.c b/components/netspeeds.c new file mode 100644 index 0000000..cde6fa9 --- /dev/null +++ b/components/netspeeds.c @@ -0,0 +1,129 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__linux__) + #include + + #define NET_RX_BYTES "/sys/class/net/%s/statistics/rx_bytes" + #define NET_TX_BYTES "/sys/class/net/%s/statistics/tx_bytes" + + const char * + netspeed_rx(const char *interface) + { + uintmax_t oldrxbytes; + static uintmax_t rxbytes; + extern const unsigned int interval; + char path[PATH_MAX]; + + oldrxbytes = rxbytes; + + if (esnprintf(path, sizeof(path), NET_RX_BYTES, interface) < 0) + return NULL; + if (pscanf(path, "%ju", &rxbytes) != 1) + return NULL; + if (oldrxbytes == 0) + return NULL; + + return fmt_human((rxbytes - oldrxbytes) * 1000 / interval, + 1024); + } + + const char * + netspeed_tx(const char *interface) + { + uintmax_t oldtxbytes; + static uintmax_t txbytes; + extern const unsigned int interval; + char path[PATH_MAX]; + + oldtxbytes = txbytes; + + if (esnprintf(path, sizeof(path), NET_TX_BYTES, interface) < 0) + return NULL; + if (pscanf(path, "%ju", &txbytes) != 1) + return NULL; + if (oldtxbytes == 0) + return NULL; + + return fmt_human((txbytes - oldtxbytes) * 1000 / interval, + 1024); + } +#elif defined(__OpenBSD__) | defined(__FreeBSD__) + #include + #include + #include + #include + #include + + const char * + netspeed_rx(const char *interface) + { + struct ifaddrs *ifal, *ifa; + struct if_data *ifd; + uintmax_t oldrxbytes; + static uintmax_t rxbytes; + extern const unsigned int interval; + int if_ok = 0; + + oldrxbytes = rxbytes; + + if (getifaddrs(&ifal) < 0) { + warn("getifaddrs failed"); + return NULL; + } + rxbytes = 0; + for (ifa = ifal; ifa; ifa = ifa->ifa_next) + if (!strcmp(ifa->ifa_name, interface) && + (ifd = (struct if_data *)ifa->ifa_data)) + rxbytes += ifd->ifi_ibytes, if_ok = 1; + + freeifaddrs(ifal); + if (!if_ok) { + warn("reading 'if_data' failed"); + return NULL; + } + if (oldrxbytes == 0) + return NULL; + + return fmt_human((rxbytes - oldrxbytes) * 1000 / interval, + 1024); + } + + const char * + netspeed_tx(const char *interface) + { + struct ifaddrs *ifal, *ifa; + struct if_data *ifd; + uintmax_t oldtxbytes; + static uintmax_t txbytes; + extern const unsigned int interval; + int if_ok = 0; + + oldtxbytes = txbytes; + + if (getifaddrs(&ifal) < 0) { + warn("getifaddrs failed"); + return NULL; + } + txbytes = 0; + for (ifa = ifal; ifa; ifa = ifa->ifa_next) + if (!strcmp(ifa->ifa_name, interface) && + (ifd = (struct if_data *)ifa->ifa_data)) + txbytes += ifd->ifi_obytes, if_ok = 1; + + freeifaddrs(ifal); + if (!if_ok) { + warn("reading 'if_data' failed"); + return NULL; + } + if (oldtxbytes == 0) + return NULL; + + return fmt_human((txbytes - oldtxbytes) * 1000 / interval, + 1024); + } +#endif diff --git a/components/num_files.c b/components/num_files.c new file mode 100644 index 0000000..df0acd1 --- /dev/null +++ b/components/num_files.c @@ -0,0 +1,32 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +num_files(const char *path) +{ + struct dirent *dp; + DIR *dir; + int num; + + if (!(dir = opendir(path))) { + warn("opendir '%s':", path); + return NULL; + } + + num = 0; + while ((dp = readdir(dir))) { + if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) + continue; /* skip self and parent */ + + num++; + } + + closedir(dir); + + return bprintf("%d", num); +} diff --git a/components/ram.c b/components/ram.c new file mode 100644 index 0000000..bf71dcf --- /dev/null +++ b/components/ram.c @@ -0,0 +1,227 @@ +/* See LICENSE file for copyright and license details. */ +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__linux__) + #include + + const char * + ram_free(const char *unused) + { + uintmax_t free; + FILE *fp; + + if (!(fp = fopen("/proc/meminfo", "r"))) + return NULL; + + if (lscanf(fp, "MemFree:", "%ju kB", &free) != 1) { + fclose(fp); + return NULL; + } + + fclose(fp); + return fmt_human(free * 1024, 1024); + } + + const char * + ram_perc(const char *unused) + { + uintmax_t total, free, buffers, cached, shmem, sreclaimable; + int percent; + FILE *fp; + + if (!(fp = fopen("/proc/meminfo", "r"))) + return NULL; + + if (lscanf(fp, "MemTotal:", "%ju kB", &total) != 1 || + lscanf(fp, "MemFree:", "%ju kB", &free) != 1 || + lscanf(fp, "Buffers:", "%ju kB", &buffers) != 1 || + lscanf(fp, "Cached:", "%ju kB", &cached) != 1 || + lscanf(fp, "Shmem:", "%ju kB", &shmem) != 1 || + lscanf(fp, "SReclaimable:", "%ju kB", &sreclaimable) != 1) { + fclose(fp); + return NULL; + } + fclose(fp); + + if (total == 0) + return NULL; + + percent = 100 * (total - free - buffers - cached - sreclaimable + shmem) / total; + return bprintf("%d", percent); + } + + const char * + ram_total(const char *unused) + { + uintmax_t total; + + if (pscanf("/proc/meminfo", "MemTotal: %ju kB\n", &total) + != 1) + return NULL; + + return fmt_human(total * 1024, 1024); + } + + const char * + ram_used(const char *unused) + { + uintmax_t total, free, buffers, cached, used, shmem, sreclaimable; + FILE *fp; + + if (!(fp = fopen("/proc/meminfo", "r"))) + return NULL; + + if (lscanf(fp, "MemTotal:", "%ju kB", &total) != 1 || + lscanf(fp, "MemFree:", "%ju kB", &free) != 1 || + lscanf(fp, "Buffers:", "%ju kB", &buffers) != 1 || + lscanf(fp, "Cached:", "%ju kB", &cached) != 1 || + lscanf(fp, "Shmem:", "%ju kB", &shmem) != 1 || + lscanf(fp, "SReclaimable:", "%ju kB", &sreclaimable) != 1) { + fclose(fp); + return NULL; + } + fclose(fp); + + used = total - free - buffers - cached - sreclaimable + shmem; + return fmt_human(used * 1024, 1024); + } +#elif defined(__OpenBSD__) + #include + #include + #include + #include + + #define LOG1024 10 + #define pagetok(size, pageshift) (size_t)(size << (pageshift - LOG1024)) + + inline int + load_uvmexp(struct uvmexp *uvmexp) + { + int uvmexp_mib[] = {CTL_VM, VM_UVMEXP}; + size_t size; + + size = sizeof(*uvmexp); + + if (sysctl(uvmexp_mib, 2, uvmexp, &size, NULL, 0) >= 0) + return 1; + + return 0; + } + + const char * + ram_free(const char *unused) + { + struct uvmexp uvmexp; + int free_pages; + + if (!load_uvmexp(&uvmexp)) + return NULL; + + free_pages = uvmexp.npages - uvmexp.active; + return fmt_human(pagetok(free_pages, uvmexp.pageshift) * + 1024, 1024); + } + + const char * + ram_perc(const char *unused) + { + struct uvmexp uvmexp; + int percent; + + if (!load_uvmexp(&uvmexp)) + return NULL; + + percent = uvmexp.active * 100 / uvmexp.npages; + return bprintf("%d", percent); + } + + const char * + ram_total(const char *unused) + { + struct uvmexp uvmexp; + + if (!load_uvmexp(&uvmexp)) + return NULL; + + return fmt_human(pagetok(uvmexp.npages, + uvmexp.pageshift) * 1024, 1024); + } + + const char * + ram_used(const char *unused) + { + struct uvmexp uvmexp; + + if (!load_uvmexp(&uvmexp)) + return NULL; + + return fmt_human(pagetok(uvmexp.active, + uvmexp.pageshift) * 1024, 1024); + } +#elif defined(__FreeBSD__) + #include + #include + #include + #include + + const char * + ram_free(const char *unused) { + struct vmtotal vm_stats; + int mib[] = {CTL_VM, VM_TOTAL}; + size_t len; + + len = sizeof(struct vmtotal); + if (sysctl(mib, 2, &vm_stats, &len, NULL, 0) < 0 + || !len) + return NULL; + + return fmt_human(vm_stats.t_free * getpagesize(), 1024); + } + + const char * + ram_total(const char *unused) { + unsigned int npages; + size_t len; + + len = sizeof(npages); + if (sysctlbyname("vm.stats.vm.v_page_count", + &npages, &len, NULL, 0) < 0 || !len) + return NULL; + + return fmt_human(npages * getpagesize(), 1024); + } + + const char * + ram_perc(const char *unused) { + unsigned int npages; + unsigned int active; + size_t len; + + len = sizeof(npages); + if (sysctlbyname("vm.stats.vm.v_page_count", + &npages, &len, NULL, 0) < 0 || !len) + return NULL; + + if (sysctlbyname("vm.stats.vm.v_active_count", + &active, &len, NULL, 0) < 0 || !len) + return NULL; + + return bprintf("%d", active * 100 / npages); + } + + const char * + ram_used(const char *unused) { + unsigned int active; + size_t len; + + len = sizeof(active); + if (sysctlbyname("vm.stats.vm.v_active_count", + &active, &len, NULL, 0) < 0 || !len) + return NULL; + + return fmt_human(active * getpagesize(), 1024); + } +#endif diff --git a/components/run_command.c b/components/run_command.c new file mode 100644 index 0000000..93bf6da --- /dev/null +++ b/components/run_command.c @@ -0,0 +1,31 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +run_command(const char *cmd) +{ + char *p; + FILE *fp; + + if (!(fp = popen(cmd, "r"))) { + warn("popen '%s':", cmd); + return NULL; + } + + p = fgets(buf, sizeof(buf) - 1, fp); + if (pclose(fp) < 0) { + warn("pclose '%s':", cmd); + return NULL; + } + if (!p) + return NULL; + + if ((p = strrchr(buf, '\n'))) + p[0] = '\0'; + + return buf[0] ? buf : NULL; +} diff --git a/components/swap.c b/components/swap.c new file mode 100644 index 0000000..dbd2747 --- /dev/null +++ b/components/swap.c @@ -0,0 +1,276 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__linux__) + static int + get_swap_info(long *s_total, long *s_free, long *s_cached) + { + FILE *fp; + struct { + const char *name; + const size_t len; + long *var; + } ent[] = { + { "SwapTotal", sizeof("SwapTotal") - 1, s_total }, + { "SwapFree", sizeof("SwapFree") - 1, s_free }, + { "SwapCached", sizeof("SwapCached") - 1, s_cached }, + }; + size_t line_len = 0, i, left; + char *line = NULL; + + /* get number of fields we want to extract */ + for (i = 0, left = 0; i < LEN(ent); i++) + if (ent[i].var) + left++; + + if (!(fp = fopen("/proc/meminfo", "r"))) { + warn("fopen '/proc/meminfo':"); + return 1; + } + + /* read file line by line and extract field information */ + while (left > 0 && getline(&line, &line_len, fp) >= 0) { + for (i = 0; i < LEN(ent); i++) { + if (ent[i].var && + !strncmp(line, ent[i].name, ent[i].len)) { + sscanf(line + ent[i].len + 1, + "%ld kB\n", ent[i].var); + left--; + break; + } + } + } + free(line); + if (ferror(fp)) { + warn("getline '/proc/meminfo':"); + return 1; + } + + fclose(fp); + return 0; + } + + const char * + swap_free(const char *unused) + { + long free; + + if (get_swap_info(NULL, &free, NULL)) + return NULL; + + return fmt_human(free * 1024, 1024); + } + + const char * + swap_perc(const char *unused) + { + long total, free, cached; + + if (get_swap_info(&total, &free, &cached) || total == 0) + return NULL; + + return bprintf("%d", 100 * (total - free - cached) / total); + } + + const char * + swap_total(const char *unused) + { + long total; + + if (get_swap_info(&total, NULL, NULL)) + return NULL; + + return fmt_human(total * 1024, 1024); + } + + const char * + swap_used(const char *unused) + { + long total, free, cached; + + if (get_swap_info(&total, &free, &cached)) + return NULL; + + return fmt_human((total - free - cached) * 1024, 1024); + } +#elif defined(__OpenBSD__) + #include + #include + #include + #include + + static int + getstats(int *total, int *used) + { + struct swapent *sep, *fsep; + int rnswap, nswap, i; + + if ((nswap = swapctl(SWAP_NSWAP, 0, 0)) < 1) { + warn("swapctl 'SWAP_NSWAP':"); + return 1; + } + if (!(fsep = sep = calloc(nswap, sizeof(*sep)))) { + warn("calloc 'nswap':"); + return 1; + } + if ((rnswap = swapctl(SWAP_STATS, (void *)sep, nswap)) < 0) { + warn("swapctl 'SWAP_STATS':"); + free(fsep); + return 1; + } + if (nswap != rnswap) { + warn("getstats: SWAP_STATS != SWAP_NSWAP"); + free(fsep); + return 1; + } + + *total = 0; + *used = 0; + + for (i = 0; i < rnswap; i++) { + *total += sep[i].se_nblks >> 1; + *used += sep[i].se_inuse >> 1; + } + + free(fsep); + + return 0; + } + + const char * + swap_free(const char *unused) + { + int total, used; + + if (getstats(&total, &used)) + return NULL; + + return fmt_human((total - used) * 1024, 1024); + } + + const char * + swap_perc(const char *unused) + { + int total, used; + + if (getstats(&total, &used)) + return NULL; + + if (total == 0) + return NULL; + + return bprintf("%d", 100 * used / total); + } + + const char * + swap_total(const char *unused) + { + int total, used; + + if (getstats(&total, &used)) + return NULL; + + return fmt_human(total * 1024, 1024); + } + + const char * + swap_used(const char *unused) + { + int total, used; + + if (getstats(&total, &used)) + return NULL; + + return fmt_human(used * 1024, 1024); + } +#elif defined(__FreeBSD__) + #include + #include + #include + #include + #include + + static int getswapinfo(struct kvm_swap *swap_info, size_t size) + { + kvm_t *kd; + + kd = kvm_openfiles(NULL, "/dev/null", NULL, 0, NULL); + if (kd == NULL) { + warn("kvm_openfiles '/dev/null':"); + return 0; + } + + if (kvm_getswapinfo(kd, swap_info, size, 0 /* Unused flags */) < 0) { + warn("kvm_getswapinfo:"); + kvm_close(kd); + return 0; + } + + kvm_close(kd); + return 1; + } + + const char * + swap_free(const char *unused) + { + struct kvm_swap swap_info[1]; + long used, total; + + if (!getswapinfo(swap_info, 1)) + return NULL; + + total = swap_info[0].ksw_total; + used = swap_info[0].ksw_used; + + return fmt_human((total - used) * getpagesize(), 1024); + } + + const char * + swap_perc(const char *unused) + { + struct kvm_swap swap_info[1]; + long used, total; + + if (!getswapinfo(swap_info, 1)) + return NULL; + + total = swap_info[0].ksw_total; + used = swap_info[0].ksw_used; + + return bprintf("%d", used * 100 / total); + } + + const char * + swap_total(const char *unused) + { + struct kvm_swap swap_info[1]; + long total; + + if (!getswapinfo(swap_info, 1)) + return NULL; + + total = swap_info[0].ksw_total; + + return fmt_human(total * getpagesize(), 1024); + } + + const char * + swap_used(const char *unused) + { + struct kvm_swap swap_info[1]; + long used; + + if (!getswapinfo(swap_info, 1)) + return NULL; + + used = swap_info[0].ksw_used; + + return fmt_human(used * getpagesize(), 1024); + } +#endif diff --git a/components/temperature.c b/components/temperature.c new file mode 100644 index 0000000..7cf1394 --- /dev/null +++ b/components/temperature.c @@ -0,0 +1,73 @@ +/* See LICENSE file for copyright and license details. */ +#include + +#include "../slstatus.h" +#include "../util.h" + + +#if defined(__linux__) + #include + + const char * + temp(const char *file) + { + uintmax_t temp; + + if (pscanf(file, "%ju", &temp) != 1) + return NULL; + + return bprintf("%ju", temp / 1000); + } +#elif defined(__OpenBSD__) + #include + #include /* before for struct timeval */ + #include + #include + + const char * + temp(const char *unused) + { + int mib[5]; + size_t size; + struct sensor temp; + + mib[0] = CTL_HW; + mib[1] = HW_SENSORS; + mib[2] = 0; /* cpu0 */ + mib[3] = SENSOR_TEMP; + mib[4] = 0; /* temp0 */ + + size = sizeof(temp); + + if (sysctl(mib, 5, &temp, &size, NULL, 0) < 0) { + warn("sysctl 'SENSOR_TEMP':"); + return NULL; + } + + /* kelvin to celsius */ + return bprintf("%d", (int)((float)(temp.value-273150000) / 1E6)); + } +#elif defined(__FreeBSD__) + #include + #include + #include + + #define ACPI_TEMP "hw.acpi.thermal.%s.temperature" + + const char * + temp(const char *zone) + { + char buf[256]; + int temp; + size_t len; + + len = sizeof(temp); + snprintf(buf, sizeof(buf), ACPI_TEMP, zone); + if (sysctlbyname(buf, &temp, &len, NULL, 0) < 0 + || !len) + return NULL; + + /* kelvin to decimal celcius */ + return bprintf("%d.%d", (temp - 2731) / 10, abs((temp - 2731) % 10)); + } +#endif diff --git a/components/uptime.c b/components/uptime.c new file mode 100644 index 0000000..6227f73 --- /dev/null +++ b/components/uptime.c @@ -0,0 +1,34 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(CLOCK_BOOTTIME) + #define UPTIME_FLAG CLOCK_BOOTTIME +#elif defined(CLOCK_UPTIME) + #define UPTIME_FLAG CLOCK_UPTIME +#else + #define UPTIME_FLAG CLOCK_MONOTONIC +#endif + +const char * +uptime(const char *unused) +{ + char warn_buf[256]; + uintmax_t h, m; + struct timespec uptime; + + if (clock_gettime(UPTIME_FLAG, &uptime) < 0) { + snprintf(warn_buf, sizeof(warn_buf), "clock_gettime %d", UPTIME_FLAG); + warn(warn_buf); + return NULL; + } + + h = uptime.tv_sec / 3600; + m = uptime.tv_sec % 3600 / 60; + + return bprintf("%juh %jum", h, m); +} diff --git a/components/user.c b/components/user.c new file mode 100644 index 0000000..3517495 --- /dev/null +++ b/components/user.c @@ -0,0 +1,33 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +const char * +gid(const char *unused) +{ + return bprintf("%d", getgid()); +} + +const char * +username(const char *unused) +{ + struct passwd *pw; + + if (!(pw = getpwuid(geteuid()))) { + warn("getpwuid '%d':", geteuid()); + return NULL; + } + + return bprintf("%s", pw->pw_name); +} + +const char * +uid(const char *unused) +{ + return bprintf("%d", geteuid()); +} diff --git a/components/volume.c b/components/volume.c new file mode 100644 index 0000000..20171da --- /dev/null +++ b/components/volume.c @@ -0,0 +1,224 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#if defined(__OpenBSD__) | defined(__FreeBSD__) + #include + #include + #include + #include + + struct control { + LIST_ENTRY(control) next; + unsigned int addr; + #define CTRL_NONE 0 + #define CTRL_LEVEL 1 + #define CTRL_MUTE 2 + unsigned int type; + unsigned int maxval; + unsigned int val; + }; + + static LIST_HEAD(, control) controls = LIST_HEAD_INITIALIZER(controls); + static struct pollfd *pfds; + static struct sioctl_hdl *hdl; + static int initialized; + + /* + * Call-back to obtain the description of all audio controls. + */ + static void + ondesc(void *unused, struct sioctl_desc *desc, int val) + { + struct control *c, *ctmp; + unsigned int type = CTRL_NONE; + + if (desc == NULL) + return; + + /* Delete existing audio control with the same address. */ + LIST_FOREACH_SAFE(c, &controls, next, ctmp) { + if (desc->addr == c->addr) { + LIST_REMOVE(c, next); + free(c); + break; + } + } + + /* Only match output.level and output.mute audio controls. */ + if (desc->group[0] != 0 || + strcmp(desc->node0.name, "output") != 0) + return; + if (desc->type == SIOCTL_NUM && + strcmp(desc->func, "level") == 0) + type = CTRL_LEVEL; + else if (desc->type == SIOCTL_SW && + strcmp(desc->func, "mute") == 0) + type = CTRL_MUTE; + else + return; + + c = malloc(sizeof(struct control)); + if (c == NULL) { + warn("sndio: failed to allocate audio control\n"); + return; + } + + c->addr = desc->addr; + c->type = type; + c->maxval = desc->maxval; + c->val = val; + LIST_INSERT_HEAD(&controls, c, next); + } + + /* + * Call-back invoked whenever an audio control changes. + */ + static void + onval(void *unused, unsigned int addr, unsigned int val) + { + struct control *c; + + LIST_FOREACH(c, &controls, next) { + if (c->addr == addr) + break; + } + if (c == NULL) + return; + c->val = val; + } + + static void + cleanup(void) + { + struct control *c; + + if (hdl) { + sioctl_close(hdl); + hdl = NULL; + } + + free(pfds); + pfds = NULL; + + while (!LIST_EMPTY(&controls)) { + c = LIST_FIRST(&controls); + LIST_REMOVE(c, next); + free(c); + } + } + + static int + init(void) + { + hdl = sioctl_open(SIO_DEVANY, SIOCTL_READ, 0); + if (hdl == NULL) { + warn("sndio: cannot open device"); + goto failed; + } + + if (!sioctl_ondesc(hdl, ondesc, NULL)) { + warn("sndio: cannot set control description call-back"); + goto failed; + } + + if (!sioctl_onval(hdl, onval, NULL)) { + warn("sndio: cannot set control values call-back"); + goto failed; + } + + pfds = calloc(sioctl_nfds(hdl), sizeof(struct pollfd)); + if (pfds == NULL) { + warn("sndio: cannot allocate pollfd structures"); + goto failed; + } + + return 1; + failed: + cleanup(); + return 0; + } + + const char * + vol_perc(const char *unused) + { + struct control *c; + int n, v, value; + + if (!initialized) + initialized = init(); + + if (hdl == NULL) + return NULL; + + n = sioctl_pollfd(hdl, pfds, POLLIN); + if (n > 0) { + n = poll(pfds, n, 0); + if (n > 0) { + if (sioctl_revents(hdl, pfds) & POLLHUP) { + warn("sndio: disconnected"); + cleanup(); + initialized = 0; + return NULL; + } + } + } + + value = 100; + LIST_FOREACH(c, &controls, next) { + if (c->type == CTRL_MUTE && c->val == 1) + value = 0; + else if (c->type == CTRL_LEVEL) { + v = (c->val * 100 + c->maxval / 2) / c->maxval; + /* For multiple channels return the minimum. */ + if (v < value) + value = v; + } + } + + return bprintf("%d", value); + } +#else + #include + + const char * + vol_perc(const char *card) + { + size_t i; + int v, afd, devmask; + char *vnames[] = SOUND_DEVICE_NAMES; + + if ((afd = open(card, O_RDONLY | O_NONBLOCK)) < 0) { + warn("open '%s':", card); + return NULL; + } + + if (ioctl(afd, (int)SOUND_MIXER_READ_DEVMASK, &devmask) < 0) { + warn("ioctl 'SOUND_MIXER_READ_DEVMASK':"); + close(afd); + return NULL; + } + for (i = 0; i < LEN(vnames); i++) { + if (devmask & (1 << i) && !strcmp("vol", vnames[i])) { + if (ioctl(afd, MIXER_READ(i), &v) < 0) { + warn("ioctl 'MIXER_READ(%ld)':", i); + close(afd); + return NULL; + } + close(afd); + return bprintf("%d", v & 0xff); + } + } + + close(afd); + warn("vol_perc: no 'vol' mixer device"); + + return NULL; + } +#endif diff --git a/components/wifi.c b/components/wifi.c new file mode 100644 index 0000000..806a7ad --- /dev/null +++ b/components/wifi.c @@ -0,0 +1,417 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include + +#include "../slstatus.h" +#include "../util.h" + +#define RSSI_TO_PERC(rssi) \ + rssi >= -50 ? 100 : \ + (rssi <= -100 ? 0 : \ + (2 * (rssi + 100))) + +#if defined(__linux__) + #include + #include + #include + #include + #include + + static int nlsock = -1; + static uint32_t seq = 1; + static char resp[4096 + 1]; + + static char * + findattr(int attr, const char *p, const char *e, size_t *len) + { + while (p < e) { + struct nlattr nla; + memcpy(&nla, p, sizeof(nla)); + if (nla.nla_type == attr) { + *len = nla.nla_len - NLA_HDRLEN; + return (char *)(p + NLA_HDRLEN); + } + p += NLA_ALIGN(nla.nla_len); + } + return NULL; + } + + static uint16_t + nl80211fam(void) + { + static const char family[] = "nl80211"; + static uint16_t id; + ssize_t r; + size_t len; + char ctrl[NLMSG_HDRLEN+GENL_HDRLEN+NLA_HDRLEN+NLA_ALIGN(sizeof(family))] = {0}, *p = ctrl; + + if (id) + return id; + + memcpy(p, &(struct nlmsghdr){ + .nlmsg_len = sizeof(ctrl), + .nlmsg_type = GENL_ID_CTRL, + .nlmsg_flags = NLM_F_REQUEST, + .nlmsg_seq = seq++, + .nlmsg_pid = 0, + }, sizeof(struct nlmsghdr)); + p += NLMSG_HDRLEN; + memcpy(p, &(struct genlmsghdr){ + .cmd = CTRL_CMD_GETFAMILY, + .version = 1, + }, sizeof(struct genlmsghdr)); + p += GENL_HDRLEN; + memcpy(p, &(struct nlattr){ + .nla_len = NLA_HDRLEN+sizeof(family), + .nla_type = CTRL_ATTR_FAMILY_NAME, + }, sizeof(struct nlattr)); + p += NLA_HDRLEN; + memcpy(p, family, sizeof(family)); + + if (nlsock < 0) + nlsock = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC); + if (nlsock < 0) { + warn("socket 'AF_NETLINK':"); + return 0; + } + if (send(nlsock, ctrl, sizeof(ctrl), 0) != sizeof(ctrl)) { + warn("send 'AF_NETLINK':"); + return 0; + } + r = recv(nlsock, resp, sizeof(resp), 0); + if (r < 0) { + warn("recv 'AF_NETLINK':"); + return 0; + } + if ((size_t)r <= sizeof(ctrl)) + return 0; + p = findattr(CTRL_ATTR_FAMILY_ID, resp + sizeof(ctrl), resp + r, &len); + if (p && len == 2) + memcpy(&id, p, 2); + + return id; + } + + static int + ifindex(const char *interface) + { + static struct ifreq ifr; + static int ifsock = -1; + + if (ifsock < 0) + ifsock = socket(AF_UNIX, SOCK_DGRAM, 0); + if (ifsock < 0) { + warn("socket 'AF_UNIX':"); + return -1; + } + if (strcmp(ifr.ifr_name, interface) != 0) { + if (snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), + "%s", interface) >= (int)sizeof(ifr.ifr_name)) { + warn("interface name too long: '%s'", interface); + return -1; + } + } + if (ioctl(ifsock, SIOCGIFINDEX, &ifr) != 0) { + warn("ioctl 'SIOCGIFINDEX':"); + return -1; + } + return ifr.ifr_ifindex; + } + + const char * + wifi_essid(const char *interface) + { + uint16_t fam = nl80211fam(); + ssize_t r; + size_t len; + char req[NLMSG_HDRLEN+GENL_HDRLEN+NLA_HDRLEN+NLA_ALIGN(4)] = {0}, *p = req; + int idx = ifindex(interface); + if (!fam) { + fprintf(stderr, "nl80211 family not found\n"); + return NULL; + } + if (idx < 0) { + fprintf(stderr, "interface %s not found\n", interface); + return NULL; + } + + memcpy(p, &(struct nlmsghdr){ + .nlmsg_len = sizeof(req), + .nlmsg_type = fam, + .nlmsg_flags = NLM_F_REQUEST, + .nlmsg_seq = seq++, + .nlmsg_pid = 0, + }, sizeof(struct nlmsghdr)); + p += NLMSG_HDRLEN; + memcpy(p, &(struct genlmsghdr){ + .cmd = NL80211_CMD_GET_INTERFACE, + .version = 1, + }, sizeof(struct genlmsghdr)); + p += GENL_HDRLEN; + memcpy(p, &(struct nlattr){ + .nla_len = NLA_HDRLEN+4, + .nla_type = NL80211_ATTR_IFINDEX, + }, sizeof(struct nlattr)); + p += NLA_HDRLEN; + memcpy(p, &(uint32_t){idx}, 4); + + if (send(nlsock, req, sizeof(req), 0) != sizeof(req)) { + warn("send 'AF_NETLINK':"); + return NULL; + } + r = recv(nlsock, resp, sizeof(resp) - 1, 0); + if (r < 0) { + warn("recv 'AF_NETLINK':"); + return NULL; + } + + if ((size_t)r <= NLMSG_HDRLEN + GENL_HDRLEN) + return NULL; + p = findattr(NL80211_ATTR_SSID, resp + NLMSG_HDRLEN + GENL_HDRLEN, resp + r, &len); + if (p) + p[len] = 0; + + return p; + } + + const char * + wifi_perc(const char *interface) + { + static char strength[4]; + struct nlmsghdr hdr; + uint16_t fam = nl80211fam(); + ssize_t r; + size_t len; + char req[NLMSG_HDRLEN + GENL_HDRLEN + NLA_HDRLEN + NLA_ALIGN(4)] = {0}, *p = req, *e; + int idx = ifindex(interface); + + if (idx < 0) { + fprintf(stderr, "interface %s not found\n", interface); + return NULL; + } + + memcpy(p, &(struct nlmsghdr){ + .nlmsg_len = sizeof(req), + .nlmsg_type = fam, + .nlmsg_flags = NLM_F_REQUEST|NLM_F_DUMP, + .nlmsg_seq = seq++, + .nlmsg_pid = 0, + }, sizeof(struct nlmsghdr)); + p += NLMSG_HDRLEN; + memcpy(p, &(struct genlmsghdr){ + .cmd = NL80211_CMD_GET_STATION, + .version = 1, + }, sizeof(struct genlmsghdr)); + p += GENL_HDRLEN; + memcpy(p, &(struct nlattr){ + .nla_len = NLA_HDRLEN + 4, + .nla_type = NL80211_ATTR_IFINDEX, + }, sizeof(struct nlattr)); + p += NLA_HDRLEN; + memcpy(p, &idx, 4); + + if (send(nlsock, req, sizeof(req), 0) != sizeof(req)) { + warn("send 'AF_NETLINK':"); + return NULL; + } + + *strength = 0; + while (1) { + r = recv(nlsock, resp, sizeof(resp), 0); + if (r < 0) { + warn("recv 'AF_NETLINK':"); + return NULL; + } + if ((size_t)r < sizeof(hdr)) + return NULL; + + for (p = resp; p != resp + r && (size_t)(resp + r-p) >= sizeof(hdr); p = e) { + memcpy(&hdr, p, sizeof(hdr)); + e = resp + r - p < hdr.nlmsg_len ? resp + r : p + hdr.nlmsg_len; + + if (!*strength && hdr.nlmsg_len > NLMSG_HDRLEN+GENL_HDRLEN) { + p += NLMSG_HDRLEN+GENL_HDRLEN; + p = findattr(NL80211_ATTR_STA_INFO, p, e, &len); + if (p) + p = findattr(NL80211_STA_INFO_SIGNAL_AVG, p, e, &len); + if (p && len == 1) + snprintf(strength, sizeof(strength), "%d", RSSI_TO_PERC(*p)); + } + if (hdr.nlmsg_type == NLMSG_DONE) + return *strength ? strength : NULL; + } + } + } +#elif defined(__OpenBSD__) + #include + #include + #include + #include /* before for NBBY */ + #include + #include + #include + + static int + load_ieee80211_nodereq(const char *interface, struct ieee80211_nodereq *nr) + { + struct ieee80211_bssid bssid; + int sockfd; + uint8_t zero_bssid[IEEE80211_ADDR_LEN]; + + memset(&bssid, 0, sizeof(bssid)); + memset(nr, 0, sizeof(struct ieee80211_nodereq)); + if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + warn("socket 'AF_INET':"); + return 0; + } + strlcpy(bssid.i_name, interface, sizeof(bssid.i_name)); + if ((ioctl(sockfd, SIOCG80211BSSID, &bssid)) < 0) { + warn("ioctl 'SIOCG80211BSSID':"); + close(sockfd); + return 0; + } + memset(&zero_bssid, 0, sizeof(zero_bssid)); + if (memcmp(bssid.i_bssid, zero_bssid, + IEEE80211_ADDR_LEN) == 0) { + close(sockfd); + return 0; + } + strlcpy(nr->nr_ifname, interface, sizeof(nr->nr_ifname)); + memcpy(&nr->nr_macaddr, bssid.i_bssid, sizeof(nr->nr_macaddr)); + if ((ioctl(sockfd, SIOCG80211NODE, nr)) < 0 && nr->nr_rssi) { + warn("ioctl 'SIOCG80211NODE':"); + close(sockfd); + return 0; + } + + return close(sockfd), 1; + } + + const char * + wifi_perc(const char *interface) + { + struct ieee80211_nodereq nr; + int q; + + if (load_ieee80211_nodereq(interface, &nr)) { + if (nr.nr_max_rssi) + q = IEEE80211_NODEREQ_RSSI(&nr); + else + q = RSSI_TO_PERC(nr.nr_rssi); + + return bprintf("%d", q); + } + + return NULL; + } + + const char * + wifi_essid(const char *interface) + { + struct ieee80211_nodereq nr; + + if (load_ieee80211_nodereq(interface, &nr)) + return bprintf("%s", nr.nr_nwid); + + return NULL; + } +#elif defined(__FreeBSD__) + #include + #include + + int + load_ieee80211req(int sock, const char *interface, void *data, int type, size_t *len) + { + char warn_buf[256]; + struct ieee80211req ireq; + memset(&ireq, 0, sizeof(ireq)); + ireq.i_type = type; + ireq.i_data = (caddr_t) data; + ireq.i_len = *len; + + strlcpy(ireq.i_name, interface, sizeof(ireq.i_name)); + if (ioctl(sock, SIOCG80211, &ireq) < 0) { + snprintf(warn_buf, sizeof(warn_buf), + "ioctl: 'SIOCG80211': %d", type); + warn(warn_buf); + return 0; + } + + *len = ireq.i_len; + return 1; + } + + const char * + wifi_perc(const char *interface) + { + union { + struct ieee80211req_sta_req sta; + uint8_t buf[24 * 1024]; + } info; + uint8_t bssid[IEEE80211_ADDR_LEN]; + int rssi_dbm; + int sockfd; + size_t len; + const char *fmt; + + if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + warn("socket 'AF_INET':"); + return NULL; + } + + /* Retreive MAC address of interface */ + len = IEEE80211_ADDR_LEN; + fmt = NULL; + if (load_ieee80211req(sockfd, interface, &bssid, IEEE80211_IOC_BSSID, &len)) + { + /* Retrieve info on station with above BSSID */ + memset(&info, 0, sizeof(info)); + memcpy(info.sta.is_u.macaddr, bssid, sizeof(bssid)); + + len = sizeof(info); + if (load_ieee80211req(sockfd, interface, &info, IEEE80211_IOC_STA_INFO, &len)) { + rssi_dbm = info.sta.info[0].isi_noise + + info.sta.info[0].isi_rssi / 2; + + fmt = bprintf("%d", RSSI_TO_PERC(rssi_dbm)); + } + } + + close(sockfd); + return fmt; + } + + const char * + wifi_essid(const char *interface) + { + char ssid[IEEE80211_NWID_LEN + 1]; + size_t len; + int sockfd; + const char *fmt; + + if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + warn("socket 'AF_INET':"); + return NULL; + } + + fmt = NULL; + len = sizeof(ssid); + memset(&ssid, 0, len); + if (load_ieee80211req(sockfd, interface, &ssid, IEEE80211_IOC_SSID, &len)) { + if (len < sizeof(ssid)) + len += 1; + else + len = sizeof(ssid); + + ssid[len - 1] = '\0'; + fmt = bprintf("%s", ssid); + } + + close(sockfd); + return fmt; + } +#endif diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..100093e --- /dev/null +++ b/config.def.h @@ -0,0 +1,70 @@ +/* See LICENSE file for copyright and license details. */ + +/* interval between updates (in ms) */ +const unsigned int interval = 1000; + +/* text to show if no value can be retrieved */ +static const char unknown_str[] = "n/a"; + +/* maximum output string length */ +#define MAXLEN 2048 + +/* + * function description argument (example) + * + * battery_perc battery percentage battery name (BAT0) + * NULL on OpenBSD/FreeBSD + * battery_remaining battery remaining HH:MM battery name (BAT0) + * NULL on OpenBSD/FreeBSD + * battery_state battery charging state battery name (BAT0) + * NULL on OpenBSD/FreeBSD + * cat read arbitrary file path + * cpu_freq cpu frequency in MHz NULL + * cpu_perc cpu usage in percent NULL + * datetime date and time format string (%F %T) + * disk_free free disk space in GB mountpoint path (/) + * disk_perc disk usage in percent mountpoint path (/) + * disk_total total disk space in GB mountpoint path (/) + * disk_used used disk space in GB mountpoint path (/) + * entropy available entropy NULL + * gid GID of current user NULL + * hostname hostname NULL + * ipv4 IPv4 address interface name (eth0) + * ipv6 IPv6 address interface name (eth0) + * kernel_release `uname -r` NULL + * keyboard_indicators caps/num lock indicators format string (c?n?) + * see keyboard_indicators.c + * keymap layout (variant) of current NULL + * keymap + * load_avg load average NULL + * netspeed_rx receive network speed interface name (wlan0) + * netspeed_tx transfer network speed interface name (wlan0) + * num_files number of files in a directory path + * (/home/foo/Inbox/cur) + * ram_free free memory in GB NULL + * ram_perc memory usage in percent NULL + * ram_total total memory size in GB NULL + * ram_used used memory in GB NULL + * run_command custom shell command command (echo foo) + * swap_free free swap in GB NULL + * swap_perc swap usage in percent NULL + * swap_total total swap size in GB NULL + * swap_used used swap in GB NULL + * temp temperature in degree celsius sensor file + * (/sys/class/thermal/...) + * NULL on OpenBSD + * thermal zone on FreeBSD + * (tz0, tz1, etc.) + * uid UID of current user NULL + * up interface is running interface name (eth0) + * uptime system uptime NULL + * username username of current user NULL + * vol_perc OSS/ALSA volume in percent mixer file (/dev/mixer) + * NULL on OpenBSD/FreeBSD + * wifi_essid WiFi ESSID interface name (wlan0) + * wifi_perc WiFi signal in percent interface name (wlan0) + */ +static const struct arg args[] = { + /* function format argument */ + { datetime, "%s", "%F %T" }, +}; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..a8f5c23 --- /dev/null +++ b/config.mk @@ -0,0 +1,22 @@ +# slstatus version +VERSION = 1.1 + +# customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +# flags +CPPFLAGS = -I$(X11INC) -D_DEFAULT_SOURCE -DVERSION=\"${VERSION}\" +CFLAGS = -std=c99 -pedantic -Wall -Wextra -Wno-unused-parameter -Os +LDFLAGS = -L$(X11LIB) -s +# OpenBSD: add -lsndio +# FreeBSD: add -lkvm -lsndio +LDLIBS = -lX11 + +# compiler and linker +CC = cc diff --git a/slstatus.1 b/slstatus.1 new file mode 100644 index 0000000..73e7a60 --- /dev/null +++ b/slstatus.1 @@ -0,0 +1,47 @@ +.Dd 2023-04-23 +.Dt SLSTATUS 1 +.Os +.Sh NAME +.Nm slstatus +.Nd suckless status +.Sh SYNOPSIS +.Nm +.Op Fl s +.Op Fl 1 +.Sh DESCRIPTION +.Nm +is a small tool for providing system status information to other programs +over the EWMH +.Em WM_NAME +property of the root window (used by +.Xr dwm 1 ) or standard input/output. It is designed to be as efficient as possible by +only issuing the minimum of system calls required. +.P +By default, +.Nm +outputs to WM_NAME. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl v +Print version information to stderr, then exit. +.It Fl s +Write to stdout instead of WM_NAME. +.It Fl 1 +Write once to stdout and quit. +.El +.Sh CUSTOMIZATION +.Nm +can be customized by creating a custom config.h and (re)compiling the source +code. This keeps it fast, secure and simple. +.Sh SIGNALS +.Nm +responds to the following signals: +.Pp +.Bl -tag -width TERM -compact +.It USR1 +Triggers an instant redraw. +.El +.Sh AUTHORS +See the LICENSE file for the authors. +.Sh SEE ALSO +.Xr dwm 1 diff --git a/slstatus.c b/slstatus.c new file mode 100644 index 0000000..16d88fe --- /dev/null +++ b/slstatus.c @@ -0,0 +1,135 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include + +#include "arg.h" +#include "slstatus.h" +#include "util.h" + +struct arg { + const char *(*func)(const char *); + const char *fmt; + const char *args; +}; + +char buf[1024]; +static volatile sig_atomic_t done; +static Display *dpy; + +#include "config.h" + +static void +terminate(const int signo) +{ + if (signo != SIGUSR1) + done = 1; +} + +static void +difftimespec(struct timespec *res, struct timespec *a, struct timespec *b) +{ + res->tv_sec = a->tv_sec - b->tv_sec - (a->tv_nsec < b->tv_nsec); + res->tv_nsec = a->tv_nsec - b->tv_nsec + + (a->tv_nsec < b->tv_nsec) * 1E9; +} + +static void +usage(void) +{ + die("usage: %s [-v] [-s] [-1]", argv0); +} + +int +main(int argc, char *argv[]) +{ + struct sigaction act; + struct timespec start, current, diff, intspec, wait; + size_t i, len; + int sflag, ret; + char status[MAXLEN]; + const char *res; + + sflag = 0; + ARGBEGIN { + case 'v': + die("slstatus-"VERSION); + break; + case '1': + done = 1; + /* FALLTHROUGH */ + case 's': + sflag = 1; + break; + default: + usage(); + } ARGEND + + if (argc) + usage(); + + memset(&act, 0, sizeof(act)); + act.sa_handler = terminate; + sigaction(SIGINT, &act, NULL); + sigaction(SIGTERM, &act, NULL); + act.sa_flags |= SA_RESTART; + sigaction(SIGUSR1, &act, NULL); + + if (!sflag && !(dpy = XOpenDisplay(NULL))) + die("XOpenDisplay: Failed to open display"); + + do { + if (clock_gettime(CLOCK_MONOTONIC, &start) < 0) + die("clock_gettime:"); + + status[0] = '\0'; + for (i = len = 0; i < LEN(args); i++) { + if (!(res = args[i].func(args[i].args))) + res = unknown_str; + + if ((ret = esnprintf(status + len, sizeof(status) - len, + args[i].fmt, res)) < 0) + break; + + len += ret; + } + + if (sflag) { + puts(status); + fflush(stdout); + if (ferror(stdout)) + die("puts:"); + } else { + if (XStoreName(dpy, DefaultRootWindow(dpy), status) < 0) + die("XStoreName: Allocation failed"); + XFlush(dpy); + } + + if (!done) { + if (clock_gettime(CLOCK_MONOTONIC, ¤t) < 0) + die("clock_gettime:"); + difftimespec(&diff, ¤t, &start); + + intspec.tv_sec = interval / 1000; + intspec.tv_nsec = (interval % 1000) * 1E6; + difftimespec(&wait, &intspec, &diff); + + if (wait.tv_sec >= 0 && + nanosleep(&wait, NULL) < 0 && + errno != EINTR) + die("nanosleep:"); + } + } while (!done); + + if (!sflag) { + XStoreName(dpy, DefaultRootWindow(dpy), NULL); + if (XCloseDisplay(dpy) < 0) + die("XCloseDisplay: Failed to close display"); + } + + return 0; +} diff --git a/slstatus.h b/slstatus.h new file mode 100644 index 0000000..394281c --- /dev/null +++ b/slstatus.h @@ -0,0 +1,85 @@ +/* See LICENSE file for copyright and license details. */ + +/* battery */ +const char *battery_perc(const char *); +const char *battery_remaining(const char *); +const char *battery_state(const char *); + +/* cat */ +const char *cat(const char *path); + +/* cpu */ +const char *cpu_freq(const char *unused); +const char *cpu_perc(const char *unused); + +/* datetime */ +const char *datetime(const char *fmt); + +/* disk */ +const char *disk_free(const char *path); +const char *disk_perc(const char *path); +const char *disk_total(const char *path); +const char *disk_used(const char *path); + +/* entropy */ +const char *entropy(const char *unused); + +/* hostname */ +const char *hostname(const char *unused); + +/* ip */ +const char *ipv4(const char *interface); +const char *ipv6(const char *interface); +const char *up(const char *interface); + +/* kernel_release */ +const char *kernel_release(const char *unused); + +/* keyboard_indicators */ +const char *keyboard_indicators(const char *fmt); + +/* keymap */ +const char *keymap(const char *unused); + +/* load_avg */ +const char *load_avg(const char *unused); + +/* netspeeds */ +const char *netspeed_rx(const char *interface); +const char *netspeed_tx(const char *interface); + +/* num_files */ +const char *num_files(const char *path); + +/* ram */ +const char *ram_free(const char *unused); +const char *ram_perc(const char *unused); +const char *ram_total(const char *unused); +const char *ram_used(const char *unused); + +/* run_command */ +const char *run_command(const char *cmd); + +/* swap */ +const char *swap_free(const char *unused); +const char *swap_perc(const char *unused); +const char *swap_total(const char *unused); +const char *swap_used(const char *unused); + +/* temperature */ +const char *temp(const char *); + +/* uptime */ +const char *uptime(const char *unused); + +/* user */ +const char *gid(const char *unused); +const char *uid(const char *unused); +const char *username(const char *unused); + +/* volume */ +const char *vol_perc(const char *card); + +/* wifi */ +const char *wifi_essid(const char *interface); +const char *wifi_perc(const char *interface); diff --git a/util.c b/util.c new file mode 100644 index 0000000..d33cf9d --- /dev/null +++ b/util.c @@ -0,0 +1,158 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include + +#include "util.h" + +char *argv0; + +static void +verr(const char *fmt, va_list ap) +{ + vfprintf(stderr, fmt, ap); + + if (fmt[0] && fmt[strlen(fmt) - 1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } +} + +void +warn(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + verr(fmt, ap); + va_end(ap); +} + +void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + verr(fmt, ap); + va_end(ap); + + exit(1); +} + +static int +evsnprintf(char *str, size_t size, const char *fmt, va_list ap) +{ + int ret; + + ret = vsnprintf(str, size, fmt, ap); + + if (ret < 0) { + warn("vsnprintf:"); + return -1; + } else if ((size_t)ret >= size) { + warn("vsnprintf: Output truncated"); + return -1; + } + + return ret; +} + +int +esnprintf(char *str, size_t size, const char *fmt, ...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = evsnprintf(str, size, fmt, ap); + va_end(ap); + + return ret; +} + +const char * +bprintf(const char *fmt, ...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = evsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + return (ret < 0) ? NULL : buf; +} + +const char * +fmt_human(uintmax_t num, int base) +{ + double scaled; + size_t i, prefixlen; + const char **prefix; + const char *prefix_1000[] = { "", "k", "M", "G", "T", "P", "E", "Z", + "Y" }; + const char *prefix_1024[] = { "", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", + "Zi", "Yi" }; + + switch (base) { + case 1000: + prefix = prefix_1000; + prefixlen = LEN(prefix_1000); + break; + case 1024: + prefix = prefix_1024; + prefixlen = LEN(prefix_1024); + break; + default: + warn("fmt_human: Invalid base"); + return NULL; + } + + scaled = num; + for (i = 0; i < prefixlen && scaled >= base; i++) + scaled /= base; + + return bprintf("%.1f %s", scaled, prefix[i]); +} + +int +pscanf(const char *path, const char *fmt, ...) +{ + FILE *fp; + va_list ap; + int n; + + if (!(fp = fopen(path, "r"))) { + warn("fopen '%s':", path); + return -1; + } + va_start(ap, fmt); + n = vfscanf(fp, fmt, ap); + va_end(ap); + fclose(fp); + + return (n == EOF) ? -1 : n; +} + +int +lscanf(FILE *fp, const char *key, const char *fmt, void *res) +{ + int n; + char line[256]; + + n = -1; + while (fgets(line, sizeof(line), fp)) + if (strncmp(line, key, strlen(key)) == 0) { + n = sscanf(line + strlen(key), fmt, res); + break; + } + + rewind(fp); + return (n == 1) ? 1 : -1; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..7a960aa --- /dev/null +++ b/util.h @@ -0,0 +1,18 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +extern char buf[1024]; + +#define LEN(x) (sizeof(x) / sizeof((x)[0])) + +extern char *argv0; + +void warn(const char *, ...); +void die(const char *, ...); + +int esnprintf(char *str, size_t size, const char *fmt, ...); +const char *bprintf(const char *fmt, ...); +const char *fmt_human(uintmax_t num, int base); +int pscanf(const char *path, const char *fmt, ...); +int lscanf(FILE *fp, const char *key, const char *fmt, void *res); From c888bafafb28d6bebfca75ea446d24d0cb736749 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 5/7] Squashed 'suckless/dmenu/' content from commit 7175c48 git-subtree-dir: suckless/dmenu git-subtree-split: 7175c4880bac3d2a2d4a6262b59193f0a38e2fdb --- LICENSE | 30 ++ Makefile | 58 ++++ README | 24 ++ arg.h | 49 ++++ config.def.h | 23 ++ config.mk | 32 +++ dmenu.1 | 204 +++++++++++++ dmenu.c | 799 +++++++++++++++++++++++++++++++++++++++++++++++++++ dmenu_path | 13 + dmenu_run | 2 + drw.c | 471 ++++++++++++++++++++++++++++++ drw.h | 60 ++++ stest.1 | 90 ++++++ stest.c | 109 +++++++ util.c | 37 +++ util.h | 9 + 16 files changed, 2010 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 arg.h create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 dmenu.1 create mode 100644 dmenu.c create mode 100755 dmenu_path create mode 100755 dmenu_run create mode 100644 drw.c create mode 100644 drw.h create mode 100644 stest.1 create mode 100644 stest.c create mode 100644 util.c create mode 100644 util.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2a64b28 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +MIT/X Consortium License + +© 2006-2019 Anselm R Garbe +© 2006-2008 Sander van Dijk +© 2006-2007 Michał Janeczek +© 2007 Kris Maglione +© 2009 Gottox +© 2009 Markus Schnalke +© 2009 Evan Gates +© 2010-2012 Connor Lane Smith +© 2014-2022 Hiltjo Posthuma +© 2015-2019 Quentin Rameau + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..458c524 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README b/README new file mode 100644 index 0000000..a8fcdfe --- /dev/null +++ b/README @@ -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. diff --git a/arg.h b/arg.h new file mode 100644 index 0000000..e94e02b --- /dev/null +++ b/arg.h @@ -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 diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..1edb647 --- /dev/null +++ b/config.def.h @@ -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[] = " "; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..dcc5bb3 --- /dev/null +++ b/config.mk @@ -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 diff --git a/dmenu.1 b/dmenu.1 new file mode 100644 index 0000000..ed0e442 --- /dev/null +++ b/dmenu.1 @@ -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) diff --git a/dmenu.c b/dmenu.c new file mode 100644 index 0000000..363d19f --- /dev/null +++ b/dmenu.c @@ -0,0 +1,799 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#ifdef XINERAMA +#include +#endif +#include + +#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 */ +} diff --git a/dmenu_path b/dmenu_path new file mode 100755 index 0000000..3a7cda7 --- /dev/null +++ b/dmenu_path @@ -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 diff --git a/dmenu_run b/dmenu_run new file mode 100755 index 0000000..834ede5 --- /dev/null +++ b/dmenu_run @@ -0,0 +1,2 @@ +#!/bin/sh +dmenu_path | dmenu "$@" | ${SHELL:-"/bin/sh"} & diff --git a/drw.c b/drw.c new file mode 100644 index 0000000..9fdd1a4 --- /dev/null +++ b/drw.c @@ -0,0 +1,471 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#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[] = "�"; + + 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); +} diff --git a/drw.h b/drw.h new file mode 100644 index 0000000..2ed77be --- /dev/null +++ b/drw.h @@ -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); diff --git a/stest.1 b/stest.1 new file mode 100644 index 0000000..2667d8a --- /dev/null +++ b/stest.1 @@ -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) diff --git a/stest.c b/stest.c new file mode 100644 index 0000000..e27d3a5 --- /dev/null +++ b/stest.c @@ -0,0 +1,109 @@ +/* See LICENSE file for copyright and license details. */ +#include + +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/util.c b/util.c new file mode 100644 index 0000000..8e26a51 --- /dev/null +++ b/util.c @@ -0,0 +1,37 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#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; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..c0a50d4 --- /dev/null +++ b/util.h @@ -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); From 7dc635497efafbd87673671be79735f124ad5680 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 6/7] Squashed 'suckless/st/' content from commit 688f70a git-subtree-dir: suckless/st git-subtree-split: 688f70add0d1da8a416bf7df763328d694a24a3a --- FAQ | 253 +++++ LEGACY | 17 + LICENSE | 34 + Makefile | 51 + README | 34 + TODO | 28 + arg.h | 50 + config.def.h | 474 +++++++++ config.mk | 36 + st.1 | 177 ++++ st.c | 2705 ++++++++++++++++++++++++++++++++++++++++++++++++++ st.h | 126 +++ st.info | 243 +++++ win.h | 41 + x.c | 2108 +++++++++++++++++++++++++++++++++++++++ 15 files changed, 6377 insertions(+) create mode 100644 FAQ create mode 100644 LEGACY create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 arg.h create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 st.1 create mode 100644 st.c create mode 100644 st.h create mode 100644 st.info create mode 100644 win.h create mode 100644 x.c diff --git a/FAQ b/FAQ new file mode 100644 index 0000000..6287a27 --- /dev/null +++ b/FAQ @@ -0,0 +1,253 @@ +## Why does st not handle utmp entries? + +Use the excellent tool of [utmp](https://git.suckless.org/utmp/) for this task. + + +## Some _random program_ complains that st is unknown/not recognised/unsupported/whatever! + +It means that st doesn’t have any terminfo entry on your system. Chances are +you did not `make install`. If you just want to test it without installing it, +you can manually run `tic -sx st.info`. + + +## Nothing works, and nothing is said about an unknown terminal! + +* Some programs just assume they’re running in xterm i.e. they don’t rely on + terminfo. What you see is the current state of the “xterm compliance”. +* Some programs don’t complain about the lacking st description and default to + another terminal. In that case see the question about terminfo. + + +## How do I scroll back up? + +* Using a terminal multiplexer. + * `st -e tmux` using C-b [ + * `st -e screen` using C-a ESC +* Using the excellent tool of [scroll](https://git.suckless.org/scroll/). +* Using the scrollback [patch](https://st.suckless.org/patches/scrollback/). + + +## I would like to have utmp and/or scroll functionality by default + +You can add the absolute path of both programs in your config.h file. You only +have to modify the value of utmp and scroll variables. + + +## Why doesn't the Del key work in some programs? + +Taken from the terminfo manpage: + + If the terminal has a keypad that transmits codes when the keys + are pressed, this information can be given. Note that it is not + possible to handle terminals where the keypad only works in + local (this applies, for example, to the unshifted HP 2621 keys). + If the keypad can be set to transmit or not transmit, give these + codes as smkx and rmkx. Otherwise the keypad is assumed to + always transmit. + +In the st case smkx=E[?1hE= and rmkx=E[?1lE>, so it is mandatory that +applications which want to test against keypad keys send these +sequences. + +But buggy applications (like bash and irssi, for example) don't do this. A fast +solution for them is to use the following command: + + $ printf '\033[?1h\033=' >/dev/tty + +or + $ tput smkx + +In the case of bash, readline is used. Readline has a different note in its +manpage about this issue: + + enable-keypad (Off) + When set to On, readline will try to enable the + application keypad when it is called. Some systems + need this to enable arrow keys. + +Adding this option to your .inputrc will fix the keypad problem for all +applications using readline. + +If you are using zsh, then read the zsh FAQ +: + + It should be noted that the O / [ confusion can occur with other keys + such as Home and End. Some systems let you query the key sequences + sent by these keys from the system's terminal database, terminfo. + Unfortunately, the key sequences given there typically apply to the + mode that is not the one zsh uses by default (it's the "application" + mode rather than the "raw" mode). Explaining the use of terminfo is + outside of the scope of this FAQ, but if you wish to use the key + sequences given there you can tell the line editor to turn on + "application" mode when it starts and turn it off when it stops: + + function zle-line-init () { echoti smkx } + function zle-line-finish () { echoti rmkx } + zle -N zle-line-init + zle -N zle-line-finish + +Putting these lines into your .zshrc will fix the problems. + + +## How can I use meta in 8bit mode? + +St supports meta in 8bit mode, but the default terminfo entry doesn't +use this capability. If you want it, you have to use the 'st-meta' value +in TERM. + + +## I cannot compile st in OpenBSD + +OpenBSD lacks librt, despite it being mandatory in POSIX +. +If you want to compile st for OpenBSD you have to remove -lrt from config.mk, and +st will compile without any loss of functionality, because all the functions are +included in libc on this platform. + + +## The Backspace Case + +St is emulating the Linux way of handling backspace being delete and delete being +backspace. + +This is an issue that was discussed in suckless mailing list +. Here is why some old grumpy +terminal users wants its backspace to be how he feels it: + + Well, I am going to comment why I want to change the behaviour + of this key. When ASCII was defined in 1968, communication + with computers was done using punched cards, or hardcopy + terminals (basically a typewriter machine connected with the + computer using a serial port). ASCII defines DELETE as 7F, + because, in punched-card terms, it means all the holes of the + card punched; it is thus a kind of 'physical delete'. In the + same way, the BACKSPACE key was a non-destructive backspace, + as on a typewriter. So, if you wanted to delete a character, + you had to BACKSPACE and then DELETE. Another use of BACKSPACE + was to type accented characters, for example 'a BACKSPACE `'. + The VT100 had no BACKSPACE key; it was generated using the + CONTROL key as another control character (CONTROL key sets to + 0 b7 b6 b5, so it converts H (code 0x48) into BACKSPACE (code + 0x08)), but it had a DELETE key in a similar position where + the BACKSPACE key is located today on common PC keyboards. + All the terminal emulators emulated the difference between + these keys correctly: the backspace key generated a BACKSPACE + (^H) and delete key generated a DELETE (^?). + + But a problem arose when Linus Torvalds wrote Linux. Unlike + earlier terminals, the Linux virtual terminal (the terminal + emulator integrated in the kernel) returned a DELETE when + backspace was pressed, due to the VT100 having a DELETE key in + the same position. This created a lot of problems (see [1] + and [2]). Since Linux has become the king, a lot of terminal + emulators today generate a DELETE when the backspace key is + pressed in order to avoid problems with Linux. The result is + that the only way of generating a BACKSPACE on these systems + is by using CONTROL + H. (I also think that emacs had an + important point here because the CONTROL + H prefix is used + in emacs in some commands (help commands).) + + From point of view of the kernel, you can change the key + for deleting a previous character with stty erase. When you + connect a real terminal into a machine you describe the type + of terminal, so getty configures the correct value of stty + erase for this terminal. In the case of terminal emulators, + however, you don't have any getty that can set the correct + value of stty erase, so you always get the default value. + For this reason, it is necessary to add 'stty erase ^H' to your + profile if you have changed the value of the backspace key. + Of course, another solution is for st itself to modify the + value of stty erase. I usually have the inverse problem: + when I connect to non-Unix machines, I have to press CONTROL + + h to get a BACKSPACE. The inverse problem occurs when a user + connects to my Unix machines from a different system with a + correct backspace key. + + [1] http://www.ibb.net/~anne/keyboard.html + [2] http://www.tldp.org/HOWTO/Keyboard-and-Console-HOWTO-5.html + + +## But I really want the old grumpy behaviour of my terminal + +Apply [1]. + +[1] https://st.suckless.org/patches/delkey + + +## Why do images not work in st using the w3m image hack? + +w3mimg uses a hack that draws an image on top of the terminal emulator Drawable +window. The hack relies on the terminal to use a single buffer to draw its +contents directly. + +st uses double-buffered drawing so the image is quickly replaced and may show a +short flicker effect. + +Below is a patch example to change st double-buffering to a single Drawable +buffer. + +diff --git a/x.c b/x.c +--- a/x.c ++++ b/x.c +@@ -732,10 +732,6 @@ xresize(int col, int row) + win.tw = col * win.cw; + win.th = row * win.ch; + +- XFreePixmap(xw.dpy, xw.buf); +- xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, +- DefaultDepth(xw.dpy, xw.scr)); +- XftDrawChange(xw.draw, xw.buf); + xclear(0, 0, win.w, win.h); + + /* resize to new width */ +@@ -1148,8 +1144,7 @@ xinit(int cols, int rows) + gcvalues.graphics_exposures = False; + dc.gc = XCreateGC(xw.dpy, parent, GCGraphicsExposures, + &gcvalues); +- xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, +- DefaultDepth(xw.dpy, xw.scr)); ++ xw.buf = xw.win; + XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); + XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); + +@@ -1632,8 +1627,6 @@ xdrawline(Line line, int x1, int y1, int x2) + void + xfinishdraw(void) + { +- XCopyArea(xw.dpy, xw.buf, xw.win, dc.gc, 0, 0, win.w, +- win.h, 0, 0); + XSetForeground(xw.dpy, dc.gc, + dc.col[IS_SET(MODE_REVERSE)? + defaultfg : defaultbg].pixel); + + +## BadLength X error in Xft when trying to render emoji + +Xft makes st crash when rendering color emojis with the following error: + +"X Error of failed request: BadLength (poly request too large or internal Xlib length error)" + Major opcode of failed request: 139 (RENDER) + Minor opcode of failed request: 20 (RenderAddGlyphs) + Serial number of failed request: 1595 + Current serial number in output stream: 1818" + +This is a known bug in Xft (not st) which happens on some platforms and +combination of particular fonts and fontconfig settings. + +See also: +https://gitlab.freedesktop.org/xorg/lib/libxft/issues/6 +https://bugs.freedesktop.org/show_bug.cgi?id=107534 +https://bugzilla.redhat.com/show_bug.cgi?id=1498269 + +The solution is to remove color emoji fonts or disable this in the fontconfig +XML configuration. As an ugly workaround (which may work only on newer +fontconfig versions (FC_COLOR)), the following code can be used to mask color +fonts: + + FcPatternAddBool(fcpattern, FC_COLOR, FcFalse); + +Please don't bother reporting this bug to st, but notify the upstream Xft +developers about fixing this bug. + +As of 2022-09-05 this now seems to be finally fixed in libXft 2.3.5: +https://gitlab.freedesktop.org/xorg/lib/libxft/-/blob/libXft-2.3.5/NEWS diff --git a/LEGACY b/LEGACY new file mode 100644 index 0000000..bf28b1e --- /dev/null +++ b/LEGACY @@ -0,0 +1,17 @@ +A STATEMENT ON LEGACY SUPPORT + +In the terminal world there is much cruft that comes from old and unsup‐ +ported terminals that inherit incompatible modes and escape sequences +which noone is able to know, except when he/she comes from that time and +developed a graphical vt100 emulator at that time. + +One goal of st is to only support what is really needed. When you en‐ +counter a sequence which you really need, implement it. But while you +are at it, do not add the other cruft you might encounter while sneek‐ +ing at other terminal emulators. History has bloated them and there is +no real evidence that most of the sequences are used today. + + +Christoph Lohmann <20h@r-36.net> +2012-09-13T07:00:36.081271045+02:00 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3cbf420 --- /dev/null +++ b/LICENSE @@ -0,0 +1,34 @@ +MIT/X Consortium License + +© 2014-2022 Hiltjo Posthuma +© 2018 Devin J. Pohly +© 2014-2017 Quentin Rameau +© 2009-2012 Aurélien APTEL +© 2008-2017 Anselm R Garbe +© 2012-2017 Roberto E. Vargas Caballero +© 2012-2016 Christoph Lohmann <20h at r-36 dot net> +© 2013 Eon S. Jeon +© 2013 Alexander Sedov +© 2013 Mark Edgar +© 2013-2014 Eric Pruitt +© 2013 Michael Forney +© 2013-2014 Markus Teich +© 2014-2015 Laslo Hunhold + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..15db421 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +# st - simple terminal +# See LICENSE file for copyright and license details. +.POSIX: + +include config.mk + +SRC = st.c x.c +OBJ = $(SRC:.c=.o) + +all: st + +config.h: + cp config.def.h config.h + +.c.o: + $(CC) $(STCFLAGS) -c $< + +st.o: config.h st.h win.h +x.o: arg.h config.h st.h win.h + +$(OBJ): config.h config.mk + +st: $(OBJ) + $(CC) -o $@ $(OBJ) $(STLDFLAGS) + +clean: + rm -f st $(OBJ) st-$(VERSION).tar.gz + +dist: clean + mkdir -p st-$(VERSION) + cp -R FAQ LEGACY TODO LICENSE Makefile README config.mk\ + config.def.h st.info st.1 arg.h st.h win.h $(SRC)\ + st-$(VERSION) + tar -cf - st-$(VERSION) | gzip > st-$(VERSION).tar.gz + rm -rf st-$(VERSION) + +install: st + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f st $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/st + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + sed "s/VERSION/$(VERSION)/g" < st.1 > $(DESTDIR)$(MANPREFIX)/man1/st.1 + chmod 644 $(DESTDIR)$(MANPREFIX)/man1/st.1 + tic -sx st.info + @echo Please see the README file regarding the terminfo entry of st. + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/st + rm -f $(DESTDIR)$(MANPREFIX)/man1/st.1 + +.PHONY: all clean dist install uninstall diff --git a/README b/README new file mode 100644 index 0000000..6a846ed --- /dev/null +++ b/README @@ -0,0 +1,34 @@ +st - simple terminal +-------------------- +st is a simple terminal emulator for X which sucks less. + + +Requirements +------------ +In order to build st you need the Xlib header files. + + +Installation +------------ +Edit config.mk to match your local setup (st is installed into +the /usr/local namespace by default). + +Afterwards enter the following command to build and install st (if +necessary as root): + + make clean install + + +Running st +---------- +If you did not install st with make clean install, you must compile +the st terminfo entry with the following command: + + tic -sx st.info + +See the man page for additional details. + +Credits +------- +Based on Aurélien APTEL bt source code. + diff --git a/TODO b/TODO new file mode 100644 index 0000000..5f74cd5 --- /dev/null +++ b/TODO @@ -0,0 +1,28 @@ +vt emulation +------------ + +* double-height support + +code & interface +---------------- + +* add a simple way to do multiplexing + +drawing +------- +* add diacritics support to xdraws() + * switch to a suckless font drawing library +* make the font cache simpler +* add better support for brightening of the upper colors + +bugs +---- + +* fix shift up/down (shift selection in emacs) +* remove DEC test sequence when appropriate + +misc +---- + + $ grep -nE 'XXX|TODO' st.c + diff --git a/arg.h b/arg.h new file mode 100644 index 0000000..a22e019 --- /dev/null +++ b/arg.h @@ -0,0 +1,50 @@ +/* + * 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;\ + }\ + int i_;\ + for (i_ = 1, brk_ = 0, argv_ = argv;\ + argv[0][i_] && !brk_;\ + i_++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][i_];\ + switch (argc_) + +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][i_+1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][i_+1] != '\0')?\ + (&argv[0][i_+1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][i_+1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][i_+1] != '\0')?\ + (&argv[0][i_+1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..2cd740a --- /dev/null +++ b/config.def.h @@ -0,0 +1,474 @@ +/* See LICENSE file for copyright and license details. */ + +/* + * appearance + * + * font: see http://freedesktop.org/software/fontconfig/fontconfig-user.html + */ +static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true"; +static int borderpx = 2; + +/* + * What program is execed by st depends of these precedence rules: + * 1: program passed with -e + * 2: scroll and/or utmp + * 3: SHELL environment variable + * 4: value of shell in /etc/passwd + * 5: value of shell in config.h + */ +static char *shell = "/bin/sh"; +char *utmp = NULL; +/* scroll program: to enable use a string like "scroll" */ +char *scroll = NULL; +char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400"; + +/* identification sequence returned in DA and DECID */ +char *vtiden = "\033[?6c"; + +/* Kerning / character bounding-box multipliers */ +static float cwscale = 1.0; +static float chscale = 1.0; + +/* + * word delimiter string + * + * More advanced example: L" `'\"()[]{}" + */ +wchar_t *worddelimiters = L" "; + +/* selection timeouts (in milliseconds) */ +static unsigned int doubleclicktimeout = 300; +static unsigned int tripleclicktimeout = 600; + +/* alt screens */ +int allowaltscreen = 1; + +/* allow certain non-interactive (insecure) window operations such as: + setting the clipboard text */ +int allowwindowops = 0; + +/* + * draw latency range in ms - from new content/keypress/etc until drawing. + * within this range, st draws when content stops arriving (idle). mostly it's + * near minlatency, but it waits longer for slow updates to avoid partial draw. + * low minlatency will tear/flicker more, as it can "detect" idle too early. + */ +static double minlatency = 2; +static double maxlatency = 33; + +/* + * blinking timeout (set to 0 to disable blinking) for the terminal blinking + * attribute. + */ +static unsigned int blinktimeout = 800; + +/* + * thickness of underline and bar cursors + */ +static unsigned int cursorthickness = 2; + +/* + * bell volume. It must be a value between -100 and 100. Use 0 for disabling + * it + */ +static int bellvolume = 0; + +/* default TERM value */ +char *termname = "st-256color"; + +/* + * spaces per tab + * + * When you are changing this value, don't forget to adapt the »it« value in + * the st.info and appropriately install the st.info in the environment where + * you use this st version. + * + * it#$tabspaces, + * + * Secondly make sure your kernel is not expanding tabs. When running `stty + * -a` »tab0« should appear. You can tell the terminal to not expand tabs by + * running following command: + * + * stty tabs + */ +unsigned int tabspaces = 8; + +/* Terminal colors (16 first used in escape sequence) */ +static const char *colorname[] = { + /* 8 normal colors */ + "black", + "red3", + "green3", + "yellow3", + "blue2", + "magenta3", + "cyan3", + "gray90", + + /* 8 bright colors */ + "gray50", + "red", + "green", + "yellow", + "#5c5cff", + "magenta", + "cyan", + "white", + + [255] = 0, + + /* more colors can be added after 255 to use with DefaultXX */ + "#cccccc", + "#555555", + "gray90", /* default foreground colour */ + "black", /* default background colour */ +}; + + +/* + * Default colors (colorname index) + * foreground, background, cursor, reverse cursor + */ +unsigned int defaultfg = 258; +unsigned int defaultbg = 259; +unsigned int defaultcs = 256; +static unsigned int defaultrcs = 257; + +/* + * Default shape of cursor + * 2: Block ("█") + * 4: Underline ("_") + * 6: Bar ("|") + * 7: Snowman ("☃") + */ +static unsigned int cursorshape = 2; + +/* + * Default columns and rows numbers + */ + +static unsigned int cols = 80; +static unsigned int rows = 24; + +/* + * Default colour and shape of the mouse cursor + */ +static unsigned int mouseshape = XC_xterm; +static unsigned int mousefg = 7; +static unsigned int mousebg = 0; + +/* + * Color used to display font attributes when fontconfig selected a font which + * doesn't match the ones requested. + */ +static unsigned int defaultattr = 11; + +/* + * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set). + * Note that if you want to use ShiftMask with selmasks, set this to an other + * modifier, set to 0 to not use it. + */ +static uint forcemousemod = ShiftMask; + +/* + * Internal mouse shortcuts. + * Beware that overloading Button1 will disable the selection. + */ +static MouseShortcut mshortcuts[] = { + /* mask button function argument release */ + { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 }, + { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} }, + { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} }, + { ShiftMask, Button5, ttysend, {.s = "\033[6;2~"} }, + { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} }, +}; + +/* Internal keyboard shortcuts. */ +#define MODKEY Mod1Mask +#define TERMMOD (ControlMask|ShiftMask) + +static Shortcut shortcuts[] = { + /* mask keysym function argument */ + { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, + { ControlMask, XK_Print, toggleprinter, {.i = 0} }, + { ShiftMask, XK_Print, printscreen, {.i = 0} }, + { XK_ANY_MOD, XK_Print, printsel, {.i = 0} }, + { TERMMOD, XK_Prior, zoom, {.f = +1} }, + { TERMMOD, XK_Next, zoom, {.f = -1} }, + { TERMMOD, XK_Home, zoomreset, {.f = 0} }, + { TERMMOD, XK_C, clipcopy, {.i = 0} }, + { TERMMOD, XK_V, clippaste, {.i = 0} }, + { TERMMOD, XK_Y, selpaste, {.i = 0} }, + { ShiftMask, XK_Insert, selpaste, {.i = 0} }, + { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, +}; + +/* + * Special keys (change & recompile st.info accordingly) + * + * Mask value: + * * Use XK_ANY_MOD to match the key no matter modifiers state + * * Use XK_NO_MOD to match the key alone (no modifiers) + * appkey value: + * * 0: no value + * * > 0: keypad application mode enabled + * * = 2: term.numlock = 1 + * * < 0: keypad application mode disabled + * appcursor value: + * * 0: no value + * * > 0: cursor application mode enabled + * * < 0: cursor application mode disabled + * + * Be careful with the order of the definitions because st searches in + * this table sequentially, so any XK_ANY_MOD must be in the last + * position for a key. + */ + +/* + * If you want keys other than the X11 function keys (0xFD00 - 0xFFFF) + * to be mapped below, add them to this array. + */ +static KeySym mappedkeys[] = { -1 }; + +/* + * State bits to ignore when matching key or button events. By default, + * numlock (Mod2Mask) and keyboard layout (XK_SWITCH_MOD) are ignored. + */ +static uint ignoremod = Mod2Mask|XK_SWITCH_MOD; + +/* + * This is the huge key array which defines all compatibility to the Linux + * world. Please decide about changes wisely. + */ +static Key key[] = { + /* keysym mask string appkey appcursor */ + { XK_KP_Home, ShiftMask, "\033[2J", 0, -1}, + { XK_KP_Home, ShiftMask, "\033[1;2H", 0, +1}, + { XK_KP_Home, XK_ANY_MOD, "\033[H", 0, -1}, + { XK_KP_Home, XK_ANY_MOD, "\033[1~", 0, +1}, + { XK_KP_Up, XK_ANY_MOD, "\033Ox", +1, 0}, + { XK_KP_Up, XK_ANY_MOD, "\033[A", 0, -1}, + { XK_KP_Up, XK_ANY_MOD, "\033OA", 0, +1}, + { XK_KP_Down, XK_ANY_MOD, "\033Or", +1, 0}, + { XK_KP_Down, XK_ANY_MOD, "\033[B", 0, -1}, + { XK_KP_Down, XK_ANY_MOD, "\033OB", 0, +1}, + { XK_KP_Left, XK_ANY_MOD, "\033Ot", +1, 0}, + { XK_KP_Left, XK_ANY_MOD, "\033[D", 0, -1}, + { XK_KP_Left, XK_ANY_MOD, "\033OD", 0, +1}, + { XK_KP_Right, XK_ANY_MOD, "\033Ov", +1, 0}, + { XK_KP_Right, XK_ANY_MOD, "\033[C", 0, -1}, + { XK_KP_Right, XK_ANY_MOD, "\033OC", 0, +1}, + { XK_KP_Prior, ShiftMask, "\033[5;2~", 0, 0}, + { XK_KP_Prior, XK_ANY_MOD, "\033[5~", 0, 0}, + { XK_KP_Begin, XK_ANY_MOD, "\033[E", 0, 0}, + { XK_KP_End, ControlMask, "\033[J", -1, 0}, + { XK_KP_End, ControlMask, "\033[1;5F", +1, 0}, + { XK_KP_End, ShiftMask, "\033[K", -1, 0}, + { XK_KP_End, ShiftMask, "\033[1;2F", +1, 0}, + { XK_KP_End, XK_ANY_MOD, "\033[4~", 0, 0}, + { XK_KP_Next, ShiftMask, "\033[6;2~", 0, 0}, + { XK_KP_Next, XK_ANY_MOD, "\033[6~", 0, 0}, + { XK_KP_Insert, ShiftMask, "\033[2;2~", +1, 0}, + { XK_KP_Insert, ShiftMask, "\033[4l", -1, 0}, + { XK_KP_Insert, ControlMask, "\033[L", -1, 0}, + { XK_KP_Insert, ControlMask, "\033[2;5~", +1, 0}, + { XK_KP_Insert, XK_ANY_MOD, "\033[4h", -1, 0}, + { XK_KP_Insert, XK_ANY_MOD, "\033[2~", +1, 0}, + { XK_KP_Delete, ControlMask, "\033[M", -1, 0}, + { XK_KP_Delete, ControlMask, "\033[3;5~", +1, 0}, + { XK_KP_Delete, ShiftMask, "\033[2K", -1, 0}, + { XK_KP_Delete, ShiftMask, "\033[3;2~", +1, 0}, + { XK_KP_Delete, XK_ANY_MOD, "\033[P", -1, 0}, + { XK_KP_Delete, XK_ANY_MOD, "\033[3~", +1, 0}, + { XK_KP_Multiply, XK_ANY_MOD, "\033Oj", +2, 0}, + { XK_KP_Add, XK_ANY_MOD, "\033Ok", +2, 0}, + { XK_KP_Enter, XK_ANY_MOD, "\033OM", +2, 0}, + { XK_KP_Enter, XK_ANY_MOD, "\r", -1, 0}, + { XK_KP_Subtract, XK_ANY_MOD, "\033Om", +2, 0}, + { XK_KP_Decimal, XK_ANY_MOD, "\033On", +2, 0}, + { XK_KP_Divide, XK_ANY_MOD, "\033Oo", +2, 0}, + { XK_KP_0, XK_ANY_MOD, "\033Op", +2, 0}, + { XK_KP_1, XK_ANY_MOD, "\033Oq", +2, 0}, + { XK_KP_2, XK_ANY_MOD, "\033Or", +2, 0}, + { XK_KP_3, XK_ANY_MOD, "\033Os", +2, 0}, + { XK_KP_4, XK_ANY_MOD, "\033Ot", +2, 0}, + { XK_KP_5, XK_ANY_MOD, "\033Ou", +2, 0}, + { XK_KP_6, XK_ANY_MOD, "\033Ov", +2, 0}, + { XK_KP_7, XK_ANY_MOD, "\033Ow", +2, 0}, + { XK_KP_8, XK_ANY_MOD, "\033Ox", +2, 0}, + { XK_KP_9, XK_ANY_MOD, "\033Oy", +2, 0}, + { XK_Up, ShiftMask, "\033[1;2A", 0, 0}, + { XK_Up, Mod1Mask, "\033[1;3A", 0, 0}, + { XK_Up, ShiftMask|Mod1Mask,"\033[1;4A", 0, 0}, + { XK_Up, ControlMask, "\033[1;5A", 0, 0}, + { XK_Up, ShiftMask|ControlMask,"\033[1;6A", 0, 0}, + { XK_Up, ControlMask|Mod1Mask,"\033[1;7A", 0, 0}, + { XK_Up,ShiftMask|ControlMask|Mod1Mask,"\033[1;8A", 0, 0}, + { XK_Up, XK_ANY_MOD, "\033[A", 0, -1}, + { XK_Up, XK_ANY_MOD, "\033OA", 0, +1}, + { XK_Down, ShiftMask, "\033[1;2B", 0, 0}, + { XK_Down, Mod1Mask, "\033[1;3B", 0, 0}, + { XK_Down, ShiftMask|Mod1Mask,"\033[1;4B", 0, 0}, + { XK_Down, ControlMask, "\033[1;5B", 0, 0}, + { XK_Down, ShiftMask|ControlMask,"\033[1;6B", 0, 0}, + { XK_Down, ControlMask|Mod1Mask,"\033[1;7B", 0, 0}, + { XK_Down,ShiftMask|ControlMask|Mod1Mask,"\033[1;8B",0, 0}, + { XK_Down, XK_ANY_MOD, "\033[B", 0, -1}, + { XK_Down, XK_ANY_MOD, "\033OB", 0, +1}, + { XK_Left, ShiftMask, "\033[1;2D", 0, 0}, + { XK_Left, Mod1Mask, "\033[1;3D", 0, 0}, + { XK_Left, ShiftMask|Mod1Mask,"\033[1;4D", 0, 0}, + { XK_Left, ControlMask, "\033[1;5D", 0, 0}, + { XK_Left, ShiftMask|ControlMask,"\033[1;6D", 0, 0}, + { XK_Left, ControlMask|Mod1Mask,"\033[1;7D", 0, 0}, + { XK_Left,ShiftMask|ControlMask|Mod1Mask,"\033[1;8D",0, 0}, + { XK_Left, XK_ANY_MOD, "\033[D", 0, -1}, + { XK_Left, XK_ANY_MOD, "\033OD", 0, +1}, + { XK_Right, ShiftMask, "\033[1;2C", 0, 0}, + { XK_Right, Mod1Mask, "\033[1;3C", 0, 0}, + { XK_Right, ShiftMask|Mod1Mask,"\033[1;4C", 0, 0}, + { XK_Right, ControlMask, "\033[1;5C", 0, 0}, + { XK_Right, ShiftMask|ControlMask,"\033[1;6C", 0, 0}, + { XK_Right, ControlMask|Mod1Mask,"\033[1;7C", 0, 0}, + { XK_Right,ShiftMask|ControlMask|Mod1Mask,"\033[1;8C",0, 0}, + { XK_Right, XK_ANY_MOD, "\033[C", 0, -1}, + { XK_Right, XK_ANY_MOD, "\033OC", 0, +1}, + { XK_ISO_Left_Tab, ShiftMask, "\033[Z", 0, 0}, + { XK_Return, Mod1Mask, "\033\r", 0, 0}, + { XK_Return, XK_ANY_MOD, "\r", 0, 0}, + { XK_Insert, ShiftMask, "\033[4l", -1, 0}, + { XK_Insert, ShiftMask, "\033[2;2~", +1, 0}, + { XK_Insert, ControlMask, "\033[L", -1, 0}, + { XK_Insert, ControlMask, "\033[2;5~", +1, 0}, + { XK_Insert, XK_ANY_MOD, "\033[4h", -1, 0}, + { XK_Insert, XK_ANY_MOD, "\033[2~", +1, 0}, + { XK_Delete, ControlMask, "\033[M", -1, 0}, + { XK_Delete, ControlMask, "\033[3;5~", +1, 0}, + { XK_Delete, ShiftMask, "\033[2K", -1, 0}, + { XK_Delete, ShiftMask, "\033[3;2~", +1, 0}, + { XK_Delete, XK_ANY_MOD, "\033[P", -1, 0}, + { XK_Delete, XK_ANY_MOD, "\033[3~", +1, 0}, + { XK_BackSpace, XK_NO_MOD, "\177", 0, 0}, + { XK_BackSpace, Mod1Mask, "\033\177", 0, 0}, + { XK_Home, ShiftMask, "\033[2J", 0, -1}, + { XK_Home, ShiftMask, "\033[1;2H", 0, +1}, + { XK_Home, XK_ANY_MOD, "\033[H", 0, -1}, + { XK_Home, XK_ANY_MOD, "\033[1~", 0, +1}, + { XK_End, ControlMask, "\033[J", -1, 0}, + { XK_End, ControlMask, "\033[1;5F", +1, 0}, + { XK_End, ShiftMask, "\033[K", -1, 0}, + { XK_End, ShiftMask, "\033[1;2F", +1, 0}, + { XK_End, XK_ANY_MOD, "\033[4~", 0, 0}, + { XK_Prior, ControlMask, "\033[5;5~", 0, 0}, + { XK_Prior, ShiftMask, "\033[5;2~", 0, 0}, + { XK_Prior, XK_ANY_MOD, "\033[5~", 0, 0}, + { XK_Next, ControlMask, "\033[6;5~", 0, 0}, + { XK_Next, ShiftMask, "\033[6;2~", 0, 0}, + { XK_Next, XK_ANY_MOD, "\033[6~", 0, 0}, + { XK_F1, XK_NO_MOD, "\033OP" , 0, 0}, + { XK_F1, /* F13 */ ShiftMask, "\033[1;2P", 0, 0}, + { XK_F1, /* F25 */ ControlMask, "\033[1;5P", 0, 0}, + { XK_F1, /* F37 */ Mod4Mask, "\033[1;6P", 0, 0}, + { XK_F1, /* F49 */ Mod1Mask, "\033[1;3P", 0, 0}, + { XK_F1, /* F61 */ Mod3Mask, "\033[1;4P", 0, 0}, + { XK_F2, XK_NO_MOD, "\033OQ" , 0, 0}, + { XK_F2, /* F14 */ ShiftMask, "\033[1;2Q", 0, 0}, + { XK_F2, /* F26 */ ControlMask, "\033[1;5Q", 0, 0}, + { XK_F2, /* F38 */ Mod4Mask, "\033[1;6Q", 0, 0}, + { XK_F2, /* F50 */ Mod1Mask, "\033[1;3Q", 0, 0}, + { XK_F2, /* F62 */ Mod3Mask, "\033[1;4Q", 0, 0}, + { XK_F3, XK_NO_MOD, "\033OR" , 0, 0}, + { XK_F3, /* F15 */ ShiftMask, "\033[1;2R", 0, 0}, + { XK_F3, /* F27 */ ControlMask, "\033[1;5R", 0, 0}, + { XK_F3, /* F39 */ Mod4Mask, "\033[1;6R", 0, 0}, + { XK_F3, /* F51 */ Mod1Mask, "\033[1;3R", 0, 0}, + { XK_F3, /* F63 */ Mod3Mask, "\033[1;4R", 0, 0}, + { XK_F4, XK_NO_MOD, "\033OS" , 0, 0}, + { XK_F4, /* F16 */ ShiftMask, "\033[1;2S", 0, 0}, + { XK_F4, /* F28 */ ControlMask, "\033[1;5S", 0, 0}, + { XK_F4, /* F40 */ Mod4Mask, "\033[1;6S", 0, 0}, + { XK_F4, /* F52 */ Mod1Mask, "\033[1;3S", 0, 0}, + { XK_F5, XK_NO_MOD, "\033[15~", 0, 0}, + { XK_F5, /* F17 */ ShiftMask, "\033[15;2~", 0, 0}, + { XK_F5, /* F29 */ ControlMask, "\033[15;5~", 0, 0}, + { XK_F5, /* F41 */ Mod4Mask, "\033[15;6~", 0, 0}, + { XK_F5, /* F53 */ Mod1Mask, "\033[15;3~", 0, 0}, + { XK_F6, XK_NO_MOD, "\033[17~", 0, 0}, + { XK_F6, /* F18 */ ShiftMask, "\033[17;2~", 0, 0}, + { XK_F6, /* F30 */ ControlMask, "\033[17;5~", 0, 0}, + { XK_F6, /* F42 */ Mod4Mask, "\033[17;6~", 0, 0}, + { XK_F6, /* F54 */ Mod1Mask, "\033[17;3~", 0, 0}, + { XK_F7, XK_NO_MOD, "\033[18~", 0, 0}, + { XK_F7, /* F19 */ ShiftMask, "\033[18;2~", 0, 0}, + { XK_F7, /* F31 */ ControlMask, "\033[18;5~", 0, 0}, + { XK_F7, /* F43 */ Mod4Mask, "\033[18;6~", 0, 0}, + { XK_F7, /* F55 */ Mod1Mask, "\033[18;3~", 0, 0}, + { XK_F8, XK_NO_MOD, "\033[19~", 0, 0}, + { XK_F8, /* F20 */ ShiftMask, "\033[19;2~", 0, 0}, + { XK_F8, /* F32 */ ControlMask, "\033[19;5~", 0, 0}, + { XK_F8, /* F44 */ Mod4Mask, "\033[19;6~", 0, 0}, + { XK_F8, /* F56 */ Mod1Mask, "\033[19;3~", 0, 0}, + { XK_F9, XK_NO_MOD, "\033[20~", 0, 0}, + { XK_F9, /* F21 */ ShiftMask, "\033[20;2~", 0, 0}, + { XK_F9, /* F33 */ ControlMask, "\033[20;5~", 0, 0}, + { XK_F9, /* F45 */ Mod4Mask, "\033[20;6~", 0, 0}, + { XK_F9, /* F57 */ Mod1Mask, "\033[20;3~", 0, 0}, + { XK_F10, XK_NO_MOD, "\033[21~", 0, 0}, + { XK_F10, /* F22 */ ShiftMask, "\033[21;2~", 0, 0}, + { XK_F10, /* F34 */ ControlMask, "\033[21;5~", 0, 0}, + { XK_F10, /* F46 */ Mod4Mask, "\033[21;6~", 0, 0}, + { XK_F10, /* F58 */ Mod1Mask, "\033[21;3~", 0, 0}, + { XK_F11, XK_NO_MOD, "\033[23~", 0, 0}, + { XK_F11, /* F23 */ ShiftMask, "\033[23;2~", 0, 0}, + { XK_F11, /* F35 */ ControlMask, "\033[23;5~", 0, 0}, + { XK_F11, /* F47 */ Mod4Mask, "\033[23;6~", 0, 0}, + { XK_F11, /* F59 */ Mod1Mask, "\033[23;3~", 0, 0}, + { XK_F12, XK_NO_MOD, "\033[24~", 0, 0}, + { XK_F12, /* F24 */ ShiftMask, "\033[24;2~", 0, 0}, + { XK_F12, /* F36 */ ControlMask, "\033[24;5~", 0, 0}, + { XK_F12, /* F48 */ Mod4Mask, "\033[24;6~", 0, 0}, + { XK_F12, /* F60 */ Mod1Mask, "\033[24;3~", 0, 0}, + { XK_F13, XK_NO_MOD, "\033[1;2P", 0, 0}, + { XK_F14, XK_NO_MOD, "\033[1;2Q", 0, 0}, + { XK_F15, XK_NO_MOD, "\033[1;2R", 0, 0}, + { XK_F16, XK_NO_MOD, "\033[1;2S", 0, 0}, + { XK_F17, XK_NO_MOD, "\033[15;2~", 0, 0}, + { XK_F18, XK_NO_MOD, "\033[17;2~", 0, 0}, + { XK_F19, XK_NO_MOD, "\033[18;2~", 0, 0}, + { XK_F20, XK_NO_MOD, "\033[19;2~", 0, 0}, + { XK_F21, XK_NO_MOD, "\033[20;2~", 0, 0}, + { XK_F22, XK_NO_MOD, "\033[21;2~", 0, 0}, + { XK_F23, XK_NO_MOD, "\033[23;2~", 0, 0}, + { XK_F24, XK_NO_MOD, "\033[24;2~", 0, 0}, + { XK_F25, XK_NO_MOD, "\033[1;5P", 0, 0}, + { XK_F26, XK_NO_MOD, "\033[1;5Q", 0, 0}, + { XK_F27, XK_NO_MOD, "\033[1;5R", 0, 0}, + { XK_F28, XK_NO_MOD, "\033[1;5S", 0, 0}, + { XK_F29, XK_NO_MOD, "\033[15;5~", 0, 0}, + { XK_F30, XK_NO_MOD, "\033[17;5~", 0, 0}, + { XK_F31, XK_NO_MOD, "\033[18;5~", 0, 0}, + { XK_F32, XK_NO_MOD, "\033[19;5~", 0, 0}, + { XK_F33, XK_NO_MOD, "\033[20;5~", 0, 0}, + { XK_F34, XK_NO_MOD, "\033[21;5~", 0, 0}, + { XK_F35, XK_NO_MOD, "\033[23;5~", 0, 0}, +}; + +/* + * Selection types' masks. + * Use the same masks as usual. + * Button1Mask is always unset, to make masks match between ButtonPress. + * ButtonRelease and MotionNotify. + * If no match is found, regular selection is used. + */ +static uint selmasks[] = { + [SEL_RECTANGULAR] = Mod1Mask, +}; + +/* + * Printable characters in ASCII, used to estimate the advance width + * of single wide characters. + */ +static char ascii_printable[] = + " !\"#$%&'()*+,-./0123456789:;<=>?" + "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~"; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..2fc854e --- /dev/null +++ b/config.mk @@ -0,0 +1,36 @@ +# st version +VERSION = 0.9.3 + +# Customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +PKG_CONFIG = pkg-config + +# includes and libs +INCS = -I$(X11INC) \ + `$(PKG_CONFIG) --cflags fontconfig` \ + `$(PKG_CONFIG) --cflags freetype2` +LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \ + `$(PKG_CONFIG) --libs fontconfig` \ + `$(PKG_CONFIG) --libs freetype2` + +# flags +STCPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600 +STCFLAGS = $(INCS) $(STCPPFLAGS) $(CPPFLAGS) $(CFLAGS) +STLDFLAGS = $(LIBS) $(LDFLAGS) + +# OpenBSD: +#CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600 -D_BSD_SOURCE +#LIBS = -L$(X11LIB) -lm -lX11 -lutil -lXft \ +# `$(PKG_CONFIG) --libs fontconfig` \ +# `$(PKG_CONFIG) --libs freetype2` +#MANPREFIX = ${PREFIX}/man + +# compiler and linker +# CC = c99 diff --git a/st.1 b/st.1 new file mode 100644 index 0000000..39120b4 --- /dev/null +++ b/st.1 @@ -0,0 +1,177 @@ +.TH ST 1 st\-VERSION +.SH NAME +st \- simple terminal +.SH SYNOPSIS +.B st +.RB [ \-aiv ] +.RB [ \-c +.IR class ] +.RB [ \-f +.IR font ] +.RB [ \-g +.IR geometry ] +.RB [ \-n +.IR name ] +.RB [ \-o +.IR iofile ] +.RB [ \-T +.IR title ] +.RB [ \-t +.IR title ] +.RB [ \-l +.IR line ] +.RB [ \-w +.IR windowid ] +.RB [[ \-e ] +.IR command +.RI [ arguments ...]] +.PP +.B st +.RB [ \-aiv ] +.RB [ \-c +.IR class ] +.RB [ \-f +.IR font ] +.RB [ \-g +.IR geometry ] +.RB [ \-n +.IR name ] +.RB [ \-o +.IR iofile ] +.RB [ \-T +.IR title ] +.RB [ \-t +.IR title ] +.RB [ \-w +.IR windowid ] +.RB \-l +.IR line +.RI [ stty_args ...] +.SH DESCRIPTION +.B st +is a simple terminal emulator. +.SH OPTIONS +.TP +.B \-a +disable alternate screens in terminal +.TP +.BI \-c " class" +defines the window class (default $TERM). +.TP +.BI \-f " font" +defines the +.I font +to use when st is run. +.TP +.BI \-g " geometry" +defines the X11 geometry string. +The form is [=][{xX}][{+-}{+-}]. See +.BR XParseGeometry (3) +for further details. +.TP +.B \-i +will fixate the position given with the -g option. +.TP +.BI \-n " name" +defines the window instance name (default $TERM). +.TP +.BI \-o " iofile" +writes all the I/O to +.I iofile. +This feature is useful when recording st sessions. A value of "-" means +standard output. +.TP +.BI \-T " title" +defines the window title (default 'st'). +.TP +.BI \-t " title" +defines the window title (default 'st'). +.TP +.BI \-w " windowid" +embeds st within the window identified by +.I windowid +.TP +.BI \-l " line" +use a tty +.I line +instead of a pseudo terminal. +.I line +should be a (pseudo-)serial device (e.g. /dev/ttyS0 on Linux for serial port +0). +When this flag is given +remaining arguments are used as flags for +.BR stty(1). +By default st initializes the serial line to 8 bits, no parity, 1 stop bit +and a 38400 baud rate. The speed is set by appending it as last argument +(e.g. 'st -l /dev/ttyS0 115200'). Arguments before the last one are +.BR stty(1) +flags. If you want to set odd parity on 115200 baud use for example 'st -l +/dev/ttyS0 parenb parodd 115200'. Set the number of bits by using for +example 'st -l /dev/ttyS0 cs7 115200'. See +.BR stty(1) +for more arguments and cases. +.TP +.B \-v +prints version information to stderr, then exits. +.TP +.BI \-e " command " [ " arguments " "... ]" +st executes +.I command +instead of the shell. If this is used it +.B must be the last option +on the command line, as in xterm / rxvt. +This option is only intended for compatibility, +and all the remaining arguments are used as a command +even without it. +.SH SHORTCUTS +.TP +.B Break +Send a break in the serial line. +Break key is obtained in PC keyboards +pressing at the same time control and pause. +.TP +.B Ctrl-Print Screen +Toggle if st should print to the +.I iofile. +.TP +.B Shift-Print Screen +Print the full screen to the +.I iofile. +.TP +.B Print Screen +Print the selection to the +.I iofile. +.TP +.B Ctrl-Shift-Page Up +Increase font size. +.TP +.B Ctrl-Shift-Page Down +Decrease font size. +.TP +.B Ctrl-Shift-Home +Reset to default font size. +.TP +.B Ctrl-Shift-y +Paste from primary selection (middle mouse button). +.TP +.B Ctrl-Shift-c +Copy the selected text to the clipboard selection. +.TP +.B Ctrl-Shift-v +Paste from the clipboard selection. +.SH CUSTOMIZATION +.B st +can be customized by creating a custom config.h and (re)compiling the source +code. This keeps it fast, secure and simple. +.SH AUTHORS +See the LICENSE file for the authors. +.SH LICENSE +See the LICENSE file for the terms of redistribution. +.SH SEE ALSO +.BR tabbed (1), +.BR utmp (1), +.BR stty (1), +.BR scroll (1) +.SH BUGS +See the TODO file in the distribution. + diff --git a/st.c b/st.c new file mode 100644 index 0000000..6f40e35 --- /dev/null +++ b/st.c @@ -0,0 +1,2705 @@ +/* See LICENSE for license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "st.h" +#include "win.h" + +#if defined(__linux) + #include +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) + #include +#elif defined(__FreeBSD__) || defined(__DragonFly__) + #include +#endif + +/* Arbitrary sizes */ +#define UTF_INVALID 0xFFFD +#define UTF_SIZ 4 +#define ESC_BUF_SIZ (128*UTF_SIZ) +#define ESC_ARG_SIZ 16 +#define STR_BUF_SIZ ESC_BUF_SIZ +#define STR_ARG_SIZ ESC_ARG_SIZ + +/* macros */ +#define IS_SET(flag) ((term.mode & (flag)) != 0) +#define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f) +#define ISCONTROLC1(c) (BETWEEN(c, 0x80, 0x9f)) +#define ISCONTROL(c) (ISCONTROLC0(c) || ISCONTROLC1(c)) +#define ISDELIM(u) (u && wcschr(worddelimiters, u)) + +enum term_mode { + MODE_WRAP = 1 << 0, + MODE_INSERT = 1 << 1, + MODE_ALTSCREEN = 1 << 2, + MODE_CRLF = 1 << 3, + MODE_ECHO = 1 << 4, + MODE_PRINT = 1 << 5, + MODE_UTF8 = 1 << 6, +}; + +enum cursor_movement { + CURSOR_SAVE, + CURSOR_LOAD +}; + +enum cursor_state { + CURSOR_DEFAULT = 0, + CURSOR_WRAPNEXT = 1, + CURSOR_ORIGIN = 2 +}; + +enum charset { + CS_GRAPHIC0, + CS_GRAPHIC1, + CS_UK, + CS_USA, + CS_MULTI, + CS_GER, + CS_FIN +}; + +enum escape_state { + ESC_START = 1, + ESC_CSI = 2, + ESC_STR = 4, /* DCS, OSC, PM, APC */ + ESC_ALTCHARSET = 8, + ESC_STR_END = 16, /* a final string was encountered */ + ESC_TEST = 32, /* Enter in test mode */ + ESC_UTF8 = 64, +}; + +typedef struct { + Glyph attr; /* current char attributes */ + int x; + int y; + char state; +} TCursor; + +typedef struct { + int mode; + int type; + int snap; + /* + * Selection variables: + * nb – normalized coordinates of the beginning of the selection + * ne – normalized coordinates of the end of the selection + * ob – original coordinates of the beginning of the selection + * oe – original coordinates of the end of the selection + */ + struct { + int x, y; + } nb, ne, ob, oe; + + int alt; +} Selection; + +/* Internal representation of the screen */ +typedef struct { + int row; /* nb row */ + int col; /* nb col */ + Line *line; /* screen */ + Line *alt; /* alternate screen */ + int *dirty; /* dirtyness of lines */ + TCursor c; /* cursor */ + int ocx; /* old cursor col */ + int ocy; /* old cursor row */ + int top; /* top scroll limit */ + int bot; /* bottom scroll limit */ + int mode; /* terminal mode flags */ + int esc; /* escape state flags */ + char trantbl[4]; /* charset table translation */ + int charset; /* current charset */ + int icharset; /* selected charset for sequence */ + int *tabs; + Rune lastc; /* last printed char outside of sequence, 0 if control */ +} Term; + +/* CSI Escape sequence structs */ +/* ESC '[' [[ [] [;]] []] */ +typedef struct { + char buf[ESC_BUF_SIZ]; /* raw string */ + size_t len; /* raw string length */ + char priv; + int arg[ESC_ARG_SIZ]; + int narg; /* nb of args */ + char mode[2]; +} CSIEscape; + +/* STR Escape sequence structs */ +/* ESC type [[ [] [;]] ] ESC '\' */ +typedef struct { + char type; /* ESC type ... */ + char *buf; /* allocated raw string */ + size_t siz; /* allocation size */ + size_t len; /* raw string length */ + char *args[STR_ARG_SIZ]; + int narg; /* nb of args */ +} STREscape; + +static void execsh(char *, char **); +static void stty(char **); +static void sigchld(int); +static void ttywriteraw(const char *, size_t); + +static void csidump(void); +static void csihandle(void); +static void csiparse(void); +static void csireset(void); +static void osc_color_response(int, int, int); +static int eschandle(uchar); +static void strdump(void); +static void strhandle(void); +static void strparse(void); +static void strreset(void); + +static void tprinter(char *, size_t); +static void tdumpsel(void); +static void tdumpline(int); +static void tdump(void); +static void tclearregion(int, int, int, int); +static void tcursor(int); +static void tdeletechar(int); +static void tdeleteline(int); +static void tinsertblank(int); +static void tinsertblankline(int); +static int tlinelen(int); +static void tmoveto(int, int); +static void tmoveato(int, int); +static void tnewline(int); +static void tputtab(int); +static void tputc(Rune); +static void treset(void); +static void tscrollup(int, int); +static void tscrolldown(int, int); +static void tsetattr(const int *, int); +static void tsetchar(Rune, const Glyph *, int, int); +static void tsetdirt(int, int); +static void tsetscroll(int, int); +static void tswapscreen(void); +static void tsetmode(int, int, const int *, int); +static int twrite(const char *, int, int); +static void tfulldirt(void); +static void tcontrolcode(uchar ); +static void tdectest(char ); +static void tdefutf8(char); +static int32_t tdefcolor(const int *, int *, int); +static void tdeftran(char); +static void tstrsequence(uchar); + +static void drawregion(int, int, int, int); + +static void selnormalize(void); +static void selscroll(int, int); +static void selsnap(int *, int *, int); + +static size_t utf8decode(const char *, Rune *, size_t); +static Rune utf8decodebyte(char, size_t *); +static char utf8encodebyte(Rune, size_t); +static size_t utf8validate(Rune *, size_t); + +static char *base64dec(const char *); +static char base64dec_getc(const char **); + +static ssize_t xwrite(int, const char *, size_t); + +/* Globals */ +static Term term; +static Selection sel; +static CSIEscape csiescseq; +static STREscape strescseq; +static int iofd = 1; +static int cmdfd; +static pid_t pid; + +static const uchar utfbyte[UTF_SIZ + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; +static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; +static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; +static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; + +ssize_t +xwrite(int fd, const char *s, size_t len) +{ + size_t aux = len; + ssize_t r; + + while (len > 0) { + r = write(fd, s, len); + if (r < 0) + return r; + len -= r; + s += r; + } + + return aux; +} + +void * +xmalloc(size_t len) +{ + void *p; + + if (!(p = malloc(len))) + die("malloc: %s\n", strerror(errno)); + + return p; +} + +void * +xrealloc(void *p, size_t len) +{ + if ((p = realloc(p, len)) == NULL) + die("realloc: %s\n", strerror(errno)); + + return p; +} + +char * +xstrdup(const char *s) +{ + char *p; + + if ((p = strdup(s)) == NULL) + die("strdup: %s\n", strerror(errno)); + + return p; +} + +size_t +utf8decode(const char *c, Rune *u, size_t clen) +{ + size_t i, j, len, type; + Rune udecoded; + + *u = UTF_INVALID; + if (!clen) + return 0; + udecoded = utf8decodebyte(c[0], &len); + if (!BETWEEN(len, 1, UTF_SIZ)) + return 1; + for (i = 1, j = 1; i < clen && j < len; ++i, ++j) { + udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type); + if (type != 0) + return j; + } + if (j < len) + return 0; + *u = udecoded; + utf8validate(u, len); + + return len; +} + +Rune +utf8decodebyte(char c, size_t *i) +{ + for (*i = 0; *i < LEN(utfmask); ++(*i)) + if (((uchar)c & utfmask[*i]) == utfbyte[*i]) + return (uchar)c & ~utfmask[*i]; + + return 0; +} + +size_t +utf8encode(Rune u, char *c) +{ + size_t len, i; + + len = utf8validate(&u, 0); + if (len > UTF_SIZ) + return 0; + + for (i = len - 1; i != 0; --i) { + c[i] = utf8encodebyte(u, 0); + u >>= 6; + } + c[0] = utf8encodebyte(u, len); + + return len; +} + +char +utf8encodebyte(Rune u, size_t i) +{ + return utfbyte[i] | (u & ~utfmask[i]); +} + +size_t +utf8validate(Rune *u, size_t i) +{ + if (!BETWEEN(*u, utfmin[i], utfmax[i]) || BETWEEN(*u, 0xD800, 0xDFFF)) + *u = UTF_INVALID; + for (i = 1; *u > utfmax[i]; ++i) + ; + + return i; +} + +char +base64dec_getc(const char **src) +{ + while (**src && !isprint((unsigned char)**src)) + (*src)++; + return **src ? *((*src)++) : '='; /* emulate padding if string ends */ +} + +char * +base64dec(const char *src) +{ + size_t in_len = strlen(src); + char *result, *dst; + static const char base64_digits[256] = { + [43] = 62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 0, 0, 0, -1, 0, 0, 0, 0, 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, 0, 0, 0, 0, + 0, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 + }; + + if (in_len % 4) + in_len += 4 - (in_len % 4); + result = dst = xmalloc(in_len / 4 * 3 + 1); + while (*src) { + int a = base64_digits[(unsigned char) base64dec_getc(&src)]; + int b = base64_digits[(unsigned char) base64dec_getc(&src)]; + int c = base64_digits[(unsigned char) base64dec_getc(&src)]; + int d = base64_digits[(unsigned char) base64dec_getc(&src)]; + + /* invalid input. 'a' can be -1, e.g. if src is "\n" (c-str) */ + if (a == -1 || b == -1) + break; + + *dst++ = (a << 2) | ((b & 0x30) >> 4); + if (c == -1) + break; + *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2); + if (d == -1) + break; + *dst++ = ((c & 0x03) << 6) | d; + } + *dst = '\0'; + return result; +} + +void +selinit(void) +{ + sel.mode = SEL_IDLE; + sel.snap = 0; + sel.ob.x = -1; +} + +int +tlinelen(int y) +{ + int i = term.col; + + if (term.line[y][i - 1].mode & ATTR_WRAP) + return i; + + while (i > 0 && term.line[y][i - 1].u == ' ') + --i; + + return i; +} + +void +selstart(int col, int row, int snap) +{ + selclear(); + sel.mode = SEL_EMPTY; + sel.type = SEL_REGULAR; + sel.alt = IS_SET(MODE_ALTSCREEN); + sel.snap = snap; + sel.oe.x = sel.ob.x = col; + sel.oe.y = sel.ob.y = row; + selnormalize(); + + if (sel.snap != 0) + sel.mode = SEL_READY; + tsetdirt(sel.nb.y, sel.ne.y); +} + +void +selextend(int col, int row, int type, int done) +{ + int oldey, oldex, oldsby, oldsey, oldtype; + + if (sel.mode == SEL_IDLE) + return; + if (done && sel.mode == SEL_EMPTY) { + selclear(); + return; + } + + oldey = sel.oe.y; + oldex = sel.oe.x; + oldsby = sel.nb.y; + oldsey = sel.ne.y; + oldtype = sel.type; + + sel.oe.x = col; + sel.oe.y = row; + selnormalize(); + sel.type = type; + + if (oldey != sel.oe.y || oldex != sel.oe.x || oldtype != sel.type || sel.mode == SEL_EMPTY) + tsetdirt(MIN(sel.nb.y, oldsby), MAX(sel.ne.y, oldsey)); + + sel.mode = done ? SEL_IDLE : SEL_READY; +} + +void +selnormalize(void) +{ + int i; + + if (sel.type == SEL_REGULAR && sel.ob.y != sel.oe.y) { + sel.nb.x = sel.ob.y < sel.oe.y ? sel.ob.x : sel.oe.x; + sel.ne.x = sel.ob.y < sel.oe.y ? sel.oe.x : sel.ob.x; + } else { + sel.nb.x = MIN(sel.ob.x, sel.oe.x); + sel.ne.x = MAX(sel.ob.x, sel.oe.x); + } + sel.nb.y = MIN(sel.ob.y, sel.oe.y); + sel.ne.y = MAX(sel.ob.y, sel.oe.y); + + selsnap(&sel.nb.x, &sel.nb.y, -1); + selsnap(&sel.ne.x, &sel.ne.y, +1); + + /* expand selection over line breaks */ + if (sel.type == SEL_RECTANGULAR) + return; + i = tlinelen(sel.nb.y); + if (i < sel.nb.x) + sel.nb.x = i; + if (tlinelen(sel.ne.y) <= sel.ne.x) + sel.ne.x = term.col - 1; +} + +int +selected(int x, int y) +{ + if (sel.mode == SEL_EMPTY || sel.ob.x == -1 || + sel.alt != IS_SET(MODE_ALTSCREEN)) + return 0; + + if (sel.type == SEL_RECTANGULAR) + return BETWEEN(y, sel.nb.y, sel.ne.y) + && BETWEEN(x, sel.nb.x, sel.ne.x); + + return BETWEEN(y, sel.nb.y, sel.ne.y) + && (y != sel.nb.y || x >= sel.nb.x) + && (y != sel.ne.y || x <= sel.ne.x); +} + +void +selsnap(int *x, int *y, int direction) +{ + int newx, newy, xt, yt; + int delim, prevdelim; + const Glyph *gp, *prevgp; + + switch (sel.snap) { + case SNAP_WORD: + /* + * Snap around if the word wraps around at the end or + * beginning of a line. + */ + prevgp = &term.line[*y][*x]; + prevdelim = ISDELIM(prevgp->u); + for (;;) { + newx = *x + direction; + newy = *y; + if (!BETWEEN(newx, 0, term.col - 1)) { + newy += direction; + newx = (newx + term.col) % term.col; + if (!BETWEEN(newy, 0, term.row - 1)) + break; + + if (direction > 0) + yt = *y, xt = *x; + else + yt = newy, xt = newx; + if (!(term.line[yt][xt].mode & ATTR_WRAP)) + break; + } + + if (newx >= tlinelen(newy)) + break; + + gp = &term.line[newy][newx]; + delim = ISDELIM(gp->u); + if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim + || (delim && gp->u != prevgp->u))) + break; + + *x = newx; + *y = newy; + prevgp = gp; + prevdelim = delim; + } + break; + case SNAP_LINE: + /* + * Snap around if the the previous line or the current one + * has set ATTR_WRAP at its end. Then the whole next or + * previous line will be selected. + */ + *x = (direction < 0) ? 0 : term.col - 1; + if (direction < 0) { + for (; *y > 0; *y += direction) { + if (!(term.line[*y-1][term.col-1].mode + & ATTR_WRAP)) { + break; + } + } + } else if (direction > 0) { + for (; *y < term.row-1; *y += direction) { + if (!(term.line[*y][term.col-1].mode + & ATTR_WRAP)) { + break; + } + } + } + break; + } +} + +char * +getsel(void) +{ + char *str, *ptr; + int y, bufsize, lastx, linelen; + const Glyph *gp, *last; + + if (sel.ob.x == -1) + return NULL; + + bufsize = (term.col+1) * (sel.ne.y-sel.nb.y+1) * UTF_SIZ; + ptr = str = xmalloc(bufsize); + + /* append every set & selected glyph to the selection */ + for (y = sel.nb.y; y <= sel.ne.y; y++) { + if ((linelen = tlinelen(y)) == 0) { + *ptr++ = '\n'; + continue; + } + + if (sel.type == SEL_RECTANGULAR) { + gp = &term.line[y][sel.nb.x]; + lastx = sel.ne.x; + } else { + gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0]; + lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1; + } + last = &term.line[y][MIN(lastx, linelen-1)]; + while (last >= gp && last->u == ' ') + --last; + + for ( ; gp <= last; ++gp) { + if (gp->mode & ATTR_WDUMMY) + continue; + + ptr += utf8encode(gp->u, ptr); + } + + /* + * Copy and pasting of line endings is inconsistent + * in the inconsistent terminal and GUI world. + * The best solution seems like to produce '\n' when + * something is copied from st and convert '\n' to + * '\r', when something to be pasted is received by + * st. + * FIXME: Fix the computer world. + */ + if ((y < sel.ne.y || lastx >= linelen) && + (!(last->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR)) + *ptr++ = '\n'; + } + *ptr = 0; + return str; +} + +void +selclear(void) +{ + if (sel.ob.x == -1) + return; + sel.mode = SEL_IDLE; + sel.ob.x = -1; + tsetdirt(sel.nb.y, sel.ne.y); +} + +void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(1); +} + +void +execsh(char *cmd, char **args) +{ + char *sh, *prog, *arg; + const struct passwd *pw; + + errno = 0; + if ((pw = getpwuid(getuid())) == NULL) { + if (errno) + die("getpwuid: %s\n", strerror(errno)); + else + die("who are you?\n"); + } + + if ((sh = getenv("SHELL")) == NULL) + sh = (pw->pw_shell[0]) ? pw->pw_shell : cmd; + + if (args) { + prog = args[0]; + arg = NULL; + } else if (scroll) { + prog = scroll; + arg = utmp ? utmp : sh; + } else if (utmp) { + prog = utmp; + arg = NULL; + } else { + prog = sh; + arg = NULL; + } + DEFAULT(args, ((char *[]) {prog, arg, NULL})); + + unsetenv("COLUMNS"); + unsetenv("LINES"); + unsetenv("TERMCAP"); + setenv("LOGNAME", pw->pw_name, 1); + setenv("USER", pw->pw_name, 1); + setenv("SHELL", sh, 1); + setenv("HOME", pw->pw_dir, 1); + setenv("TERM", termname, 1); + + signal(SIGCHLD, SIG_DFL); + signal(SIGHUP, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + signal(SIGALRM, SIG_DFL); + + execvp(prog, args); + _exit(1); +} + +void +sigchld(int a) +{ + int stat; + pid_t p; + + if ((p = waitpid(pid, &stat, WNOHANG)) < 0) + die("waiting for pid %hd failed: %s\n", pid, strerror(errno)); + + if (pid != p) + return; + + if (WIFEXITED(stat) && WEXITSTATUS(stat)) + die("child exited with status %d\n", WEXITSTATUS(stat)); + else if (WIFSIGNALED(stat)) + die("child terminated due to signal %d\n", WTERMSIG(stat)); + _exit(0); +} + +void +stty(char **args) +{ + char cmd[_POSIX_ARG_MAX], **p, *q, *s; + size_t n, siz; + + if ((n = strlen(stty_args)) > sizeof(cmd)-1) + die("incorrect stty parameters\n"); + memcpy(cmd, stty_args, n); + q = cmd + n; + siz = sizeof(cmd) - n; + for (p = args; p && (s = *p); ++p) { + if ((n = strlen(s)) > siz-1) + die("stty parameter length too long\n"); + *q++ = ' '; + memcpy(q, s, n); + q += n; + siz -= n + 1; + } + *q = '\0'; + if (system(cmd) != 0) + perror("Couldn't call stty"); +} + +int +ttynew(const char *line, char *cmd, const char *out, char **args) +{ + int m, s; + + if (out) { + term.mode |= MODE_PRINT; + iofd = (!strcmp(out, "-")) ? + 1 : open(out, O_WRONLY | O_CREAT, 0666); + if (iofd < 0) { + fprintf(stderr, "Error opening %s:%s\n", + out, strerror(errno)); + } + } + + if (line) { + if ((cmdfd = open(line, O_RDWR)) < 0) + die("open line '%s' failed: %s\n", + line, strerror(errno)); + dup2(cmdfd, 0); + stty(args); + return cmdfd; + } + + /* seems to work fine on linux, openbsd and freebsd */ + if (openpty(&m, &s, NULL, NULL, NULL) < 0) + die("openpty failed: %s\n", strerror(errno)); + + switch (pid = fork()) { + case -1: + die("fork failed: %s\n", strerror(errno)); + break; + case 0: + close(iofd); + close(m); + setsid(); /* create a new process group */ + dup2(s, 0); + dup2(s, 1); + dup2(s, 2); + if (ioctl(s, TIOCSCTTY, NULL) < 0) + die("ioctl TIOCSCTTY failed: %s\n", strerror(errno)); + if (s > 2) + close(s); +#ifdef __OpenBSD__ + if (pledge("stdio getpw proc exec", NULL) == -1) + die("pledge\n"); +#endif + execsh(cmd, args); + break; + default: +#ifdef __OpenBSD__ + if (pledge("stdio rpath tty proc", NULL) == -1) + die("pledge\n"); +#endif + close(s); + cmdfd = m; + signal(SIGCHLD, sigchld); + break; + } + return cmdfd; +} + +size_t +ttyread(void) +{ + static char buf[BUFSIZ]; + static int buflen = 0; + int ret, written; + + /* append read bytes to unprocessed bytes */ + ret = read(cmdfd, buf+buflen, LEN(buf)-buflen); + + switch (ret) { + case 0: + exit(0); + case -1: + die("couldn't read from shell: %s\n", strerror(errno)); + default: + buflen += ret; + written = twrite(buf, buflen, 0); + buflen -= written; + /* keep any incomplete UTF-8 byte sequence for the next call */ + if (buflen > 0) + memmove(buf, buf + written, buflen); + return ret; + } +} + +void +ttywrite(const char *s, size_t n, int may_echo) +{ + const char *next; + + if (may_echo && IS_SET(MODE_ECHO)) + twrite(s, n, 1); + + if (!IS_SET(MODE_CRLF)) { + ttywriteraw(s, n); + return; + } + + /* This is similar to how the kernel handles ONLCR for ttys */ + while (n > 0) { + if (*s == '\r') { + next = s + 1; + ttywriteraw("\r\n", 2); + } else { + next = memchr(s, '\r', n); + DEFAULT(next, s + n); + ttywriteraw(s, next - s); + } + n -= next - s; + s = next; + } +} + +void +ttywriteraw(const char *s, size_t n) +{ + fd_set wfd, rfd; + ssize_t r; + size_t lim = 256; + + /* + * Remember that we are using a pty, which might be a modem line. + * Writing too much will clog the line. That's why we are doing this + * dance. + * FIXME: Migrate the world to Plan 9. + */ + while (n > 0) { + FD_ZERO(&wfd); + FD_ZERO(&rfd); + FD_SET(cmdfd, &wfd); + FD_SET(cmdfd, &rfd); + + /* Check if we can write. */ + if (pselect(cmdfd+1, &rfd, &wfd, NULL, NULL, NULL) < 0) { + if (errno == EINTR) + continue; + die("select failed: %s\n", strerror(errno)); + } + if (FD_ISSET(cmdfd, &wfd)) { + /* + * Only write the bytes written by ttywrite() or the + * default of 256. This seems to be a reasonable value + * for a serial line. Bigger values might clog the I/O. + */ + if ((r = write(cmdfd, s, (n < lim)? n : lim)) < 0) + goto write_error; + if (r < n) { + /* + * We weren't able to write out everything. + * This means the buffer is getting full + * again. Empty it. + */ + if (n < lim) + lim = ttyread(); + n -= r; + s += r; + } else { + /* All bytes have been written. */ + break; + } + } + if (FD_ISSET(cmdfd, &rfd)) + lim = ttyread(); + } + return; + +write_error: + die("write error on tty: %s\n", strerror(errno)); +} + +void +ttyresize(int tw, int th) +{ + struct winsize w; + + w.ws_row = term.row; + w.ws_col = term.col; + w.ws_xpixel = tw; + w.ws_ypixel = th; + if (ioctl(cmdfd, TIOCSWINSZ, &w) < 0) + fprintf(stderr, "Couldn't set window size: %s\n", strerror(errno)); +} + +void +ttyhangup(void) +{ + /* Send SIGHUP to shell */ + kill(pid, SIGHUP); +} + +int +tattrset(int attr) +{ + int i, j; + + for (i = 0; i < term.row-1; i++) { + for (j = 0; j < term.col-1; j++) { + if (term.line[i][j].mode & attr) + return 1; + } + } + + return 0; +} + +void +tsetdirt(int top, int bot) +{ + int i; + + if (term.row <= 0) + return; + + LIMIT(top, 0, term.row-1); + LIMIT(bot, 0, term.row-1); + + for (i = top; i <= bot; i++) + term.dirty[i] = 1; +} + +void +tsetdirtattr(int attr) +{ + int i, j; + + for (i = 0; i < term.row-1; i++) { + for (j = 0; j < term.col-1; j++) { + if (term.line[i][j].mode & attr) { + tsetdirt(i, i); + break; + } + } + } +} + +void +tfulldirt(void) +{ + tsetdirt(0, term.row-1); +} + +void +tcursor(int mode) +{ + static TCursor c[2]; + int alt = IS_SET(MODE_ALTSCREEN); + + if (mode == CURSOR_SAVE) { + c[alt] = term.c; + } else if (mode == CURSOR_LOAD) { + term.c = c[alt]; + tmoveto(c[alt].x, c[alt].y); + } +} + +void +treset(void) +{ + uint i; + + term.c = (TCursor){{ + .mode = ATTR_NULL, + .fg = defaultfg, + .bg = defaultbg + }, .x = 0, .y = 0, .state = CURSOR_DEFAULT}; + + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); + for (i = tabspaces; i < term.col; i += tabspaces) + term.tabs[i] = 1; + term.top = 0; + term.bot = term.row - 1; + term.mode = MODE_WRAP|MODE_UTF8; + memset(term.trantbl, CS_USA, sizeof(term.trantbl)); + term.charset = 0; + + for (i = 0; i < 2; i++) { + tmoveto(0, 0); + tcursor(CURSOR_SAVE); + tclearregion(0, 0, term.col-1, term.row-1); + tswapscreen(); + } +} + +void +tnew(int col, int row) +{ + term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; + tresize(col, row); + treset(); +} + +void +tswapscreen(void) +{ + Line *tmp = term.line; + + term.line = term.alt; + term.alt = tmp; + term.mode ^= MODE_ALTSCREEN; + tfulldirt(); +} + +void +tscrolldown(int orig, int n) +{ + int i; + Line temp; + + LIMIT(n, 0, term.bot-orig+1); + + tsetdirt(orig, term.bot-n); + tclearregion(0, term.bot-n+1, term.col-1, term.bot); + + for (i = term.bot; i >= orig+n; i--) { + temp = term.line[i]; + term.line[i] = term.line[i-n]; + term.line[i-n] = temp; + } + + selscroll(orig, n); +} + +void +tscrollup(int orig, int n) +{ + int i; + Line temp; + + LIMIT(n, 0, term.bot-orig+1); + + tclearregion(0, orig, term.col-1, orig+n-1); + tsetdirt(orig+n, term.bot); + + for (i = orig; i <= term.bot-n; i++) { + temp = term.line[i]; + term.line[i] = term.line[i+n]; + term.line[i+n] = temp; + } + + selscroll(orig, -n); +} + +void +selscroll(int orig, int n) +{ + if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN)) + return; + + if (BETWEEN(sel.nb.y, orig, term.bot) != BETWEEN(sel.ne.y, orig, term.bot)) { + selclear(); + } else if (BETWEEN(sel.nb.y, orig, term.bot)) { + sel.ob.y += n; + sel.oe.y += n; + if (sel.ob.y < term.top || sel.ob.y > term.bot || + sel.oe.y < term.top || sel.oe.y > term.bot) { + selclear(); + } else { + selnormalize(); + } + } +} + +void +tnewline(int first_col) +{ + int y = term.c.y; + + if (y == term.bot) { + tscrollup(term.top, 1); + } else { + y++; + } + tmoveto(first_col ? 0 : term.c.x, y); +} + +void +csiparse(void) +{ + char *p = csiescseq.buf, *np; + long int v; + int sep = ';'; /* colon or semi-colon, but not both */ + + csiescseq.narg = 0; + if (*p == '?') { + csiescseq.priv = 1; + p++; + } + + csiescseq.buf[csiescseq.len] = '\0'; + while (p < csiescseq.buf+csiescseq.len) { + np = NULL; + v = strtol(p, &np, 10); + if (np == p) + v = 0; + if (v == LONG_MAX || v == LONG_MIN) + v = -1; + csiescseq.arg[csiescseq.narg++] = v; + p = np; + if (sep == ';' && *p == ':') + sep = ':'; /* allow override to colon once */ + if (*p != sep || csiescseq.narg == ESC_ARG_SIZ) + break; + p++; + } + csiescseq.mode[0] = *p++; + csiescseq.mode[1] = (p < csiescseq.buf+csiescseq.len) ? *p : '\0'; +} + +/* for absolute user moves, when decom is set */ +void +tmoveato(int x, int y) +{ + tmoveto(x, y + ((term.c.state & CURSOR_ORIGIN) ? term.top: 0)); +} + +void +tmoveto(int x, int y) +{ + int miny, maxy; + + if (term.c.state & CURSOR_ORIGIN) { + miny = term.top; + maxy = term.bot; + } else { + miny = 0; + maxy = term.row - 1; + } + term.c.state &= ~CURSOR_WRAPNEXT; + term.c.x = LIMIT(x, 0, term.col-1); + term.c.y = LIMIT(y, miny, maxy); +} + +void +tsetchar(Rune u, const Glyph *attr, int x, int y) +{ + static const char *vt100_0[62] = { /* 0x41 - 0x7e */ + "↑", "↓", "→", "←", "█", "▚", "☃", /* A - G */ + 0, 0, 0, 0, 0, 0, 0, 0, /* H - O */ + 0, 0, 0, 0, 0, 0, 0, 0, /* P - W */ + 0, 0, 0, 0, 0, 0, 0, " ", /* X - _ */ + "◆", "▒", "␉", "␌", "␍", "␊", "°", "±", /* ` - g */ + "␤", "␋", "┘", "┐", "┌", "└", "┼", "⎺", /* h - o */ + "⎻", "─", "⎼", "⎽", "├", "┤", "┴", "┬", /* p - w */ + "│", "≤", "≥", "π", "≠", "£", "·", /* x - ~ */ + }; + + /* + * The table is proudly stolen from rxvt. + */ + if (term.trantbl[term.charset] == CS_GRAPHIC0 && + BETWEEN(u, 0x41, 0x7e) && vt100_0[u - 0x41]) + utf8decode(vt100_0[u - 0x41], &u, UTF_SIZ); + + if (term.line[y][x].mode & ATTR_WIDE) { + if (x+1 < term.col) { + term.line[y][x+1].u = ' '; + term.line[y][x+1].mode &= ~ATTR_WDUMMY; + } + } else if (term.line[y][x].mode & ATTR_WDUMMY) { + term.line[y][x-1].u = ' '; + term.line[y][x-1].mode &= ~ATTR_WIDE; + } + + term.dirty[y] = 1; + term.line[y][x] = *attr; + term.line[y][x].u = u; +} + +void +tclearregion(int x1, int y1, int x2, int y2) +{ + int x, y, temp; + Glyph *gp; + + if (x1 > x2) + temp = x1, x1 = x2, x2 = temp; + if (y1 > y2) + temp = y1, y1 = y2, y2 = temp; + + LIMIT(x1, 0, term.col-1); + LIMIT(x2, 0, term.col-1); + LIMIT(y1, 0, term.row-1); + LIMIT(y2, 0, term.row-1); + + for (y = y1; y <= y2; y++) { + term.dirty[y] = 1; + for (x = x1; x <= x2; x++) { + gp = &term.line[y][x]; + if (selected(x, y)) + selclear(); + gp->fg = term.c.attr.fg; + gp->bg = term.c.attr.bg; + gp->mode = 0; + gp->u = ' '; + } + } +} + +void +tdeletechar(int n) +{ + int dst, src, size; + Glyph *line; + + LIMIT(n, 0, term.col - term.c.x); + + dst = term.c.x; + src = term.c.x + n; + size = term.col - src; + line = term.line[term.c.y]; + + memmove(&line[dst], &line[src], size * sizeof(Glyph)); + tclearregion(term.col-n, term.c.y, term.col-1, term.c.y); +} + +void +tinsertblank(int n) +{ + int dst, src, size; + Glyph *line; + + LIMIT(n, 0, term.col - term.c.x); + + dst = term.c.x + n; + src = term.c.x; + size = term.col - dst; + line = term.line[term.c.y]; + + memmove(&line[dst], &line[src], size * sizeof(Glyph)); + tclearregion(src, term.c.y, dst - 1, term.c.y); +} + +void +tinsertblankline(int n) +{ + if (BETWEEN(term.c.y, term.top, term.bot)) + tscrolldown(term.c.y, n); +} + +void +tdeleteline(int n) +{ + if (BETWEEN(term.c.y, term.top, term.bot)) + tscrollup(term.c.y, n); +} + +int32_t +tdefcolor(const int *attr, int *npar, int l) +{ + int32_t idx = -1; + uint r, g, b; + + switch (attr[*npar + 1]) { + case 2: /* direct color in RGB space */ + if (*npar + 4 >= l) { + fprintf(stderr, + "erresc(38): Incorrect number of parameters (%d)\n", + *npar); + break; + } + r = attr[*npar + 2]; + g = attr[*npar + 3]; + b = attr[*npar + 4]; + *npar += 4; + if (!BETWEEN(r, 0, 255) || !BETWEEN(g, 0, 255) || !BETWEEN(b, 0, 255)) + fprintf(stderr, "erresc: bad rgb color (%u,%u,%u)\n", + r, g, b); + else + idx = TRUECOLOR(r, g, b); + break; + case 5: /* indexed color */ + if (*npar + 2 >= l) { + fprintf(stderr, + "erresc(38): Incorrect number of parameters (%d)\n", + *npar); + break; + } + *npar += 2; + if (!BETWEEN(attr[*npar], 0, 255)) + fprintf(stderr, "erresc: bad fgcolor %d\n", attr[*npar]); + else + idx = attr[*npar]; + break; + case 0: /* implemented defined (only foreground) */ + case 1: /* transparent */ + case 3: /* direct color in CMY space */ + case 4: /* direct color in CMYK space */ + default: + fprintf(stderr, + "erresc(38): gfx attr %d unknown\n", attr[*npar]); + break; + } + + return idx; +} + +void +tsetattr(const int *attr, int l) +{ + int i; + int32_t idx; + + for (i = 0; i < l; i++) { + switch (attr[i]) { + case 0: + term.c.attr.mode &= ~( + ATTR_BOLD | + ATTR_FAINT | + ATTR_ITALIC | + ATTR_UNDERLINE | + ATTR_BLINK | + ATTR_REVERSE | + ATTR_INVISIBLE | + ATTR_STRUCK ); + term.c.attr.fg = defaultfg; + term.c.attr.bg = defaultbg; + break; + case 1: + term.c.attr.mode |= ATTR_BOLD; + break; + case 2: + term.c.attr.mode |= ATTR_FAINT; + break; + case 3: + term.c.attr.mode |= ATTR_ITALIC; + break; + case 4: + term.c.attr.mode |= ATTR_UNDERLINE; + break; + case 5: /* slow blink */ + /* FALLTHROUGH */ + case 6: /* rapid blink */ + term.c.attr.mode |= ATTR_BLINK; + break; + case 7: + term.c.attr.mode |= ATTR_REVERSE; + break; + case 8: + term.c.attr.mode |= ATTR_INVISIBLE; + break; + case 9: + term.c.attr.mode |= ATTR_STRUCK; + break; + case 22: + term.c.attr.mode &= ~(ATTR_BOLD | ATTR_FAINT); + break; + case 23: + term.c.attr.mode &= ~ATTR_ITALIC; + break; + case 24: + term.c.attr.mode &= ~ATTR_UNDERLINE; + break; + case 25: + term.c.attr.mode &= ~ATTR_BLINK; + break; + case 27: + term.c.attr.mode &= ~ATTR_REVERSE; + break; + case 28: + term.c.attr.mode &= ~ATTR_INVISIBLE; + break; + case 29: + term.c.attr.mode &= ~ATTR_STRUCK; + break; + case 38: + if ((idx = tdefcolor(attr, &i, l)) >= 0) + term.c.attr.fg = idx; + break; + case 39: /* set foreground color to default */ + term.c.attr.fg = defaultfg; + break; + case 48: + if ((idx = tdefcolor(attr, &i, l)) >= 0) + term.c.attr.bg = idx; + break; + case 49: /* set background color to default */ + term.c.attr.bg = defaultbg; + break; + case 58: + /* This starts a sequence to change the color of + * "underline" pixels. We don't support that and + * instead eat up a following "5;n" or "2;r;g;b". */ + tdefcolor(attr, &i, l); + break; + default: + if (BETWEEN(attr[i], 30, 37)) { + term.c.attr.fg = attr[i] - 30; + } else if (BETWEEN(attr[i], 40, 47)) { + term.c.attr.bg = attr[i] - 40; + } else if (BETWEEN(attr[i], 90, 97)) { + term.c.attr.fg = attr[i] - 90 + 8; + } else if (BETWEEN(attr[i], 100, 107)) { + term.c.attr.bg = attr[i] - 100 + 8; + } else { + fprintf(stderr, + "erresc(default): gfx attr %d unknown\n", + attr[i]); + csidump(); + } + break; + } + } +} + +void +tsetscroll(int t, int b) +{ + int temp; + + LIMIT(t, 0, term.row-1); + LIMIT(b, 0, term.row-1); + if (t > b) { + temp = t; + t = b; + b = temp; + } + term.top = t; + term.bot = b; +} + +void +tsetmode(int priv, int set, const int *args, int narg) +{ + int alt; const int *lim; + + for (lim = args + narg; args < lim; ++args) { + if (priv) { + switch (*args) { + case 1: /* DECCKM -- Cursor key */ + xsetmode(set, MODE_APPCURSOR); + break; + case 5: /* DECSCNM -- Reverse video */ + xsetmode(set, MODE_REVERSE); + break; + case 6: /* DECOM -- Origin */ + MODBIT(term.c.state, set, CURSOR_ORIGIN); + tmoveato(0, 0); + break; + case 7: /* DECAWM -- Auto wrap */ + MODBIT(term.mode, set, MODE_WRAP); + break; + case 0: /* Error (IGNORED) */ + case 2: /* DECANM -- ANSI/VT52 (IGNORED) */ + case 3: /* DECCOLM -- Column (IGNORED) */ + case 4: /* DECSCLM -- Scroll (IGNORED) */ + case 8: /* DECARM -- Auto repeat (IGNORED) */ + case 18: /* DECPFF -- Printer feed (IGNORED) */ + case 19: /* DECPEX -- Printer extent (IGNORED) */ + case 42: /* DECNRCM -- National characters (IGNORED) */ + case 12: /* att610 -- Start blinking cursor (IGNORED) */ + break; + case 25: /* DECTCEM -- Text Cursor Enable Mode */ + xsetmode(!set, MODE_HIDE); + break; + case 9: /* X10 mouse compatibility mode */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEX10); + break; + case 1000: /* 1000: report button press */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEBTN); + break; + case 1002: /* 1002: report motion on button press */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEMOTION); + break; + case 1003: /* 1003: enable all mouse motions */ + xsetpointermotion(set); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEMANY); + break; + case 1004: /* 1004: send focus events to tty */ + xsetmode(set, MODE_FOCUS); + break; + case 1006: /* 1006: extended reporting mode */ + xsetmode(set, MODE_MOUSESGR); + break; + case 1034: /* 1034: enable 8-bit mode for keyboard input */ + xsetmode(set, MODE_8BIT); + break; + case 1049: /* swap screen & set/restore cursor as xterm */ + if (!allowaltscreen) + break; + tcursor((set) ? CURSOR_SAVE : CURSOR_LOAD); + /* FALLTHROUGH */ + case 47: /* swap screen buffer */ + case 1047: /* swap screen buffer */ + if (!allowaltscreen) + break; + alt = IS_SET(MODE_ALTSCREEN); + if (alt) { + tclearregion(0, 0, term.col-1, + term.row-1); + } + if (set ^ alt) /* set is always 1 or 0 */ + tswapscreen(); + if (*args != 1049) + break; + /* FALLTHROUGH */ + case 1048: /* save/restore cursor (like DECSC/DECRC) */ + tcursor((set) ? CURSOR_SAVE : CURSOR_LOAD); + break; + case 2004: /* 2004: bracketed paste mode */ + xsetmode(set, MODE_BRCKTPASTE); + break; + /* Not implemented mouse modes. See comments there. */ + case 1001: /* mouse highlight mode; can hang the + terminal by design when implemented. */ + case 1005: /* UTF-8 mouse mode; will confuse + applications not supporting UTF-8 + and luit. */ + case 1015: /* urxvt mangled mouse mode; incompatible + and can be mistaken for other control + codes. */ + break; + default: + fprintf(stderr, + "erresc: unknown private set/reset mode %d\n", + *args); + break; + } + } else { + switch (*args) { + case 0: /* Error (IGNORED) */ + break; + case 2: + xsetmode(set, MODE_KBDLOCK); + break; + case 4: /* IRM -- Insertion-replacement */ + MODBIT(term.mode, set, MODE_INSERT); + break; + case 12: /* SRM -- Send/Receive */ + MODBIT(term.mode, !set, MODE_ECHO); + break; + case 20: /* LNM -- Linefeed/new line */ + MODBIT(term.mode, set, MODE_CRLF); + break; + default: + fprintf(stderr, + "erresc: unknown set/reset mode %d\n", + *args); + break; + } + } + } +} + +void +csihandle(void) +{ + char buf[40]; + int len; + + switch (csiescseq.mode[0]) { + default: + unknown: + fprintf(stderr, "erresc: unknown csi "); + csidump(); + /* die(""); */ + break; + case '@': /* ICH -- Insert blank char */ + DEFAULT(csiescseq.arg[0], 1); + tinsertblank(csiescseq.arg[0]); + break; + case 'A': /* CUU -- Cursor Up */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x, term.c.y-csiescseq.arg[0]); + break; + case 'B': /* CUD -- Cursor Down */ + case 'e': /* VPR --Cursor Down */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x, term.c.y+csiescseq.arg[0]); + break; + case 'i': /* MC -- Media Copy */ + switch (csiescseq.arg[0]) { + case 0: + tdump(); + break; + case 1: + tdumpline(term.c.y); + break; + case 2: + tdumpsel(); + break; + case 4: + term.mode &= ~MODE_PRINT; + break; + case 5: + term.mode |= MODE_PRINT; + break; + } + break; + case 'c': /* DA -- Device Attributes */ + if (csiescseq.arg[0] == 0) + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 'b': /* REP -- if last char is printable print it more times */ + LIMIT(csiescseq.arg[0], 1, 65535); + if (term.lastc) + while (csiescseq.arg[0]-- > 0) + tputc(term.lastc); + break; + case 'C': /* CUF -- Cursor Forward */ + case 'a': /* HPR -- Cursor Forward */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x+csiescseq.arg[0], term.c.y); + break; + case 'D': /* CUB -- Cursor Backward */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x-csiescseq.arg[0], term.c.y); + break; + case 'E': /* CNL -- Cursor Down and first col */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(0, term.c.y+csiescseq.arg[0]); + break; + case 'F': /* CPL -- Cursor Up and first col */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(0, term.c.y-csiescseq.arg[0]); + break; + case 'g': /* TBC -- Tabulation clear */ + switch (csiescseq.arg[0]) { + case 0: /* clear current tab stop */ + term.tabs[term.c.x] = 0; + break; + case 3: /* clear all the tabs */ + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); + break; + default: + goto unknown; + } + break; + case 'G': /* CHA -- Move to */ + case '`': /* HPA */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(csiescseq.arg[0]-1, term.c.y); + break; + case 'H': /* CUP -- Move to */ + case 'f': /* HVP */ + DEFAULT(csiescseq.arg[0], 1); + DEFAULT(csiescseq.arg[1], 1); + tmoveato(csiescseq.arg[1]-1, csiescseq.arg[0]-1); + break; + case 'I': /* CHT -- Cursor Forward Tabulation tab stops */ + DEFAULT(csiescseq.arg[0], 1); + tputtab(csiescseq.arg[0]); + break; + case 'J': /* ED -- Clear screen */ + switch (csiescseq.arg[0]) { + case 0: /* below */ + tclearregion(term.c.x, term.c.y, term.col-1, term.c.y); + if (term.c.y < term.row-1) { + tclearregion(0, term.c.y+1, term.col-1, + term.row-1); + } + break; + case 1: /* above */ + if (term.c.y > 0) + tclearregion(0, 0, term.col-1, term.c.y-1); + tclearregion(0, term.c.y, term.c.x, term.c.y); + break; + case 2: /* all */ + tclearregion(0, 0, term.col-1, term.row-1); + break; + default: + goto unknown; + } + break; + case 'K': /* EL -- Clear line */ + switch (csiescseq.arg[0]) { + case 0: /* right */ + tclearregion(term.c.x, term.c.y, term.col-1, + term.c.y); + break; + case 1: /* left */ + tclearregion(0, term.c.y, term.c.x, term.c.y); + break; + case 2: /* all */ + tclearregion(0, term.c.y, term.col-1, term.c.y); + break; + } + break; + case 'S': /* SU -- Scroll line up */ + if (csiescseq.priv) break; + DEFAULT(csiescseq.arg[0], 1); + tscrollup(term.top, csiescseq.arg[0]); + break; + case 'T': /* SD -- Scroll line down */ + DEFAULT(csiescseq.arg[0], 1); + tscrolldown(term.top, csiescseq.arg[0]); + break; + case 'L': /* IL -- Insert blank lines */ + DEFAULT(csiescseq.arg[0], 1); + tinsertblankline(csiescseq.arg[0]); + break; + case 'l': /* RM -- Reset Mode */ + tsetmode(csiescseq.priv, 0, csiescseq.arg, csiescseq.narg); + break; + case 'M': /* DL -- Delete lines */ + DEFAULT(csiescseq.arg[0], 1); + tdeleteline(csiescseq.arg[0]); + break; + case 'X': /* ECH -- Erase char */ + DEFAULT(csiescseq.arg[0], 1); + tclearregion(term.c.x, term.c.y, + term.c.x + csiescseq.arg[0] - 1, term.c.y); + break; + case 'P': /* DCH -- Delete char */ + DEFAULT(csiescseq.arg[0], 1); + tdeletechar(csiescseq.arg[0]); + break; + case 'Z': /* CBT -- Cursor Backward Tabulation tab stops */ + DEFAULT(csiescseq.arg[0], 1); + tputtab(-csiescseq.arg[0]); + break; + case 'd': /* VPA -- Move to */ + DEFAULT(csiescseq.arg[0], 1); + tmoveato(term.c.x, csiescseq.arg[0]-1); + break; + case 'h': /* SM -- Set terminal mode */ + tsetmode(csiescseq.priv, 1, csiescseq.arg, csiescseq.narg); + break; + case 'm': /* SGR -- Terminal attribute (color) */ + tsetattr(csiescseq.arg, csiescseq.narg); + break; + case 'n': /* DSR -- Device Status Report */ + switch (csiescseq.arg[0]) { + case 5: /* Status Report "OK" `0n` */ + ttywrite("\033[0n", sizeof("\033[0n") - 1, 0); + break; + case 6: /* Report Cursor Position (CPR) ";R" */ + len = snprintf(buf, sizeof(buf), "\033[%i;%iR", + term.c.y+1, term.c.x+1); + ttywrite(buf, len, 0); + break; + default: + goto unknown; + } + break; + case 'r': /* DECSTBM -- Set Scrolling Region */ + if (csiescseq.priv) { + goto unknown; + } else { + DEFAULT(csiescseq.arg[0], 1); + DEFAULT(csiescseq.arg[1], term.row); + tsetscroll(csiescseq.arg[0]-1, csiescseq.arg[1]-1); + tmoveato(0, 0); + } + break; + case 's': /* DECSC -- Save cursor position (ANSI.SYS) */ + tcursor(CURSOR_SAVE); + break; + case 'u': /* DECRC -- Restore cursor position (ANSI.SYS) */ + if (csiescseq.priv) { + goto unknown; + } else { + tcursor(CURSOR_LOAD); + } + break; + case ' ': + switch (csiescseq.mode[1]) { + case 'q': /* DECSCUSR -- Set Cursor Style */ + if (xsetcursor(csiescseq.arg[0])) + goto unknown; + break; + default: + goto unknown; + } + break; + } +} + +void +csidump(void) +{ + size_t i; + uint c; + + fprintf(stderr, "ESC["); + for (i = 0; i < csiescseq.len; i++) { + c = csiescseq.buf[i] & 0xff; + if (isprint(c)) { + putc(c, stderr); + } else if (c == '\n') { + fprintf(stderr, "(\\n)"); + } else if (c == '\r') { + fprintf(stderr, "(\\r)"); + } else if (c == 0x1b) { + fprintf(stderr, "(\\e)"); + } else { + fprintf(stderr, "(%02x)", c); + } + } + putc('\n', stderr); +} + +void +csireset(void) +{ + memset(&csiescseq, 0, sizeof(csiescseq)); +} + +void +osc_color_response(int num, int index, int is_osc4) +{ + int n; + char buf[32]; + unsigned char r, g, b; + + if (xgetcolor(is_osc4 ? num : index, &r, &g, &b)) { + fprintf(stderr, "erresc: failed to fetch %s color %d\n", + is_osc4 ? "osc4" : "osc", + is_osc4 ? num : index); + return; + } + + n = snprintf(buf, sizeof buf, "\033]%s%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", + is_osc4 ? "4;" : "", num, r, r, g, g, b, b); + if (n < 0 || n >= sizeof(buf)) { + fprintf(stderr, "error: %s while printing %s response\n", + n < 0 ? "snprintf failed" : "truncation occurred", + is_osc4 ? "osc4" : "osc"); + } else { + ttywrite(buf, n, 1); + } +} + +void +strhandle(void) +{ + char *p = NULL, *dec; + int j, narg, par; + const struct { int idx; char *str; } osc_table[] = { + { defaultfg, "foreground" }, + { defaultbg, "background" }, + { defaultcs, "cursor" } + }; + + term.esc &= ~(ESC_STR_END|ESC_STR); + strparse(); + par = (narg = strescseq.narg) ? atoi(strescseq.args[0]) : 0; + + switch (strescseq.type) { + case ']': /* OSC -- Operating System Command */ + switch (par) { + case 0: + if (narg > 1) { + xsettitle(strescseq.args[1]); + xseticontitle(strescseq.args[1]); + } + return; + case 1: + if (narg > 1) + xseticontitle(strescseq.args[1]); + return; + case 2: + if (narg > 1) + xsettitle(strescseq.args[1]); + return; + case 52: /* manipulate selection data */ + if (narg > 2 && allowwindowops) { + dec = base64dec(strescseq.args[2]); + if (dec) { + xsetsel(dec); + xclipcopy(); + } else { + fprintf(stderr, "erresc: invalid base64\n"); + } + } + return; + case 10: /* set dynamic VT100 text foreground color */ + case 11: /* set dynamic VT100 text background color */ + case 12: /* set dynamic text cursor color */ + if (narg < 2) + break; + p = strescseq.args[1]; + if ((j = par - 10) < 0 || j >= LEN(osc_table)) + break; /* shouldn't be possible */ + + if (!strcmp(p, "?")) { + osc_color_response(par, osc_table[j].idx, 0); + } else if (xsetcolorname(osc_table[j].idx, p)) { + fprintf(stderr, "erresc: invalid %s color: %s\n", + osc_table[j].str, p); + } else { + tfulldirt(); + } + return; + case 4: /* color set */ + if (narg < 3) + break; + p = strescseq.args[2]; + /* FALLTHROUGH */ + case 104: /* color reset */ + j = (narg > 1) ? atoi(strescseq.args[1]) : -1; + + if (p && !strcmp(p, "?")) { + osc_color_response(j, 0, 1); + } else if (xsetcolorname(j, p)) { + if (par == 104 && narg <= 1) { + xloadcols(); + return; /* color reset without parameter */ + } + fprintf(stderr, "erresc: invalid color j=%d, p=%s\n", + j, p ? p : "(null)"); + } else { + /* + * TODO if defaultbg color is changed, borders + * are dirty + */ + tfulldirt(); + } + return; + case 110: /* reset dynamic VT100 text foreground color */ + case 111: /* reset dynamic VT100 text background color */ + case 112: /* reset dynamic text cursor color */ + if (narg != 1) + break; + if ((j = par - 110) < 0 || j >= LEN(osc_table)) + break; /* shouldn't be possible */ + if (xsetcolorname(osc_table[j].idx, NULL)) { + fprintf(stderr, "erresc: %s color not found\n", osc_table[j].str); + } else { + tfulldirt(); + } + return; + } + break; + case 'k': /* old title set compatibility */ + xsettitle(strescseq.args[0]); + return; + case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ + case '^': /* PM -- Privacy Message */ + return; + } + + fprintf(stderr, "erresc: unknown str "); + strdump(); +} + +void +strparse(void) +{ + int c; + char *p = strescseq.buf; + + strescseq.narg = 0; + strescseq.buf[strescseq.len] = '\0'; + + if (*p == '\0') + return; + + while (strescseq.narg < STR_ARG_SIZ) { + strescseq.args[strescseq.narg++] = p; + while ((c = *p) != ';' && c != '\0') + ++p; + if (c == '\0') + return; + *p++ = '\0'; + } +} + +void +strdump(void) +{ + size_t i; + uint c; + + fprintf(stderr, "ESC%c", strescseq.type); + for (i = 0; i < strescseq.len; i++) { + c = strescseq.buf[i] & 0xff; + if (c == '\0') { + putc('\n', stderr); + return; + } else if (isprint(c)) { + putc(c, stderr); + } else if (c == '\n') { + fprintf(stderr, "(\\n)"); + } else if (c == '\r') { + fprintf(stderr, "(\\r)"); + } else if (c == 0x1b) { + fprintf(stderr, "(\\e)"); + } else { + fprintf(stderr, "(%02x)", c); + } + } + fprintf(stderr, "ESC\\\n"); +} + +void +strreset(void) +{ + strescseq = (STREscape){ + .buf = xrealloc(strescseq.buf, STR_BUF_SIZ), + .siz = STR_BUF_SIZ, + }; +} + +void +sendbreak(const Arg *arg) +{ + if (tcsendbreak(cmdfd, 0)) + perror("Error sending break"); +} + +void +tprinter(char *s, size_t len) +{ + if (iofd != -1 && xwrite(iofd, s, len) < 0) { + perror("Error writing to output file"); + close(iofd); + iofd = -1; + } +} + +void +toggleprinter(const Arg *arg) +{ + term.mode ^= MODE_PRINT; +} + +void +printscreen(const Arg *arg) +{ + tdump(); +} + +void +printsel(const Arg *arg) +{ + tdumpsel(); +} + +void +tdumpsel(void) +{ + char *ptr; + + if ((ptr = getsel())) { + tprinter(ptr, strlen(ptr)); + free(ptr); + } +} + +void +tdumpline(int n) +{ + char buf[UTF_SIZ]; + const Glyph *bp, *end; + + bp = &term.line[n][0]; + end = &bp[MIN(tlinelen(n), term.col) - 1]; + if (bp != end || bp->u != ' ') { + for ( ; bp <= end; ++bp) + tprinter(buf, utf8encode(bp->u, buf)); + } + tprinter("\n", 1); +} + +void +tdump(void) +{ + int i; + + for (i = 0; i < term.row; ++i) + tdumpline(i); +} + +void +tputtab(int n) +{ + uint x = term.c.x; + + if (n > 0) { + while (x < term.col && n--) + for (++x; x < term.col && !term.tabs[x]; ++x) + /* nothing */ ; + } else if (n < 0) { + while (x > 0 && n++) + for (--x; x > 0 && !term.tabs[x]; --x) + /* nothing */ ; + } + term.c.x = LIMIT(x, 0, term.col-1); +} + +void +tdefutf8(char ascii) +{ + if (ascii == 'G') + term.mode |= MODE_UTF8; + else if (ascii == '@') + term.mode &= ~MODE_UTF8; +} + +void +tdeftran(char ascii) +{ + static char cs[] = "0B"; + static int vcs[] = {CS_GRAPHIC0, CS_USA}; + char *p; + + if ((p = strchr(cs, ascii)) == NULL) { + fprintf(stderr, "esc unhandled charset: ESC ( %c\n", ascii); + } else { + term.trantbl[term.icharset] = vcs[p - cs]; + } +} + +void +tdectest(char c) +{ + int x, y; + + if (c == '8') { /* DEC screen alignment test. */ + for (x = 0; x < term.col; ++x) { + for (y = 0; y < term.row; ++y) + tsetchar('E', &term.c.attr, x, y); + } + } +} + +void +tstrsequence(uchar c) +{ + switch (c) { + case 0x90: /* DCS -- Device Control String */ + c = 'P'; + break; + case 0x9f: /* APC -- Application Program Command */ + c = '_'; + break; + case 0x9e: /* PM -- Privacy Message */ + c = '^'; + break; + case 0x9d: /* OSC -- Operating System Command */ + c = ']'; + break; + } + strreset(); + strescseq.type = c; + term.esc |= ESC_STR; +} + +void +tcontrolcode(uchar ascii) +{ + switch (ascii) { + case '\t': /* HT */ + tputtab(1); + return; + case '\b': /* BS */ + tmoveto(term.c.x-1, term.c.y); + return; + case '\r': /* CR */ + tmoveto(0, term.c.y); + return; + case '\f': /* LF */ + case '\v': /* VT */ + case '\n': /* LF */ + /* go to first col if the mode is set */ + tnewline(IS_SET(MODE_CRLF)); + return; + case '\a': /* BEL */ + if (term.esc & ESC_STR_END) { + /* backwards compatibility to xterm */ + strhandle(); + } else { + xbell(); + } + break; + case '\033': /* ESC */ + csireset(); + term.esc &= ~(ESC_CSI|ESC_ALTCHARSET|ESC_TEST); + term.esc |= ESC_START; + return; + case '\016': /* SO (LS1 -- Locking shift 1) */ + case '\017': /* SI (LS0 -- Locking shift 0) */ + term.charset = 1 - (ascii - '\016'); + return; + case '\032': /* SUB */ + tsetchar('?', &term.c.attr, term.c.x, term.c.y); + /* FALLTHROUGH */ + case '\030': /* CAN */ + csireset(); + break; + case '\005': /* ENQ (IGNORED) */ + case '\000': /* NUL (IGNORED) */ + case '\021': /* XON (IGNORED) */ + case '\023': /* XOFF (IGNORED) */ + case 0177: /* DEL (IGNORED) */ + return; + case 0x80: /* TODO: PAD */ + case 0x81: /* TODO: HOP */ + case 0x82: /* TODO: BPH */ + case 0x83: /* TODO: NBH */ + case 0x84: /* TODO: IND */ + break; + case 0x85: /* NEL -- Next line */ + tnewline(1); /* always go to first col */ + break; + case 0x86: /* TODO: SSA */ + case 0x87: /* TODO: ESA */ + break; + case 0x88: /* HTS -- Horizontal tab stop */ + term.tabs[term.c.x] = 1; + break; + case 0x89: /* TODO: HTJ */ + case 0x8a: /* TODO: VTS */ + case 0x8b: /* TODO: PLD */ + case 0x8c: /* TODO: PLU */ + case 0x8d: /* TODO: RI */ + case 0x8e: /* TODO: SS2 */ + case 0x8f: /* TODO: SS3 */ + case 0x91: /* TODO: PU1 */ + case 0x92: /* TODO: PU2 */ + case 0x93: /* TODO: STS */ + case 0x94: /* TODO: CCH */ + case 0x95: /* TODO: MW */ + case 0x96: /* TODO: SPA */ + case 0x97: /* TODO: EPA */ + case 0x98: /* TODO: SOS */ + case 0x99: /* TODO: SGCI */ + break; + case 0x9a: /* DECID -- Identify Terminal */ + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 0x9b: /* TODO: CSI */ + case 0x9c: /* TODO: ST */ + break; + case 0x90: /* DCS -- Device Control String */ + case 0x9d: /* OSC -- Operating System Command */ + case 0x9e: /* PM -- Privacy Message */ + case 0x9f: /* APC -- Application Program Command */ + tstrsequence(ascii); + return; + } + /* only CAN, SUB, \a and C1 chars interrupt a sequence */ + term.esc &= ~(ESC_STR_END|ESC_STR); +} + +/* + * returns 1 when the sequence is finished and it hasn't to read + * more characters for this sequence, otherwise 0 + */ +int +eschandle(uchar ascii) +{ + switch (ascii) { + case '[': + term.esc |= ESC_CSI; + return 0; + case '#': + term.esc |= ESC_TEST; + return 0; + case '%': + term.esc |= ESC_UTF8; + return 0; + case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ + case '^': /* PM -- Privacy Message */ + case ']': /* OSC -- Operating System Command */ + case 'k': /* old title set compatibility */ + tstrsequence(ascii); + return 0; + case 'n': /* LS2 -- Locking shift 2 */ + case 'o': /* LS3 -- Locking shift 3 */ + term.charset = 2 + (ascii - 'n'); + break; + case '(': /* GZD4 -- set primary charset G0 */ + case ')': /* G1D4 -- set secondary charset G1 */ + case '*': /* G2D4 -- set tertiary charset G2 */ + case '+': /* G3D4 -- set quaternary charset G3 */ + term.icharset = ascii - '('; + term.esc |= ESC_ALTCHARSET; + return 0; + case 'D': /* IND -- Linefeed */ + if (term.c.y == term.bot) { + tscrollup(term.top, 1); + } else { + tmoveto(term.c.x, term.c.y+1); + } + break; + case 'E': /* NEL -- Next line */ + tnewline(1); /* always go to first col */ + break; + case 'H': /* HTS -- Horizontal tab stop */ + term.tabs[term.c.x] = 1; + break; + case 'M': /* RI -- Reverse index */ + if (term.c.y == term.top) { + tscrolldown(term.top, 1); + } else { + tmoveto(term.c.x, term.c.y-1); + } + break; + case 'Z': /* DECID -- Identify Terminal */ + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 'c': /* RIS -- Reset to initial state */ + treset(); + resettitle(); + xloadcols(); + xsetmode(0, MODE_HIDE); + xsetmode(0, MODE_BRCKTPASTE); + break; + case '=': /* DECPAM -- Application keypad */ + xsetmode(1, MODE_APPKEYPAD); + break; + case '>': /* DECPNM -- Normal keypad */ + xsetmode(0, MODE_APPKEYPAD); + break; + case '7': /* DECSC -- Save Cursor */ + tcursor(CURSOR_SAVE); + break; + case '8': /* DECRC -- Restore Cursor */ + tcursor(CURSOR_LOAD); + break; + case '\\': /* ST -- String Terminator */ + if (term.esc & ESC_STR_END) + strhandle(); + break; + default: + fprintf(stderr, "erresc: unknown sequence ESC 0x%02X '%c'\n", + (uchar) ascii, isprint(ascii)? ascii:'.'); + break; + } + return 1; +} + +void +tputc(Rune u) +{ + char c[UTF_SIZ]; + int control; + int width, len; + Glyph *gp; + + control = ISCONTROL(u); + if (u < 127 || !IS_SET(MODE_UTF8)) { + c[0] = u; + width = len = 1; + } else { + len = utf8encode(u, c); + if (!control && (width = wcwidth(u)) == -1) + width = 1; + } + + if (IS_SET(MODE_PRINT)) + tprinter(c, len); + + /* + * STR sequence must be checked before anything else + * because it uses all following characters until it + * receives a ESC, a SUB, a ST or any other C1 control + * character. + */ + if (term.esc & ESC_STR) { + if (u == '\a' || u == 030 || u == 032 || u == 033 || + ISCONTROLC1(u)) { + term.esc &= ~(ESC_START|ESC_STR); + term.esc |= ESC_STR_END; + goto check_control_code; + } + + if (strescseq.len+len >= strescseq.siz) { + /* + * Here is a bug in terminals. If the user never sends + * some code to stop the str or esc command, then st + * will stop responding. But this is better than + * silently failing with unknown characters. At least + * then users will report back. + * + * In the case users ever get fixed, here is the code: + */ + /* + * term.esc = 0; + * strhandle(); + */ + if (strescseq.siz > (SIZE_MAX - UTF_SIZ) / 2) + return; + strescseq.siz *= 2; + strescseq.buf = xrealloc(strescseq.buf, strescseq.siz); + } + + memmove(&strescseq.buf[strescseq.len], c, len); + strescseq.len += len; + return; + } + +check_control_code: + /* + * Actions of control codes must be performed as soon they arrive + * because they can be embedded inside a control sequence, and + * they must not cause conflicts with sequences. + */ + if (control) { + /* in UTF-8 mode ignore handling C1 control characters */ + if (IS_SET(MODE_UTF8) && ISCONTROLC1(u)) + return; + tcontrolcode(u); + /* + * control codes are not shown ever + */ + if (!term.esc) + term.lastc = 0; + return; + } else if (term.esc & ESC_START) { + if (term.esc & ESC_CSI) { + csiescseq.buf[csiescseq.len++] = u; + if (BETWEEN(u, 0x40, 0x7E) + || csiescseq.len >= \ + sizeof(csiescseq.buf)-1) { + term.esc = 0; + csiparse(); + csihandle(); + } + return; + } else if (term.esc & ESC_UTF8) { + tdefutf8(u); + } else if (term.esc & ESC_ALTCHARSET) { + tdeftran(u); + } else if (term.esc & ESC_TEST) { + tdectest(u); + } else { + if (!eschandle(u)) + return; + /* sequence already finished */ + } + term.esc = 0; + /* + * All characters which form part of a sequence are not + * printed + */ + return; + } + if (selected(term.c.x, term.c.y)) + selclear(); + + gp = &term.line[term.c.y][term.c.x]; + if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) { + gp->mode |= ATTR_WRAP; + tnewline(1); + gp = &term.line[term.c.y][term.c.x]; + } + + if (IS_SET(MODE_INSERT) && term.c.x+width < term.col) { + memmove(gp+width, gp, (term.col - term.c.x - width) * sizeof(Glyph)); + gp->mode &= ~ATTR_WIDE; + } + + if (term.c.x+width > term.col) { + if (IS_SET(MODE_WRAP)) + tnewline(1); + else + tmoveto(term.col - width, term.c.y); + gp = &term.line[term.c.y][term.c.x]; + } + + tsetchar(u, &term.c.attr, term.c.x, term.c.y); + term.lastc = u; + + if (width == 2) { + gp->mode |= ATTR_WIDE; + if (term.c.x+1 < term.col) { + if (gp[1].mode == ATTR_WIDE && term.c.x+2 < term.col) { + gp[2].u = ' '; + gp[2].mode &= ~ATTR_WDUMMY; + } + gp[1].u = '\0'; + gp[1].mode = ATTR_WDUMMY; + } + } + if (term.c.x+width < term.col) { + tmoveto(term.c.x+width, term.c.y); + } else { + term.c.state |= CURSOR_WRAPNEXT; + } +} + +int +twrite(const char *buf, int buflen, int show_ctrl) +{ + int charsize; + Rune u; + int n; + + for (n = 0; n < buflen; n += charsize) { + if (IS_SET(MODE_UTF8)) { + /* process a complete utf8 char */ + charsize = utf8decode(buf + n, &u, buflen - n); + if (charsize == 0) + break; + } else { + u = buf[n] & 0xFF; + charsize = 1; + } + if (show_ctrl && ISCONTROL(u)) { + if (u & 0x80) { + u &= 0x7f; + tputc('^'); + tputc('['); + } else if (u != '\n' && u != '\r' && u != '\t') { + u ^= 0x40; + tputc('^'); + } + } + tputc(u); + } + return n; +} + +void +tresize(int col, int row) +{ + int i; + int minrow = MIN(row, term.row); + int mincol = MIN(col, term.col); + int *bp; + TCursor c; + + if (col < 1 || row < 1) { + fprintf(stderr, + "tresize: error resizing to %dx%d\n", col, row); + return; + } + + /* + * slide screen to keep cursor where we expect it - + * tscrollup would work here, but we can optimize to + * memmove because we're freeing the earlier lines + */ + for (i = 0; i <= term.c.y - row; i++) { + free(term.line[i]); + free(term.alt[i]); + } + /* ensure that both src and dst are not NULL */ + if (i > 0) { + memmove(term.line, term.line + i, row * sizeof(Line)); + memmove(term.alt, term.alt + i, row * sizeof(Line)); + } + for (i += row; i < term.row; i++) { + free(term.line[i]); + free(term.alt[i]); + } + + /* resize to new height */ + term.line = xrealloc(term.line, row * sizeof(Line)); + term.alt = xrealloc(term.alt, row * sizeof(Line)); + term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty)); + term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs)); + + /* resize each row to new width, zero-pad if needed */ + for (i = 0; i < minrow; i++) { + term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph)); + term.alt[i] = xrealloc(term.alt[i], col * sizeof(Glyph)); + } + + /* allocate any new rows */ + for (/* i = minrow */; i < row; i++) { + term.line[i] = xmalloc(col * sizeof(Glyph)); + term.alt[i] = xmalloc(col * sizeof(Glyph)); + } + if (col > term.col) { + bp = term.tabs + term.col; + + memset(bp, 0, sizeof(*term.tabs) * (col - term.col)); + while (--bp > term.tabs && !*bp) + /* nothing */ ; + for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces) + *bp = 1; + } + /* update terminal size */ + term.col = col; + term.row = row; + /* reset scrolling region */ + tsetscroll(0, row-1); + /* make use of the LIMIT in tmoveto */ + tmoveto(term.c.x, term.c.y); + /* Clearing both screens (it makes dirty all lines) */ + c = term.c; + for (i = 0; i < 2; i++) { + if (mincol < col && 0 < minrow) { + tclearregion(mincol, 0, col - 1, minrow - 1); + } + if (0 < col && minrow < row) { + tclearregion(0, minrow, col - 1, row - 1); + } + tswapscreen(); + tcursor(CURSOR_LOAD); + } + term.c = c; +} + +void +resettitle(void) +{ + xsettitle(NULL); +} + +void +drawregion(int x1, int y1, int x2, int y2) +{ + int y; + + for (y = y1; y < y2; y++) { + if (!term.dirty[y]) + continue; + + term.dirty[y] = 0; + xdrawline(term.line[y], x1, y, x2); + } +} + +void +draw(void) +{ + int cx = term.c.x, ocx = term.ocx, ocy = term.ocy; + + if (!xstartdraw()) + return; + + /* adjust cursor position */ + LIMIT(term.ocx, 0, term.col-1); + LIMIT(term.ocy, 0, term.row-1); + if (term.line[term.ocy][term.ocx].mode & ATTR_WDUMMY) + term.ocx--; + if (term.line[term.c.y][cx].mode & ATTR_WDUMMY) + cx--; + + drawregion(0, 0, term.col, term.row); + xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], + term.ocx, term.ocy, term.line[term.ocy][term.ocx]); + term.ocx = cx; + term.ocy = term.c.y; + xfinishdraw(); + if (ocx != term.ocx || ocy != term.ocy) + xximspot(term.ocx, term.ocy); +} + +void +redraw(void) +{ + tfulldirt(); + draw(); +} diff --git a/st.h b/st.h new file mode 100644 index 0000000..fd3b0d8 --- /dev/null +++ b/st.h @@ -0,0 +1,126 @@ +/* See LICENSE for license details. */ + +#include +#include + +/* macros */ +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) < (b) ? (b) : (a)) +#define LEN(a) (sizeof(a) / sizeof(a)[0]) +#define BETWEEN(x, a, b) ((a) <= (x) && (x) <= (b)) +#define DIVCEIL(n, d) (((n) + ((d) - 1)) / (d)) +#define DEFAULT(a, b) (a) = (a) ? (a) : (b) +#define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) +#define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \ + (a).bg != (b).bg) +#define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \ + (t1.tv_nsec-t2.tv_nsec)/1E6) +#define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) + +#define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b)) +#define IS_TRUECOL(x) (1 << 24 & (x)) + +enum glyph_attribute { + ATTR_NULL = 0, + ATTR_BOLD = 1 << 0, + ATTR_FAINT = 1 << 1, + ATTR_ITALIC = 1 << 2, + ATTR_UNDERLINE = 1 << 3, + ATTR_BLINK = 1 << 4, + ATTR_REVERSE = 1 << 5, + ATTR_INVISIBLE = 1 << 6, + ATTR_STRUCK = 1 << 7, + ATTR_WRAP = 1 << 8, + ATTR_WIDE = 1 << 9, + ATTR_WDUMMY = 1 << 10, + ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, +}; + +enum selection_mode { + SEL_IDLE = 0, + SEL_EMPTY = 1, + SEL_READY = 2 +}; + +enum selection_type { + SEL_REGULAR = 1, + SEL_RECTANGULAR = 2 +}; + +enum selection_snap { + SNAP_WORD = 1, + SNAP_LINE = 2 +}; + +typedef unsigned char uchar; +typedef unsigned int uint; +typedef unsigned long ulong; +typedef unsigned short ushort; + +typedef uint_least32_t Rune; + +#define Glyph Glyph_ +typedef struct { + Rune u; /* character code */ + ushort mode; /* attribute flags */ + uint32_t fg; /* foreground */ + uint32_t bg; /* background */ +} Glyph; + +typedef Glyph *Line; + +typedef union { + int i; + uint ui; + float f; + const void *v; + const char *s; +} Arg; + +void die(const char *, ...); +void redraw(void); +void draw(void); + +void printscreen(const Arg *); +void printsel(const Arg *); +void sendbreak(const Arg *); +void toggleprinter(const Arg *); + +int tattrset(int); +void tnew(int, int); +void tresize(int, int); +void tsetdirtattr(int); +void ttyhangup(void); +int ttynew(const char *, char *, const char *, char **); +size_t ttyread(void); +void ttyresize(int, int); +void ttywrite(const char *, size_t, int); + +void resettitle(void); + +void selclear(void); +void selinit(void); +void selstart(int, int, int); +void selextend(int, int, int, int); +int selected(int, int); +char *getsel(void); + +size_t utf8encode(Rune, char *); + +void *xmalloc(size_t); +void *xrealloc(void *, size_t); +char *xstrdup(const char *); + +/* config.h globals */ +extern char *utmp; +extern char *scroll; +extern char *stty_args; +extern char *vtiden; +extern wchar_t *worddelimiters; +extern int allowaltscreen; +extern int allowwindowops; +extern char *termname; +extern unsigned int tabspaces; +extern unsigned int defaultfg; +extern unsigned int defaultbg; +extern unsigned int defaultcs; diff --git a/st.info b/st.info new file mode 100644 index 0000000..efab2cf --- /dev/null +++ b/st.info @@ -0,0 +1,243 @@ +st-mono| simpleterm monocolor, + acsc=+C\,D-A.B0E``aaffgghFiGjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, + am, + bce, + bel=^G, + blink=\E[5m, + bold=\E[1m, + cbt=\E[Z, + cvvis=\E[?25h, + civis=\E[?25l, + clear=\E[H\E[2J, + cnorm=\E[?12l\E[?25h, + colors#2, + cols#80, + cr=^M, + csr=\E[%i%p1%d;%p2%dr, + cub=\E[%p1%dD, + cub1=^H, + cud1=^J, + cud=\E[%p1%dB, + cuf1=\E[C, + cuf=\E[%p1%dC, + cup=\E[%i%p1%d;%p2%dH, + cuu1=\E[A, + cuu=\E[%p1%dA, + dch=\E[%p1%dP, + dch1=\E[P, + dim=\E[2m, + dl=\E[%p1%dM, + dl1=\E[M, + ech=\E[%p1%dX, + ed=\E[J, + el=\E[K, + el1=\E[1K, + enacs=\E)0, + flash=\E[?5h$<80/>\E[?5l, + fsl=^G, + home=\E[H, + hpa=\E[%i%p1%dG, + hs, + ht=^I, + hts=\EH, + ich=\E[%p1%d@, + il1=\E[L, + il=\E[%p1%dL, + ind=^J, + indn=\E[%p1%dS, + invis=\E[8m, + is2=\E[4l\E>\E[?1034l, + it#8, + kel=\E[1;2F, + ked=\E[1;5F, + ka1=\E[1~, + ka3=\E[5~, + kc1=\E[4~, + kc3=\E[6~, + kbs=\177, + kcbt=\E[Z, + kb2=\EOu, + kcub1=\EOD, + kcud1=\EOB, + kcuf1=\EOC, + kcuu1=\EOA, + kDC=\E[3;2~, + kent=\EOM, + kEND=\E[1;2F, + kIC=\E[2;2~, + kNXT=\E[6;2~, + kPRV=\E[5;2~, + kHOM=\E[1;2H, + kLFT=\E[1;2D, + kRIT=\E[1;2C, + kind=\E[1;2B, + kri=\E[1;2A, + kclr=\E[3;5~, + kdl1=\E[3;2~, + kdch1=\E[3~, + kich1=\E[2~, + kend=\E[4~, + kf1=\EOP, + kf2=\EOQ, + kf3=\EOR, + kf4=\EOS, + kf5=\E[15~, + kf6=\E[17~, + kf7=\E[18~, + kf8=\E[19~, + kf9=\E[20~, + kf10=\E[21~, + kf11=\E[23~, + kf12=\E[24~, + kf13=\E[1;2P, + kf14=\E[1;2Q, + kf15=\E[1;2R, + kf16=\E[1;2S, + kf17=\E[15;2~, + kf18=\E[17;2~, + kf19=\E[18;2~, + kf20=\E[19;2~, + kf21=\E[20;2~, + kf22=\E[21;2~, + kf23=\E[23;2~, + kf24=\E[24;2~, + kf25=\E[1;5P, + kf26=\E[1;5Q, + kf27=\E[1;5R, + kf28=\E[1;5S, + kf29=\E[15;5~, + kf30=\E[17;5~, + kf31=\E[18;5~, + kf32=\E[19;5~, + kf33=\E[20;5~, + kf34=\E[21;5~, + kf35=\E[23;5~, + kf36=\E[24;5~, + kf37=\E[1;6P, + kf38=\E[1;6Q, + kf39=\E[1;6R, + kf40=\E[1;6S, + kf41=\E[15;6~, + kf42=\E[17;6~, + kf43=\E[18;6~, + kf44=\E[19;6~, + kf45=\E[20;6~, + kf46=\E[21;6~, + kf47=\E[23;6~, + kf48=\E[24;6~, + kf49=\E[1;3P, + kf50=\E[1;3Q, + kf51=\E[1;3R, + kf52=\E[1;3S, + kf53=\E[15;3~, + kf54=\E[17;3~, + kf55=\E[18;3~, + kf56=\E[19;3~, + kf57=\E[20;3~, + kf58=\E[21;3~, + kf59=\E[23;3~, + kf60=\E[24;3~, + kf61=\E[1;4P, + kf62=\E[1;4Q, + kf63=\E[1;4R, + khome=\E[1~, + kil1=\E[2;5~, + krmir=\E[2;2~, + knp=\E[6~, + kmous=\E[M, + kpp=\E[5~, + lines#24, + mir, + msgr, + npc, + op=\E[39;49m, + pairs#64, + mc0=\E[i, + mc4=\E[4i, + mc5=\E[5i, + rc=\E8, + rev=\E[7m, + ri=\EM, + rin=\E[%p1%dT, + ritm=\E[23m, + rmacs=\E(B, + rmcup=\E[?1049l, + rmir=\E[4l, + rmkx=\E[?1l\E>, + rmso=\E[27m, + rmul=\E[24m, + rs1=\Ec, + rs2=\E[4l\E>\E[?1034l, + sc=\E7, + sitm=\E[3m, + sgr0=\E[0m, + smacs=\E(0, + smcup=\E[?1049h, + smir=\E[4h, + smkx=\E[?1h\E=, + smso=\E[7m, + smul=\E[4m, + tbc=\E[3g, + tsl=\E]0;, + xenl, + vpa=\E[%i%p1%dd, +# XTerm extensions + rmxx=\E[29m, + smxx=\E[9m, + BE=\E[?2004h, + BD=\E[?2004l, + PS=\E[200~, + PE=\E[201~, +# disabled rep for now: causes some issues with older ncurses versions. +# rep=%p1%c\E[%p2%{1}%-%db, +# tmux extensions, see TERMINFO EXTENSIONS in tmux(1) + Tc, + Ms=\E]52;%p1%s;%p2%s\007, + Se=\E[2 q, + Ss=\E[%p1%d q, + +st| simpleterm, + use=st-mono, + colors#8, + setab=\E[4%p1%dm, + setaf=\E[3%p1%dm, + setb=\E[4%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m, + setf=\E[3%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m, + sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m, + +st-256color| simpleterm with 256 colors, + use=st, + ccc, + colors#256, + oc=\E]104\007, + pairs#32767, +# Nicked from xterm-256color + initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, + setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m, + +st-meta| simpleterm with meta key, + use=st, + km, + rmm=\E[?1034l, + smm=\E[?1034h, + rs2=\E[4l\E>\E[?1034h, + is2=\E[4l\E>\E[?1034h, + +st-meta-256color| simpleterm with meta key and 256 colors, + use=st-256color, + km, + rmm=\E[?1034l, + smm=\E[?1034h, + rs2=\E[4l\E>\E[?1034h, + is2=\E[4l\E>\E[?1034h, + +st-bs| simpleterm with backspace as backspace, + use=st, + kbs=\010, + kdch1=\177, + +st-bs-256color| simpleterm with backspace as backspace and 256colors, + use=st-256color, + kbs=\010, + kdch1=\177, diff --git a/win.h b/win.h new file mode 100644 index 0000000..6de960d --- /dev/null +++ b/win.h @@ -0,0 +1,41 @@ +/* See LICENSE for license details. */ + +enum win_mode { + MODE_VISIBLE = 1 << 0, + MODE_FOCUSED = 1 << 1, + MODE_APPKEYPAD = 1 << 2, + MODE_MOUSEBTN = 1 << 3, + MODE_MOUSEMOTION = 1 << 4, + MODE_REVERSE = 1 << 5, + MODE_KBDLOCK = 1 << 6, + MODE_HIDE = 1 << 7, + MODE_APPCURSOR = 1 << 8, + MODE_MOUSESGR = 1 << 9, + MODE_8BIT = 1 << 10, + MODE_BLINK = 1 << 11, + MODE_FBLINK = 1 << 12, + MODE_FOCUS = 1 << 13, + MODE_MOUSEX10 = 1 << 14, + MODE_MOUSEMANY = 1 << 15, + MODE_BRCKTPASTE = 1 << 16, + MODE_NUMLOCK = 1 << 17, + MODE_MOUSE = MODE_MOUSEBTN|MODE_MOUSEMOTION|MODE_MOUSEX10\ + |MODE_MOUSEMANY, +}; + +void xbell(void); +void xclipcopy(void); +void xdrawcursor(int, int, Glyph, int, int, Glyph); +void xdrawline(Line, int, int, int); +void xfinishdraw(void); +void xloadcols(void); +int xsetcolorname(int, const char *); +int xgetcolor(int, unsigned char *, unsigned char *, unsigned char *); +void xseticontitle(char *); +void xsettitle(char *); +int xsetcursor(int); +void xsetmode(int, unsigned int); +void xsetpointermotion(int); +void xsetsel(char *); +int xstartdraw(void); +void xximspot(int, int); diff --git a/x.c b/x.c new file mode 100644 index 0000000..d73152b --- /dev/null +++ b/x.c @@ -0,0 +1,2108 @@ +/* See LICENSE for license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +char *argv0; +#include "arg.h" +#include "st.h" +#include "win.h" + +/* types used in config.h */ +typedef struct { + uint mod; + KeySym keysym; + void (*func)(const Arg *); + const Arg arg; +} Shortcut; + +typedef struct { + uint mod; + uint button; + void (*func)(const Arg *); + const Arg arg; + uint release; +} MouseShortcut; + +typedef struct { + KeySym k; + uint mask; + char *s; + /* three-valued logic variables: 0 indifferent, 1 on, -1 off */ + signed char appkey; /* application keypad */ + signed char appcursor; /* application cursor */ +} Key; + +/* X modifiers */ +#define XK_ANY_MOD UINT_MAX +#define XK_NO_MOD 0 +#define XK_SWITCH_MOD (1<<13|1<<14) + +/* function definitions used in config.h */ +static void clipcopy(const Arg *); +static void clippaste(const Arg *); +static void numlock(const Arg *); +static void selpaste(const Arg *); +static void zoom(const Arg *); +static void zoomabs(const Arg *); +static void zoomreset(const Arg *); +static void ttysend(const Arg *); + +/* config.h for applying patches and the configuration. */ +#include "config.h" + +/* XEMBED messages */ +#define XEMBED_FOCUS_IN 4 +#define XEMBED_FOCUS_OUT 5 + +/* macros */ +#define IS_SET(flag) ((win.mode & (flag)) != 0) +#define TRUERED(x) (((x) & 0xff0000) >> 8) +#define TRUEGREEN(x) (((x) & 0xff00)) +#define TRUEBLUE(x) (((x) & 0xff) << 8) + +typedef XftDraw *Draw; +typedef XftColor Color; +typedef XftGlyphFontSpec GlyphFontSpec; + +/* Purely graphic info */ +typedef struct { + int tw, th; /* tty width and height */ + int w, h; /* window width and height */ + int ch; /* char height */ + int cw; /* char width */ + int mode; /* window state/mode flags */ + int cursor; /* cursor style */ +} TermWindow; + +typedef struct { + Display *dpy; + Colormap cmap; + Window win; + Drawable buf; + GlyphFontSpec *specbuf; /* font spec buffer used for rendering */ + Atom xembed, wmdeletewin, netwmname, netwmiconname, netwmpid; + struct { + XIM xim; + XIC xic; + XPoint spot; + XVaNestedList spotlist; + } ime; + Draw draw; + Visual *vis; + XSetWindowAttributes attrs; + int scr; + int isfixed; /* is fixed geometry? */ + int l, t; /* left and top offset */ + int gm; /* geometry mask */ +} XWindow; + +typedef struct { + Atom xtarget; + char *primary, *clipboard; + struct timespec tclick1; + struct timespec tclick2; +} XSelection; + +/* Font structure */ +#define Font Font_ +typedef struct { + int height; + int width; + int ascent; + int descent; + int badslant; + int badweight; + short lbearing; + short rbearing; + XftFont *match; + FcFontSet *set; + FcPattern *pattern; +} Font; + +/* Drawing Context */ +typedef struct { + Color *col; + size_t collen; + Font font, bfont, ifont, ibfont; + GC gc; +} DC; + +static inline ushort sixd_to_16bit(int); +static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int); +static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int); +static void xdrawglyph(Glyph, int, int); +static void xclear(int, int, int, int); +static int xgeommasktogravity(int); +static int ximopen(Display *); +static void ximinstantiate(Display *, XPointer, XPointer); +static void ximdestroy(XIM, XPointer, XPointer); +static int xicdestroy(XIC, XPointer, XPointer); +static void xinit(int, int); +static void cresize(int, int); +static void xresize(int, int); +static void xhints(void); +static int xloadcolor(int, const char *, Color *); +static int xloadfont(Font *, FcPattern *); +static void xloadfonts(const char *, double); +static void xunloadfont(Font *); +static void xunloadfonts(void); +static void xsetenv(void); +static void xseturgency(int); +static int evcol(XEvent *); +static int evrow(XEvent *); + +static void expose(XEvent *); +static void visibility(XEvent *); +static void unmap(XEvent *); +static void kpress(XEvent *); +static void cmessage(XEvent *); +static void resize(XEvent *); +static void focus(XEvent *); +static uint buttonmask(uint); +static int mouseaction(XEvent *, uint); +static void brelease(XEvent *); +static void bpress(XEvent *); +static void bmotion(XEvent *); +static void propnotify(XEvent *); +static void selnotify(XEvent *); +static void selclear_(XEvent *); +static void selrequest(XEvent *); +static void setsel(char *, Time); +static void mousesel(XEvent *, int); +static void mousereport(XEvent *); +static char *kmap(KeySym, uint); +static int match(uint, uint); + +static void run(void); +static void usage(void); + +static void (*handler[LASTEvent])(XEvent *) = { + [KeyPress] = kpress, + [ClientMessage] = cmessage, + [ConfigureNotify] = resize, + [VisibilityNotify] = visibility, + [UnmapNotify] = unmap, + [Expose] = expose, + [FocusIn] = focus, + [FocusOut] = focus, + [MotionNotify] = bmotion, + [ButtonPress] = bpress, + [ButtonRelease] = brelease, +/* + * Uncomment if you want the selection to disappear when you select something + * different in another window. + */ +/* [SelectionClear] = selclear_, */ + [SelectionNotify] = selnotify, +/* + * PropertyNotify is only turned on when there is some INCR transfer happening + * for the selection retrieval. + */ + [PropertyNotify] = propnotify, + [SelectionRequest] = selrequest, +}; + +/* Globals */ +static DC dc; +static XWindow xw; +static XSelection xsel; +static TermWindow win; + +/* Font Ring Cache */ +enum { + FRC_NORMAL, + FRC_ITALIC, + FRC_BOLD, + FRC_ITALICBOLD +}; + +typedef struct { + XftFont *font; + int flags; + Rune unicodep; +} Fontcache; + +/* Fontcache is an array now. A new font will be appended to the array. */ +static Fontcache *frc = NULL; +static int frclen = 0; +static int frccap = 0; +static char *usedfont = NULL; +static double usedfontsize = 0; +static double defaultfontsize = 0; + +static char *opt_class = NULL; +static char **opt_cmd = NULL; +static char *opt_embed = NULL; +static char *opt_font = NULL; +static char *opt_io = NULL; +static char *opt_line = NULL; +static char *opt_name = NULL; +static char *opt_title = NULL; + +static uint buttons; /* bit field of pressed buttons */ + +void +clipcopy(const Arg *dummy) +{ + Atom clipboard; + + free(xsel.clipboard); + xsel.clipboard = NULL; + + if (xsel.primary != NULL) { + xsel.clipboard = xstrdup(xsel.primary); + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + XSetSelectionOwner(xw.dpy, clipboard, xw.win, CurrentTime); + } +} + +void +clippaste(const Arg *dummy) +{ + Atom clipboard; + + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + XConvertSelection(xw.dpy, clipboard, xsel.xtarget, clipboard, + xw.win, CurrentTime); +} + +void +selpaste(const Arg *dummy) +{ + XConvertSelection(xw.dpy, XA_PRIMARY, xsel.xtarget, XA_PRIMARY, + xw.win, CurrentTime); +} + +void +numlock(const Arg *dummy) +{ + win.mode ^= MODE_NUMLOCK; +} + +void +zoom(const Arg *arg) +{ + Arg larg; + + larg.f = usedfontsize + arg->f; + zoomabs(&larg); +} + +void +zoomabs(const Arg *arg) +{ + xunloadfonts(); + xloadfonts(usedfont, arg->f); + cresize(0, 0); + redraw(); + xhints(); +} + +void +zoomreset(const Arg *arg) +{ + Arg larg; + + if (defaultfontsize > 0) { + larg.f = defaultfontsize; + zoomabs(&larg); + } +} + +void +ttysend(const Arg *arg) +{ + ttywrite(arg->s, strlen(arg->s), 1); +} + +int +evcol(XEvent *e) +{ + int x = e->xbutton.x - borderpx; + LIMIT(x, 0, win.tw - 1); + return x / win.cw; +} + +int +evrow(XEvent *e) +{ + int y = e->xbutton.y - borderpx; + LIMIT(y, 0, win.th - 1); + return y / win.ch; +} + +void +mousesel(XEvent *e, int done) +{ + int type, seltype = SEL_REGULAR; + uint state = e->xbutton.state & ~(Button1Mask | forcemousemod); + + for (type = 1; type < LEN(selmasks); ++type) { + if (match(selmasks[type], state)) { + seltype = type; + break; + } + } + selextend(evcol(e), evrow(e), seltype, done); + if (done) + setsel(getsel(), e->xbutton.time); +} + +void +mousereport(XEvent *e) +{ + int len, btn, code; + int x = evcol(e), y = evrow(e); + int state = e->xbutton.state; + char buf[40]; + static int ox, oy; + + if (e->type == MotionNotify) { + if (x == ox && y == oy) + return; + if (!IS_SET(MODE_MOUSEMOTION) && !IS_SET(MODE_MOUSEMANY)) + return; + /* MODE_MOUSEMOTION: no reporting if no button is pressed */ + if (IS_SET(MODE_MOUSEMOTION) && buttons == 0) + return; + /* Set btn to lowest-numbered pressed button, or 12 if no + * buttons are pressed. */ + for (btn = 1; btn <= 11 && !(buttons & (1<<(btn-1))); btn++) + ; + code = 32; + } else { + btn = e->xbutton.button; + /* Only buttons 1 through 11 can be encoded */ + if (btn < 1 || btn > 11) + return; + if (e->type == ButtonRelease) { + /* MODE_MOUSEX10: no button release reporting */ + if (IS_SET(MODE_MOUSEX10)) + return; + /* Don't send release events for the scroll wheel */ + if (btn == 4 || btn == 5) + return; + } + code = 0; + } + + ox = x; + oy = y; + + /* Encode btn into code. If no button is pressed for a motion event in + * MODE_MOUSEMANY, then encode it as a release. */ + if ((!IS_SET(MODE_MOUSESGR) && e->type == ButtonRelease) || btn == 12) + code += 3; + else if (btn >= 8) + code += 128 + btn - 8; + else if (btn >= 4) + code += 64 + btn - 4; + else + code += btn - 1; + + if (!IS_SET(MODE_MOUSEX10)) { + code += ((state & ShiftMask ) ? 4 : 0) + + ((state & Mod1Mask ) ? 8 : 0) /* meta key: alt */ + + ((state & ControlMask) ? 16 : 0); + } + + if (IS_SET(MODE_MOUSESGR)) { + len = snprintf(buf, sizeof(buf), "\033[<%d;%d;%d%c", + code, x+1, y+1, + e->type == ButtonRelease ? 'm' : 'M'); + } else if (x < 223 && y < 223) { + len = snprintf(buf, sizeof(buf), "\033[M%c%c%c", + 32+code, 32+x+1, 32+y+1); + } else { + return; + } + + ttywrite(buf, len, 0); +} + +uint +buttonmask(uint button) +{ + return button == Button1 ? Button1Mask + : button == Button2 ? Button2Mask + : button == Button3 ? Button3Mask + : button == Button4 ? Button4Mask + : button == Button5 ? Button5Mask + : 0; +} + +int +mouseaction(XEvent *e, uint release) +{ + MouseShortcut *ms; + + /* ignore Buttonmask for Button - it's set on release */ + uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { + if (ms->release == release && + ms->button == e->xbutton.button && + (match(ms->mod, state) || /* exact or forced */ + match(ms->mod, state & ~forcemousemod))) { + ms->func(&(ms->arg)); + return 1; + } + } + + return 0; +} + +void +bpress(XEvent *e) +{ + int btn = e->xbutton.button; + struct timespec now; + int snap; + + if (1 <= btn && btn <= 11) + buttons |= 1 << (btn-1); + + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + if (mouseaction(e, 0)) + return; + + if (btn == Button1) { + /* + * If the user clicks below predefined timeouts specific + * snapping behaviour is exposed. + */ + clock_gettime(CLOCK_MONOTONIC, &now); + if (TIMEDIFF(now, xsel.tclick2) <= tripleclicktimeout) { + snap = SNAP_LINE; + } else if (TIMEDIFF(now, xsel.tclick1) <= doubleclicktimeout) { + snap = SNAP_WORD; + } else { + snap = 0; + } + xsel.tclick2 = xsel.tclick1; + xsel.tclick1 = now; + + selstart(evcol(e), evrow(e), snap); + } +} + +void +propnotify(XEvent *e) +{ + XPropertyEvent *xpev; + Atom clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + + xpev = &e->xproperty; + if (xpev->state == PropertyNewValue && + (xpev->atom == XA_PRIMARY || + xpev->atom == clipboard)) { + selnotify(e); + } +} + +void +selnotify(XEvent *e) +{ + ulong nitems, ofs, rem; + int format; + uchar *data, *last, *repl; + Atom type, incratom, property = None; + + incratom = XInternAtom(xw.dpy, "INCR", 0); + + ofs = 0; + if (e->type == SelectionNotify) + property = e->xselection.property; + else if (e->type == PropertyNotify) + property = e->xproperty.atom; + + if (property == None) + return; + + do { + if (XGetWindowProperty(xw.dpy, xw.win, property, ofs, + BUFSIZ/4, False, AnyPropertyType, + &type, &format, &nitems, &rem, + &data)) { + fprintf(stderr, "Clipboard allocation failed\n"); + return; + } + + if (e->type == PropertyNotify && nitems == 0 && rem == 0) { + /* + * If there is some PropertyNotify with no data, then + * this is the signal of the selection owner that all + * data has been transferred. We won't need to receive + * PropertyNotify events anymore. + */ + MODBIT(xw.attrs.event_mask, 0, PropertyChangeMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, + &xw.attrs); + } + + if (type == incratom) { + /* + * Activate the PropertyNotify events so we receive + * when the selection owner does send us the next + * chunk of data. + */ + MODBIT(xw.attrs.event_mask, 1, PropertyChangeMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, + &xw.attrs); + + /* + * Deleting the property is the transfer start signal. + */ + XDeleteProperty(xw.dpy, xw.win, (int)property); + continue; + } + + /* + * As seen in getsel: + * Line endings are inconsistent in the terminal and GUI world + * copy and pasting. When receiving some selection data, + * replace all '\n' with '\r'. + * FIXME: Fix the computer world. + */ + repl = data; + last = data + nitems * format / 8; + while ((repl = memchr(repl, '\n', last - repl))) { + *repl++ = '\r'; + } + + if (IS_SET(MODE_BRCKTPASTE) && ofs == 0) + ttywrite("\033[200~", 6, 0); + ttywrite((char *)data, nitems * format / 8, 1); + if (IS_SET(MODE_BRCKTPASTE) && rem == 0) + ttywrite("\033[201~", 6, 0); + XFree(data); + /* number of 32-bit chunks returned */ + ofs += nitems * format / 32; + } while (rem > 0); + + /* + * Deleting the property again tells the selection owner to send the + * next data chunk in the property. + */ + XDeleteProperty(xw.dpy, xw.win, (int)property); +} + +void +xclipcopy(void) +{ + clipcopy(NULL); +} + +void +selclear_(XEvent *e) +{ + selclear(); +} + +void +selrequest(XEvent *e) +{ + XSelectionRequestEvent *xsre; + XSelectionEvent xev; + Atom xa_targets, string, clipboard; + char *seltext; + + xsre = (XSelectionRequestEvent *) e; + xev.type = SelectionNotify; + xev.requestor = xsre->requestor; + xev.selection = xsre->selection; + xev.target = xsre->target; + xev.time = xsre->time; + if (xsre->property == None) + xsre->property = xsre->target; + + /* reject */ + xev.property = None; + + xa_targets = XInternAtom(xw.dpy, "TARGETS", 0); + if (xsre->target == xa_targets) { + /* respond with the supported type */ + string = xsel.xtarget; + XChangeProperty(xsre->display, xsre->requestor, xsre->property, + XA_ATOM, 32, PropModeReplace, + (uchar *) &string, 1); + xev.property = xsre->property; + } else if (xsre->target == xsel.xtarget || xsre->target == XA_STRING) { + /* + * xith XA_STRING non ascii characters may be incorrect in the + * requestor. It is not our problem, use utf8. + */ + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + if (xsre->selection == XA_PRIMARY) { + seltext = xsel.primary; + } else if (xsre->selection == clipboard) { + seltext = xsel.clipboard; + } else { + fprintf(stderr, + "Unhandled clipboard selection 0x%lx\n", + xsre->selection); + return; + } + if (seltext != NULL) { + XChangeProperty(xsre->display, xsre->requestor, + xsre->property, xsre->target, + 8, PropModeReplace, + (uchar *)seltext, strlen(seltext)); + xev.property = xsre->property; + } + } + + /* all done, send a notification to the listener */ + if (!XSendEvent(xsre->display, xsre->requestor, 1, 0, (XEvent *) &xev)) + fprintf(stderr, "Error sending SelectionNotify event\n"); +} + +void +setsel(char *str, Time t) +{ + if (!str) + return; + + free(xsel.primary); + xsel.primary = str; + + XSetSelectionOwner(xw.dpy, XA_PRIMARY, xw.win, t); + if (XGetSelectionOwner(xw.dpy, XA_PRIMARY) != xw.win) + selclear(); +} + +void +xsetsel(char *str) +{ + setsel(str, CurrentTime); +} + +void +brelease(XEvent *e) +{ + int btn = e->xbutton.button; + + if (1 <= btn && btn <= 11) + buttons &= ~(1 << (btn-1)); + + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + if (mouseaction(e, 1)) + return; + if (btn == Button1) + mousesel(e, 1); +} + +void +bmotion(XEvent *e) +{ + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + mousesel(e, 0); +} + +void +cresize(int width, int height) +{ + int col, row; + + if (width != 0) + win.w = width; + if (height != 0) + win.h = height; + + col = (win.w - 2 * borderpx) / win.cw; + row = (win.h - 2 * borderpx) / win.ch; + col = MAX(1, col); + row = MAX(1, row); + + tresize(col, row); + xresize(col, row); + ttyresize(win.tw, win.th); +} + +void +xresize(int col, int row) +{ + win.tw = col * win.cw; + win.th = row * win.ch; + + XFreePixmap(xw.dpy, xw.buf); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, + DefaultDepth(xw.dpy, xw.scr)); + XftDrawChange(xw.draw, xw.buf); + xclear(0, 0, win.w, win.h); + + /* resize to new width */ + xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec)); +} + +ushort +sixd_to_16bit(int x) +{ + return x == 0 ? 0 : 0x3737 + 0x2828 * x; +} + +int +xloadcolor(int i, const char *name, Color *ncolor) +{ + XRenderColor color = { .alpha = 0xffff }; + + if (!name) { + if (BETWEEN(i, 16, 255)) { /* 256 color */ + if (i < 6*6*6+16) { /* same colors as xterm */ + color.red = sixd_to_16bit( ((i-16)/36)%6 ); + color.green = sixd_to_16bit( ((i-16)/6) %6 ); + color.blue = sixd_to_16bit( ((i-16)/1) %6 ); + } else { /* greyscale */ + color.red = 0x0808 + 0x0a0a * (i - (6*6*6+16)); + color.green = color.blue = color.red; + } + return XftColorAllocValue(xw.dpy, xw.vis, + xw.cmap, &color, ncolor); + } else + name = colorname[i]; + } + + return XftColorAllocName(xw.dpy, xw.vis, xw.cmap, name, ncolor); +} + +void +xloadcols(void) +{ + int i; + static int loaded; + Color *cp; + + if (loaded) { + for (cp = dc.col; cp < &dc.col[dc.collen]; ++cp) + XftColorFree(xw.dpy, xw.vis, xw.cmap, cp); + } else { + dc.collen = MAX(LEN(colorname), 256); + dc.col = xmalloc(dc.collen * sizeof(Color)); + } + + for (i = 0; i < dc.collen; i++) + if (!xloadcolor(i, NULL, &dc.col[i])) { + if (colorname[i]) + die("could not allocate color '%s'\n", colorname[i]); + else + die("could not allocate color %d\n", i); + } + loaded = 1; +} + +int +xgetcolor(int x, unsigned char *r, unsigned char *g, unsigned char *b) +{ + if (!BETWEEN(x, 0, dc.collen - 1)) + return 1; + + *r = dc.col[x].color.red >> 8; + *g = dc.col[x].color.green >> 8; + *b = dc.col[x].color.blue >> 8; + + return 0; +} + +int +xsetcolorname(int x, const char *name) +{ + Color ncolor; + + if (!BETWEEN(x, 0, dc.collen - 1)) + return 1; + + if (!xloadcolor(x, name, &ncolor)) + return 1; + + XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]); + dc.col[x] = ncolor; + + return 0; +} + +/* + * Absolute coordinates. + */ +void +xclear(int x1, int y1, int x2, int y2) +{ + XftDrawRect(xw.draw, + &dc.col[IS_SET(MODE_REVERSE)? defaultfg : defaultbg], + x1, y1, x2-x1, y2-y1); +} + +void +xhints(void) +{ + XClassHint class = {opt_name ? opt_name : termname, + opt_class ? opt_class : termname}; + XWMHints wm = {.flags = InputHint, .input = 1}; + XSizeHints *sizeh; + + sizeh = XAllocSizeHints(); + + sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize; + sizeh->height = win.h; + sizeh->width = win.w; + sizeh->height_inc = win.ch; + sizeh->width_inc = win.cw; + sizeh->base_height = 2 * borderpx; + sizeh->base_width = 2 * borderpx; + sizeh->min_height = win.ch + 2 * borderpx; + sizeh->min_width = win.cw + 2 * borderpx; + if (xw.isfixed) { + sizeh->flags |= PMaxSize; + sizeh->min_width = sizeh->max_width = win.w; + sizeh->min_height = sizeh->max_height = win.h; + } + if (xw.gm & (XValue|YValue)) { + sizeh->flags |= USPosition | PWinGravity; + sizeh->x = xw.l; + sizeh->y = xw.t; + sizeh->win_gravity = xgeommasktogravity(xw.gm); + } + + XSetWMProperties(xw.dpy, xw.win, NULL, NULL, NULL, 0, sizeh, &wm, + &class); + XFree(sizeh); +} + +int +xgeommasktogravity(int mask) +{ + switch (mask & (XNegative|YNegative)) { + case 0: + return NorthWestGravity; + case XNegative: + return NorthEastGravity; + case YNegative: + return SouthWestGravity; + } + + return SouthEastGravity; +} + +int +xloadfont(Font *f, FcPattern *pattern) +{ + FcPattern *configured; + FcPattern *match; + FcResult result; + XGlyphInfo extents; + int wantattr, haveattr; + + /* + * Manually configure instead of calling XftMatchFont + * so that we can use the configured pattern for + * "missing glyph" lookups. + */ + configured = FcPatternDuplicate(pattern); + if (!configured) + return 1; + + FcConfigSubstitute(NULL, configured, FcMatchPattern); + XftDefaultSubstitute(xw.dpy, xw.scr, configured); + + match = FcFontMatch(NULL, configured, &result); + if (!match) { + FcPatternDestroy(configured); + return 1; + } + + if (!(f->match = XftFontOpenPattern(xw.dpy, match))) { + FcPatternDestroy(configured); + FcPatternDestroy(match); + return 1; + } + + if ((XftPatternGetInteger(pattern, "slant", 0, &wantattr) == + XftResultMatch)) { + /* + * Check if xft was unable to find a font with the appropriate + * slant but gave us one anyway. Try to mitigate. + */ + if ((XftPatternGetInteger(f->match->pattern, "slant", 0, + &haveattr) != XftResultMatch) || haveattr < wantattr) { + f->badslant = 1; + fputs("font slant does not match\n", stderr); + } + } + + if ((XftPatternGetInteger(pattern, "weight", 0, &wantattr) == + XftResultMatch)) { + if ((XftPatternGetInteger(f->match->pattern, "weight", 0, + &haveattr) != XftResultMatch) || haveattr != wantattr) { + f->badweight = 1; + fputs("font weight does not match\n", stderr); + } + } + + XftTextExtentsUtf8(xw.dpy, f->match, + (const FcChar8 *) ascii_printable, + strlen(ascii_printable), &extents); + + f->set = NULL; + f->pattern = configured; + + f->ascent = f->match->ascent; + f->descent = f->match->descent; + f->lbearing = 0; + f->rbearing = f->match->max_advance_width; + + f->height = f->ascent + f->descent; + f->width = DIVCEIL(extents.xOff, strlen(ascii_printable)); + + return 0; +} + +void +xloadfonts(const char *fontstr, double fontsize) +{ + FcPattern *pattern; + double fontval; + + if (fontstr[0] == '-') + pattern = XftXlfdParse(fontstr, False, False); + else + pattern = FcNameParse((const FcChar8 *)fontstr); + + if (!pattern) + die("can't open font %s\n", fontstr); + + if (fontsize > 1) { + FcPatternDel(pattern, FC_PIXEL_SIZE); + FcPatternDel(pattern, FC_SIZE); + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, (double)fontsize); + usedfontsize = fontsize; + } else { + if (FcPatternGetDouble(pattern, FC_PIXEL_SIZE, 0, &fontval) == + FcResultMatch) { + usedfontsize = fontval; + } else if (FcPatternGetDouble(pattern, FC_SIZE, 0, &fontval) == + FcResultMatch) { + usedfontsize = -1; + } else { + /* + * Default font size is 12, if none given. This is to + * have a known usedfontsize value. + */ + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12); + usedfontsize = 12; + } + defaultfontsize = usedfontsize; + } + + if (xloadfont(&dc.font, pattern)) + die("can't open font %s\n", fontstr); + + if (usedfontsize < 0) { + FcPatternGetDouble(dc.font.match->pattern, + FC_PIXEL_SIZE, 0, &fontval); + usedfontsize = fontval; + if (fontsize == 0) + defaultfontsize = fontval; + } + + /* Setting character width and height. */ + win.cw = ceilf(dc.font.width * cwscale); + win.ch = ceilf(dc.font.height * chscale); + + FcPatternDel(pattern, FC_SLANT); + FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ITALIC); + if (xloadfont(&dc.ifont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDel(pattern, FC_WEIGHT); + FcPatternAddInteger(pattern, FC_WEIGHT, FC_WEIGHT_BOLD); + if (xloadfont(&dc.ibfont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDel(pattern, FC_SLANT); + FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ROMAN); + if (xloadfont(&dc.bfont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDestroy(pattern); +} + +void +xunloadfont(Font *f) +{ + XftFontClose(xw.dpy, f->match); + FcPatternDestroy(f->pattern); + if (f->set) + FcFontSetDestroy(f->set); +} + +void +xunloadfonts(void) +{ + /* Free the loaded fonts in the font cache. */ + while (frclen > 0) + XftFontClose(xw.dpy, frc[--frclen].font); + + xunloadfont(&dc.font); + xunloadfont(&dc.bfont); + xunloadfont(&dc.ifont); + xunloadfont(&dc.ibfont); +} + +int +ximopen(Display *dpy) +{ + XIMCallback imdestroy = { .client_data = NULL, .callback = ximdestroy }; + XICCallback icdestroy = { .client_data = NULL, .callback = xicdestroy }; + + xw.ime.xim = XOpenIM(xw.dpy, NULL, NULL, NULL); + if (xw.ime.xim == NULL) + return 0; + + if (XSetIMValues(xw.ime.xim, XNDestroyCallback, &imdestroy, NULL)) + fprintf(stderr, "XSetIMValues: " + "Could not set XNDestroyCallback.\n"); + + xw.ime.spotlist = XVaCreateNestedList(0, XNSpotLocation, &xw.ime.spot, + NULL); + + if (xw.ime.xic == NULL) { + xw.ime.xic = XCreateIC(xw.ime.xim, XNInputStyle, + XIMPreeditNothing | XIMStatusNothing, + XNClientWindow, xw.win, + XNDestroyCallback, &icdestroy, + NULL); + } + if (xw.ime.xic == NULL) + fprintf(stderr, "XCreateIC: Could not create input context.\n"); + + return 1; +} + +void +ximinstantiate(Display *dpy, XPointer client, XPointer call) +{ + if (ximopen(dpy)) + XUnregisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); +} + +void +ximdestroy(XIM xim, XPointer client, XPointer call) +{ + xw.ime.xim = NULL; + XRegisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); + XFree(xw.ime.spotlist); +} + +int +xicdestroy(XIC xim, XPointer client, XPointer call) +{ + xw.ime.xic = NULL; + return 1; +} + +void +xinit(int cols, int rows) +{ + XGCValues gcvalues; + Cursor cursor; + Window parent, root; + pid_t thispid = getpid(); + XColor xmousefg, xmousebg; + + if (!(xw.dpy = XOpenDisplay(NULL))) + die("can't open display\n"); + xw.scr = XDefaultScreen(xw.dpy); + xw.vis = XDefaultVisual(xw.dpy, xw.scr); + + /* font */ + if (!FcInit()) + die("could not init fontconfig.\n"); + + usedfont = (opt_font == NULL)? font : opt_font; + xloadfonts(usedfont, 0); + + /* colors */ + xw.cmap = XDefaultColormap(xw.dpy, xw.scr); + xloadcols(); + + /* adjust fixed window geometry */ + win.w = 2 * borderpx + cols * win.cw; + win.h = 2 * borderpx + rows * win.ch; + if (xw.gm & XNegative) + xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2; + if (xw.gm & YNegative) + xw.t += DisplayHeight(xw.dpy, xw.scr) - win.h - 2; + + /* Events */ + xw.attrs.background_pixel = dc.col[defaultbg].pixel; + xw.attrs.border_pixel = dc.col[defaultbg].pixel; + xw.attrs.bit_gravity = NorthWestGravity; + xw.attrs.event_mask = FocusChangeMask | KeyPressMask | KeyReleaseMask + | ExposureMask | VisibilityChangeMask | StructureNotifyMask + | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask; + xw.attrs.colormap = xw.cmap; + + root = XRootWindow(xw.dpy, xw.scr); + if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) + parent = root; + xw.win = XCreateWindow(xw.dpy, root, xw.l, xw.t, + win.w, win.h, 0, XDefaultDepth(xw.dpy, xw.scr), InputOutput, + xw.vis, CWBackPixel | CWBorderPixel | CWBitGravity + | CWEventMask | CWColormap, &xw.attrs); + if (parent != root) + XReparentWindow(xw.dpy, xw.win, parent, xw.l, xw.t); + + memset(&gcvalues, 0, sizeof(gcvalues)); + gcvalues.graphics_exposures = False; + dc.gc = XCreateGC(xw.dpy, xw.win, GCGraphicsExposures, + &gcvalues); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, + DefaultDepth(xw.dpy, xw.scr)); + XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); + XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); + + /* font spec buffer */ + xw.specbuf = xmalloc(cols * sizeof(GlyphFontSpec)); + + /* Xft rendering context */ + xw.draw = XftDrawCreate(xw.dpy, xw.buf, xw.vis, xw.cmap); + + /* input methods */ + if (!ximopen(xw.dpy)) { + XRegisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); + } + + /* white cursor, black outline */ + cursor = XCreateFontCursor(xw.dpy, mouseshape); + XDefineCursor(xw.dpy, xw.win, cursor); + + if (XParseColor(xw.dpy, xw.cmap, colorname[mousefg], &xmousefg) == 0) { + xmousefg.red = 0xffff; + xmousefg.green = 0xffff; + xmousefg.blue = 0xffff; + } + + if (XParseColor(xw.dpy, xw.cmap, colorname[mousebg], &xmousebg) == 0) { + xmousebg.red = 0x0000; + xmousebg.green = 0x0000; + xmousebg.blue = 0x0000; + } + + XRecolorCursor(xw.dpy, cursor, &xmousefg, &xmousebg); + + xw.xembed = XInternAtom(xw.dpy, "_XEMBED", False); + xw.wmdeletewin = XInternAtom(xw.dpy, "WM_DELETE_WINDOW", False); + xw.netwmname = XInternAtom(xw.dpy, "_NET_WM_NAME", False); + xw.netwmiconname = XInternAtom(xw.dpy, "_NET_WM_ICON_NAME", False); + XSetWMProtocols(xw.dpy, xw.win, &xw.wmdeletewin, 1); + + xw.netwmpid = XInternAtom(xw.dpy, "_NET_WM_PID", False); + XChangeProperty(xw.dpy, xw.win, xw.netwmpid, XA_CARDINAL, 32, + PropModeReplace, (uchar *)&thispid, 1); + + win.mode = MODE_NUMLOCK; + resettitle(); + xhints(); + XMapWindow(xw.dpy, xw.win); + XSync(xw.dpy, False); + + clock_gettime(CLOCK_MONOTONIC, &xsel.tclick1); + clock_gettime(CLOCK_MONOTONIC, &xsel.tclick2); + xsel.primary = NULL; + xsel.clipboard = NULL; + xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0); + if (xsel.xtarget == None) + xsel.xtarget = XA_STRING; +} + +int +xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y) +{ + float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp; + ushort mode, prevmode = USHRT_MAX; + Font *font = &dc.font; + int frcflags = FRC_NORMAL; + float runewidth = win.cw; + Rune rune; + FT_UInt glyphidx; + FcResult fcres; + FcPattern *fcpattern, *fontpattern; + FcFontSet *fcsets[] = { NULL }; + FcCharSet *fccharset; + int i, f, numspecs = 0; + + for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) { + /* Fetch rune and mode for current glyph. */ + rune = glyphs[i].u; + mode = glyphs[i].mode; + + /* Skip dummy wide-character spacing. */ + if (mode == ATTR_WDUMMY) + continue; + + /* Determine font for glyph if different from previous glyph. */ + if (prevmode != mode) { + prevmode = mode; + font = &dc.font; + frcflags = FRC_NORMAL; + runewidth = win.cw * ((mode & ATTR_WIDE) ? 2.0f : 1.0f); + if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) { + font = &dc.ibfont; + frcflags = FRC_ITALICBOLD; + } else if (mode & ATTR_ITALIC) { + font = &dc.ifont; + frcflags = FRC_ITALIC; + } else if (mode & ATTR_BOLD) { + font = &dc.bfont; + frcflags = FRC_BOLD; + } + yp = winy + font->ascent; + } + + /* Lookup character index with default font. */ + glyphidx = XftCharIndex(xw.dpy, font->match, rune); + if (glyphidx) { + specs[numspecs].font = font->match; + specs[numspecs].glyph = glyphidx; + specs[numspecs].x = (short)xp; + specs[numspecs].y = (short)yp; + xp += runewidth; + numspecs++; + continue; + } + + /* Fallback on font cache, search the font cache for match. */ + for (f = 0; f < frclen; f++) { + glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune); + /* Everything correct. */ + if (glyphidx && frc[f].flags == frcflags) + break; + /* We got a default font for a not found glyph. */ + if (!glyphidx && frc[f].flags == frcflags + && frc[f].unicodep == rune) { + break; + } + } + + /* Nothing was found. Use fontconfig to find matching font. */ + if (f >= frclen) { + if (!font->set) + font->set = FcFontSort(0, font->pattern, + 1, 0, &fcres); + fcsets[0] = font->set; + + /* + * Nothing was found in the cache. Now use + * some dozen of Fontconfig calls to get the + * font for one single character. + * + * Xft and fontconfig are design failures. + */ + fcpattern = FcPatternDuplicate(font->pattern); + fccharset = FcCharSetCreate(); + + FcCharSetAddChar(fccharset, rune); + FcPatternAddCharSet(fcpattern, FC_CHARSET, + fccharset); + FcPatternAddBool(fcpattern, FC_SCALABLE, 1); + + FcConfigSubstitute(0, fcpattern, + FcMatchPattern); + FcDefaultSubstitute(fcpattern); + + fontpattern = FcFontSetMatch(0, fcsets, 1, + fcpattern, &fcres); + + /* Allocate memory for the new cache entry. */ + if (frclen >= frccap) { + frccap += 16; + frc = xrealloc(frc, frccap * sizeof(Fontcache)); + } + + frc[frclen].font = XftFontOpenPattern(xw.dpy, + fontpattern); + if (!frc[frclen].font) + die("XftFontOpenPattern failed seeking fallback font: %s\n", + strerror(errno)); + frc[frclen].flags = frcflags; + frc[frclen].unicodep = rune; + + glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune); + + f = frclen; + frclen++; + + FcPatternDestroy(fcpattern); + FcCharSetDestroy(fccharset); + } + + specs[numspecs].font = frc[f].font; + specs[numspecs].glyph = glyphidx; + specs[numspecs].x = (short)xp; + specs[numspecs].y = (short)yp; + xp += runewidth; + numspecs++; + } + + return numspecs; +} + +void +xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y) +{ + int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1); + int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, + width = charlen * win.cw; + Color *fg, *bg, *temp, revfg, revbg, truefg, truebg; + XRenderColor colfg, colbg; + XRectangle r; + + /* Fallback on color display for attributes not supported by the font */ + if (base.mode & ATTR_ITALIC && base.mode & ATTR_BOLD) { + if (dc.ibfont.badslant || dc.ibfont.badweight) + base.fg = defaultattr; + } else if ((base.mode & ATTR_ITALIC && dc.ifont.badslant) || + (base.mode & ATTR_BOLD && dc.bfont.badweight)) { + base.fg = defaultattr; + } + + if (IS_TRUECOL(base.fg)) { + colfg.alpha = 0xffff; + colfg.red = TRUERED(base.fg); + colfg.green = TRUEGREEN(base.fg); + colfg.blue = TRUEBLUE(base.fg); + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &truefg); + fg = &truefg; + } else { + fg = &dc.col[base.fg]; + } + + if (IS_TRUECOL(base.bg)) { + colbg.alpha = 0xffff; + colbg.green = TRUEGREEN(base.bg); + colbg.red = TRUERED(base.bg); + colbg.blue = TRUEBLUE(base.bg); + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colbg, &truebg); + bg = &truebg; + } else { + bg = &dc.col[base.bg]; + } + + /* Change basic system colors [0-7] to bright system colors [8-15] */ + if ((base.mode & ATTR_BOLD_FAINT) == ATTR_BOLD && BETWEEN(base.fg, 0, 7)) + fg = &dc.col[base.fg + 8]; + + if (IS_SET(MODE_REVERSE)) { + if (fg == &dc.col[defaultfg]) { + fg = &dc.col[defaultbg]; + } else { + colfg.red = ~fg->color.red; + colfg.green = ~fg->color.green; + colfg.blue = ~fg->color.blue; + colfg.alpha = fg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, + &revfg); + fg = &revfg; + } + + if (bg == &dc.col[defaultbg]) { + bg = &dc.col[defaultfg]; + } else { + colbg.red = ~bg->color.red; + colbg.green = ~bg->color.green; + colbg.blue = ~bg->color.blue; + colbg.alpha = bg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colbg, + &revbg); + bg = &revbg; + } + } + + if ((base.mode & ATTR_BOLD_FAINT) == ATTR_FAINT) { + colfg.red = fg->color.red / 2; + colfg.green = fg->color.green / 2; + colfg.blue = fg->color.blue / 2; + colfg.alpha = fg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &revfg); + fg = &revfg; + } + + if (base.mode & ATTR_REVERSE) { + temp = fg; + fg = bg; + bg = temp; + } + + if (base.mode & ATTR_BLINK && win.mode & MODE_BLINK) + fg = bg; + + if (base.mode & ATTR_INVISIBLE) + fg = bg; + + /* Intelligent cleaning up of the borders. */ + if (x == 0) { + xclear(0, (y == 0)? 0 : winy, borderpx, + winy + win.ch + + ((winy + win.ch >= borderpx + win.th)? win.h : 0)); + } + if (winx + width >= borderpx + win.tw) { + xclear(winx + width, (y == 0)? 0 : winy, win.w, + ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch))); + } + if (y == 0) + xclear(winx, 0, winx + width, borderpx); + if (winy + win.ch >= borderpx + win.th) + xclear(winx, winy + win.ch, winx + width, win.h); + + /* Clean up the region we want to draw to. */ + XftDrawRect(xw.draw, bg, winx, winy, width, win.ch); + + /* Set the clip region because Xft is sometimes dirty. */ + r.x = 0; + r.y = 0; + r.height = win.ch; + r.width = width; + XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1); + + /* Render the glyphs. */ + XftDrawGlyphFontSpec(xw.draw, fg, specs, len); + + /* Render underline and strikethrough. */ + if (base.mode & ATTR_UNDERLINE) { + XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1, + width, 1); + } + + if (base.mode & ATTR_STRUCK) { + XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3, + width, 1); + } + + /* Reset clip to none. */ + XftDrawSetClip(xw.draw, 0); +} + +void +xdrawglyph(Glyph g, int x, int y) +{ + int numspecs; + XftGlyphFontSpec spec; + + numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y); + xdrawglyphfontspecs(&spec, g, numspecs, x, y); +} + +void +xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) +{ + Color drawcol; + + /* remove the old cursor */ + if (selected(ox, oy)) + og.mode ^= ATTR_REVERSE; + xdrawglyph(og, ox, oy); + + if (IS_SET(MODE_HIDE)) + return; + + /* + * Select the right color for the right mode. + */ + g.mode &= ATTR_BOLD|ATTR_ITALIC|ATTR_UNDERLINE|ATTR_STRUCK|ATTR_WIDE; + + if (IS_SET(MODE_REVERSE)) { + g.mode |= ATTR_REVERSE; + g.bg = defaultfg; + if (selected(cx, cy)) { + drawcol = dc.col[defaultcs]; + g.fg = defaultrcs; + } else { + drawcol = dc.col[defaultrcs]; + g.fg = defaultcs; + } + } else { + if (selected(cx, cy)) { + g.fg = defaultfg; + g.bg = defaultrcs; + } else { + g.fg = defaultbg; + g.bg = defaultcs; + } + drawcol = dc.col[g.bg]; + } + + /* draw the new one */ + if (IS_SET(MODE_FOCUSED)) { + switch (win.cursor) { + case 7: /* st extension */ + g.u = 0x2603; /* snowman (U+2603) */ + /* FALLTHROUGH */ + case 0: /* Blinking Block */ + case 1: /* Blinking Block (Default) */ + case 2: /* Steady Block */ + xdrawglyph(g, cx, cy); + break; + case 3: /* Blinking Underline */ + case 4: /* Steady Underline */ + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + (cy + 1) * win.ch - \ + cursorthickness, + win.cw, cursorthickness); + break; + case 5: /* Blinking bar */ + case 6: /* Steady bar */ + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + cy * win.ch, + cursorthickness, win.ch); + break; + } + } else { + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + cy * win.ch, + win.cw - 1, 1); + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, + borderpx + (cx + 1) * win.cw - 1, + borderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + (cy + 1) * win.ch - 1, + win.cw, 1); + } +} + +void +xsetenv(void) +{ + char buf[sizeof(long) * 8 + 1]; + + snprintf(buf, sizeof(buf), "%lu", xw.win); + setenv("WINDOWID", buf, 1); +} + +void +xseticontitle(char *p) +{ + XTextProperty prop; + DEFAULT(p, opt_title); + + if (p[0] == '\0') + p = opt_title; + + if (Xutf8TextListToTextProperty(xw.dpy, &p, 1, XUTF8StringStyle, + &prop) != Success) + return; + XSetWMIconName(xw.dpy, xw.win, &prop); + XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmiconname); + XFree(prop.value); +} + +void +xsettitle(char *p) +{ + XTextProperty prop; + DEFAULT(p, opt_title); + + if (p[0] == '\0') + p = opt_title; + + if (Xutf8TextListToTextProperty(xw.dpy, &p, 1, XUTF8StringStyle, + &prop) != Success) + return; + XSetWMName(xw.dpy, xw.win, &prop); + XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmname); + XFree(prop.value); +} + +int +xstartdraw(void) +{ + return IS_SET(MODE_VISIBLE); +} + +void +xdrawline(Line line, int x1, int y1, int x2) +{ + int i, x, ox, numspecs; + Glyph base, new; + XftGlyphFontSpec *specs = xw.specbuf; + + numspecs = xmakeglyphfontspecs(specs, &line[x1], x2 - x1, x1, y1); + i = ox = 0; + for (x = x1; x < x2 && i < numspecs; x++) { + new = line[x]; + if (new.mode == ATTR_WDUMMY) + continue; + if (selected(x, y1)) + new.mode ^= ATTR_REVERSE; + if (i > 0 && ATTRCMP(base, new)) { + xdrawglyphfontspecs(specs, base, i, ox, y1); + specs += i; + numspecs -= i; + i = 0; + } + if (i == 0) { + ox = x; + base = new; + } + i++; + } + if (i > 0) + xdrawglyphfontspecs(specs, base, i, ox, y1); +} + +void +xfinishdraw(void) +{ + XCopyArea(xw.dpy, xw.buf, xw.win, dc.gc, 0, 0, win.w, + win.h, 0, 0); + XSetForeground(xw.dpy, dc.gc, + dc.col[IS_SET(MODE_REVERSE)? + defaultfg : defaultbg].pixel); +} + +void +xximspot(int x, int y) +{ + if (xw.ime.xic == NULL) + return; + + xw.ime.spot.x = borderpx + x * win.cw; + xw.ime.spot.y = borderpx + (y + 1) * win.ch; + + XSetICValues(xw.ime.xic, XNPreeditAttributes, xw.ime.spotlist, NULL); +} + +void +expose(XEvent *ev) +{ + redraw(); +} + +void +visibility(XEvent *ev) +{ + XVisibilityEvent *e = &ev->xvisibility; + + MODBIT(win.mode, e->state != VisibilityFullyObscured, MODE_VISIBLE); +} + +void +unmap(XEvent *ev) +{ + win.mode &= ~MODE_VISIBLE; +} + +void +xsetpointermotion(int set) +{ + MODBIT(xw.attrs.event_mask, set, PointerMotionMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, &xw.attrs); +} + +void +xsetmode(int set, unsigned int flags) +{ + int mode = win.mode; + MODBIT(win.mode, set, flags); + if ((win.mode & MODE_REVERSE) != (mode & MODE_REVERSE)) + redraw(); +} + +int +xsetcursor(int cursor) +{ + if (!BETWEEN(cursor, 0, 7)) /* 7: st extension */ + return 1; + win.cursor = cursor; + return 0; +} + +void +xseturgency(int add) +{ + XWMHints *h = XGetWMHints(xw.dpy, xw.win); + + MODBIT(h->flags, add, XUrgencyHint); + XSetWMHints(xw.dpy, xw.win, h); + XFree(h); +} + +void +xbell(void) +{ + if (!(IS_SET(MODE_FOCUSED))) + xseturgency(1); + if (bellvolume) + XkbBell(xw.dpy, xw.win, bellvolume, (Atom)NULL); +} + +void +focus(XEvent *ev) +{ + XFocusChangeEvent *e = &ev->xfocus; + + if (e->mode == NotifyGrab) + return; + + if (ev->type == FocusIn) { + if (xw.ime.xic) + XSetICFocus(xw.ime.xic); + win.mode |= MODE_FOCUSED; + xseturgency(0); + if (IS_SET(MODE_FOCUS)) + ttywrite("\033[I", 3, 0); + } else { + if (xw.ime.xic) + XUnsetICFocus(xw.ime.xic); + win.mode &= ~MODE_FOCUSED; + if (IS_SET(MODE_FOCUS)) + ttywrite("\033[O", 3, 0); + } +} + +int +match(uint mask, uint state) +{ + return mask == XK_ANY_MOD || mask == (state & ~ignoremod); +} + +char* +kmap(KeySym k, uint state) +{ + Key *kp; + int i; + + /* Check for mapped keys out of X11 function keys. */ + for (i = 0; i < LEN(mappedkeys); i++) { + if (mappedkeys[i] == k) + break; + } + if (i == LEN(mappedkeys)) { + if ((k & 0xFFFF) < 0xFD00) + return NULL; + } + + for (kp = key; kp < key + LEN(key); kp++) { + if (kp->k != k) + continue; + + if (!match(kp->mask, state)) + continue; + + if (IS_SET(MODE_APPKEYPAD) ? kp->appkey < 0 : kp->appkey > 0) + continue; + if (IS_SET(MODE_NUMLOCK) && kp->appkey == 2) + continue; + + if (IS_SET(MODE_APPCURSOR) ? kp->appcursor < 0 : kp->appcursor > 0) + continue; + + return kp->s; + } + + return NULL; +} + +void +kpress(XEvent *ev) +{ + XKeyEvent *e = &ev->xkey; + KeySym ksym = NoSymbol; + char buf[64], *customkey; + int len; + Rune c; + Status status; + Shortcut *bp; + + if (IS_SET(MODE_KBDLOCK)) + return; + + if (xw.ime.xic) { + len = XmbLookupString(xw.ime.xic, e, buf, sizeof buf, &ksym, &status); + if (status == XBufferOverflow) + return; + } else { + len = XLookupString(e, buf, sizeof buf, &ksym, NULL); + } + /* 1. shortcuts */ + for (bp = shortcuts; bp < shortcuts + LEN(shortcuts); bp++) { + if (ksym == bp->keysym && match(bp->mod, e->state)) { + bp->func(&(bp->arg)); + return; + } + } + + /* 2. custom keys from config.h */ + if ((customkey = kmap(ksym, e->state))) { + ttywrite(customkey, strlen(customkey), 1); + return; + } + + /* 3. composed string from input method */ + if (len == 0) + return; + if (len == 1 && e->state & Mod1Mask) { + if (IS_SET(MODE_8BIT)) { + if (*buf < 0177) { + c = *buf | 0x80; + len = utf8encode(c, buf); + } + } else { + buf[1] = buf[0]; + buf[0] = '\033'; + len = 2; + } + } + ttywrite(buf, len, 1); +} + +void +cmessage(XEvent *e) +{ + /* + * See xembed specs + * http://standards.freedesktop.org/xembed-spec/xembed-spec-latest.html + */ + if (e->xclient.message_type == xw.xembed && e->xclient.format == 32) { + if (e->xclient.data.l[1] == XEMBED_FOCUS_IN) { + win.mode |= MODE_FOCUSED; + xseturgency(0); + } else if (e->xclient.data.l[1] == XEMBED_FOCUS_OUT) { + win.mode &= ~MODE_FOCUSED; + } + } else if (e->xclient.data.l[0] == xw.wmdeletewin) { + ttyhangup(); + exit(0); + } +} + +void +resize(XEvent *e) +{ + if (e->xconfigure.width == win.w && e->xconfigure.height == win.h) + return; + + cresize(e->xconfigure.width, e->xconfigure.height); +} + +void +run(void) +{ + XEvent ev; + int w = win.w, h = win.h; + fd_set rfd; + int xfd = XConnectionNumber(xw.dpy), ttyfd, xev, drawing; + struct timespec seltv, *tv, now, lastblink, trigger; + double timeout; + + /* Waiting for window mapping */ + do { + XNextEvent(xw.dpy, &ev); + /* + * This XFilterEvent call is required because of XOpenIM. It + * does filter out the key event and some client message for + * the input method too. + */ + if (XFilterEvent(&ev, None)) + continue; + if (ev.type == ConfigureNotify) { + w = ev.xconfigure.width; + h = ev.xconfigure.height; + } + } while (ev.type != MapNotify); + + ttyfd = ttynew(opt_line, shell, opt_io, opt_cmd); + cresize(w, h); + + for (timeout = -1, drawing = 0, lastblink = (struct timespec){0};;) { + FD_ZERO(&rfd); + FD_SET(ttyfd, &rfd); + FD_SET(xfd, &rfd); + + if (XPending(xw.dpy)) + timeout = 0; /* existing events might not set xfd */ + + seltv.tv_sec = timeout / 1E3; + seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec); + tv = timeout >= 0 ? &seltv : NULL; + + if (pselect(MAX(xfd, ttyfd)+1, &rfd, NULL, NULL, tv, NULL) < 0) { + if (errno == EINTR) + continue; + die("select failed: %s\n", strerror(errno)); + } + clock_gettime(CLOCK_MONOTONIC, &now); + + if (FD_ISSET(ttyfd, &rfd)) + ttyread(); + + xev = 0; + while (XPending(xw.dpy)) { + xev = 1; + XNextEvent(xw.dpy, &ev); + if (XFilterEvent(&ev, None)) + continue; + if (handler[ev.type]) + (handler[ev.type])(&ev); + } + + /* + * To reduce flicker and tearing, when new content or event + * triggers drawing, we first wait a bit to ensure we got + * everything, and if nothing new arrives - we draw. + * We start with trying to wait minlatency ms. If more content + * arrives sooner, we retry with shorter and shorter periods, + * and eventually draw even without idle after maxlatency ms. + * Typically this results in low latency while interacting, + * maximum latency intervals during `cat huge.txt`, and perfect + * sync with periodic updates from animations/key-repeats/etc. + */ + if (FD_ISSET(ttyfd, &rfd) || xev) { + if (!drawing) { + trigger = now; + drawing = 1; + } + timeout = (maxlatency - TIMEDIFF(now, trigger)) \ + / maxlatency * minlatency; + if (timeout > 0) + continue; /* we have time, try to find idle */ + } + + /* idle detected or maxlatency exhausted -> draw */ + timeout = -1; + if (blinktimeout && tattrset(ATTR_BLINK)) { + timeout = blinktimeout - TIMEDIFF(now, lastblink); + if (timeout <= 0) { + if (-timeout > blinktimeout) /* start visible */ + win.mode |= MODE_BLINK; + win.mode ^= MODE_BLINK; + tsetdirtattr(ATTR_BLINK); + lastblink = now; + timeout = blinktimeout; + } + } + + draw(); + XFlush(xw.dpy); + drawing = 0; + } +} + +void +usage(void) +{ + die("usage: %s [-aiv] [-c class] [-f font] [-g geometry]" + " [-n name] [-o file]\n" + " [-T title] [-t title] [-w windowid]" + " [[-e] command [args ...]]\n" + " %s [-aiv] [-c class] [-f font] [-g geometry]" + " [-n name] [-o file]\n" + " [-T title] [-t title] [-w windowid] -l line" + " [stty_args ...]\n", argv0, argv0); +} + +int +main(int argc, char *argv[]) +{ + xw.l = xw.t = 0; + xw.isfixed = False; + xsetcursor(cursorshape); + + ARGBEGIN { + case 'a': + allowaltscreen = 0; + break; + case 'c': + opt_class = EARGF(usage()); + break; + case 'e': + if (argc > 0) + --argc, ++argv; + goto run; + case 'f': + opt_font = EARGF(usage()); + break; + case 'g': + xw.gm = XParseGeometry(EARGF(usage()), + &xw.l, &xw.t, &cols, &rows); + break; + case 'i': + xw.isfixed = 1; + break; + case 'o': + opt_io = EARGF(usage()); + break; + case 'l': + opt_line = EARGF(usage()); + break; + case 'n': + opt_name = EARGF(usage()); + break; + case 't': + case 'T': + opt_title = EARGF(usage()); + break; + case 'w': + opt_embed = EARGF(usage()); + break; + case 'v': + die("%s " VERSION "\n", argv0); + break; + default: + usage(); + } ARGEND; + +run: + if (argc > 0) /* eat all remaining arguments */ + opt_cmd = argv; + + if (!opt_title) + opt_title = (opt_line || !opt_cmd) ? "st" : opt_cmd[0]; + + setlocale(LC_CTYPE, ""); + XSetLocaleModifiers(""); + cols = MAX(cols, 1); + rows = MAX(rows, 1); + tnew(cols, rows); + xinit(cols, rows); + xsetenv(); + selinit(); + run(); + + return 0; +} From 307fba1e17d43f6b9197843a74c02a3908e63049 Mon Sep 17 00:00:00 2001 From: alejandrogs73 Date: Sun, 27 Sep 2026 17:52:02 +0200 Subject: [PATCH 7/7] Squashed 'suckless/dwm/' content from commit 44dbc68 git-subtree-dir: suckless/dwm git-subtree-split: 44dbc6809d05b8f2addc483f882e670db0b6b8e9 --- LICENSE | 38 + Makefile | 45 ++ README | 48 ++ config.def.h | 117 +++ config.mk | 39 + drw.c | 471 +++++++++++ drw.h | 60 ++ dwm.1 | 176 ++++ dwm.c | 2165 ++++++++++++++++++++++++++++++++++++++++++++++++++ dwm.png | Bin 0 -> 373 bytes transient.c | 42 + util.c | 37 + util.h | 9 + 13 files changed, 3247 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 drw.c create mode 100644 drw.h create mode 100644 dwm.1 create mode 100644 dwm.c create mode 100644 dwm.png create mode 100644 transient.c create mode 100644 util.c create mode 100644 util.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..596e6cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,38 @@ +MIT/X Consortium License + +© 2010-2026 Hiltjo Posthuma +© 2006-2019 Anselm R Garbe +© 2006-2009 Jukka Salmi +© 2006-2007 Sander van Dijk +© 2007-2011 Peter Hartlich +© 2007-2009 Szabolcs Nagy +© 2007-2009 Christof Musik +© 2007-2009 Premysl Hruby +© 2007-2008 Enno Gottox Boland +© 2008 Martin Hurton +© 2008 Neale Pickett +© 2009 Mate Nagy +© 2010-2012 Connor Lane Smith +© 2011 Christoph Lohmann <20h@r-36.net> +© 2015-2016 Quentin Rameau +© 2015-2016 Eric Pruitt +© 2016-2017 Markus Teich +© 2020-2022 Chris 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ffa69b4 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README b/README new file mode 100644 index 0000000..95d4fd0 --- /dev/null +++ b/README @@ -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. diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..81c3fc0 --- /dev/null +++ b/config.def.h @@ -0,0 +1,117 @@ +/* See LICENSE file for copyright and license details. */ + +/* appearance */ +static const unsigned int borderpx = 1; /* border pixel of 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 */ +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 monitor */ + { "Gimp", NULL, NULL, 0, 1, -1 }, + { "Firefox", NULL, NULL, 1 << 8, 0, -1 }, +}; + +/* 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 }; + +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 } }, + 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} }, +}; + +/* 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, Button2, spawn, {.v = termcmd } }, + { 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} }, +}; + diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..982dc21 --- /dev/null +++ b/config.mk @@ -0,0 +1,39 @@ +# 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 + +# 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=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 diff --git a/drw.c b/drw.c new file mode 100644 index 0000000..9fdd1a4 --- /dev/null +++ b/drw.c @@ -0,0 +1,471 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#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[] = "�"; + + 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); +} diff --git a/drw.h b/drw.h new file mode 100644 index 0000000..bda06f9 --- /dev/null +++ b/drw.h @@ -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); diff --git a/dwm.1 b/dwm.1 new file mode 100644 index 0000000..ddc8321 --- /dev/null +++ b/dwm.1 @@ -0,0 +1,176 @@ +.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. +.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 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. diff --git a/dwm.c b/dwm.c new file mode 100644 index 0000000..ab3a84c --- /dev/null +++ b/dwm.c @@ -0,0 +1,2165 @@ +/* See LICENSE file for copyright and license details. + * + * dynamic window manager is designed like any other X client as well. It is + * driven through handling X events. In contrast to other X clients, a window + * manager selects for SubstructureRedirectMask on the root window, to receive + * events about window (dis-)appearance. Only one X connection at a time is + * allowed to select for this event mask. + * + * The event handlers of dwm are organized in an array which is accessed + * whenever a new event has been fetched. This allows event dispatching + * in O(1) time. + * + * Each child of the root window is called a client, except windows which have + * set the override_redirect flag. Clients are organized in a linked client + * list on each monitor, the focus history is remembered through a stack list + * on each monitor. Each client contains a bit array to indicate the tags of a + * client. + * + * Keys and tagging rules are organized as arrays and defined in config.h. + * + * To understand everything else, start reading main(). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef XINERAMA +#include +#endif /* XINERAMA */ +#include + +#include "drw.h" +#include "util.h" + +/* macros */ +#define BUTTONMASK (ButtonPressMask|ButtonReleaseMask) +#define CLEANMASK(mask) (mask & ~(numlockmask|LockMask) & (ShiftMask|ControlMask|Mod1Mask|Mod2Mask|Mod3Mask|Mod4Mask|Mod5Mask)) +#define INTERSECT(x,y,w,h,m) (MAX(0, MIN((x)+(w),(m)->wx+(m)->ww) - MAX((x),(m)->wx)) \ + * MAX(0, MIN((y)+(h),(m)->wy+(m)->wh) - MAX((y),(m)->wy))) +#define ISVISIBLE(C) ((C->tags & C->mon->tagset[C->mon->seltags])) +#define MOUSEMASK (BUTTONMASK|PointerMotionMask) +#define WIDTH(X) ((X)->w + 2 * (X)->bw) +#define HEIGHT(X) ((X)->h + 2 * (X)->bw) +#define TAGMASK ((1 << LENGTH(tags)) - 1) +#define TEXTW(X) (drw_fontset_getwidth(drw, (X)) + lrpad) + +/* enums */ +enum { CurNormal, CurResize, CurMove, CurLast }; /* cursor */ +enum { SchemeNorm, SchemeSel }; /* color schemes */ +enum { NetSupported, NetWMName, NetWMState, NetWMCheck, + NetWMFullscreen, NetActiveWindow, NetWMWindowType, + NetWMWindowTypeDialog, NetClientList, NetLast }; /* EWMH atoms */ +enum { WMProtocols, WMDelete, WMState, WMTakeFocus, WMLast }; /* default atoms */ +enum { ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, + ClkClientWin, ClkRootWin, ClkLast }; /* clicks */ + +typedef union { + int i; + unsigned int ui; + float f; + const void *v; +} Arg; + +typedef struct { + unsigned int click; + unsigned int mask; + unsigned int button; + void (*func)(const Arg *arg); + const Arg arg; +} Button; + +typedef struct Monitor Monitor; +typedef struct Client Client; +struct Client { + char name[256]; + float mina, maxa; + int x, y, w, h; + int oldx, oldy, oldw, oldh; + int basew, baseh, incw, inch, maxw, maxh, minw, minh, hintsvalid; + int bw, oldbw; + unsigned int tags; + int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen; + Client *next; + Client *snext; + Monitor *mon; + Window win; +}; + +typedef struct { + unsigned int mod; + KeySym keysym; + void (*func)(const Arg *); + const Arg arg; +} Key; + +typedef struct { + const char *symbol; + void (*arrange)(Monitor *); +} Layout; + +struct Monitor { + char ltsymbol[16]; + float mfact; + int nmaster; + int num; + int by; /* bar geometry */ + int mx, my, mw, mh; /* screen size */ + int wx, wy, ww, wh; /* window area */ + unsigned int seltags; + unsigned int sellt; + unsigned int tagset[2]; + int showbar; + int topbar; + Client *clients; + Client *sel; + Client *stack; + Monitor *next; + Window barwin; + const Layout *lt[2]; +}; + +typedef struct { + const char *class; + const char *instance; + const char *title; + unsigned int tags; + int isfloating; + int monitor; +} Rule; + +/* function declarations */ +static void applyrules(Client *c); +static int applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact); +static void arrange(Monitor *m); +static void arrangemon(Monitor *m); +static void attach(Client *c); +static void attachstack(Client *c); +static void buttonpress(XEvent *e); +static void checkotherwm(void); +static void cleanup(void); +static void cleanupmon(Monitor *mon); +static void clientmessage(XEvent *e); +static void configure(Client *c); +static void configurenotify(XEvent *e); +static void configurerequest(XEvent *e); +static Monitor *createmon(void); +static void destroynotify(XEvent *e); +static void detach(Client *c); +static void detachstack(Client *c); +static Monitor *dirtomon(int dir); +static void drawbar(Monitor *m); +static void drawbars(void); +static void enternotify(XEvent *e); +static void expose(XEvent *e); +static void focus(Client *c); +static void focusin(XEvent *e); +static void focusmon(const Arg *arg); +static void focusstack(const Arg *arg); +static Atom getatomprop(Client *c, Atom prop); +static int getrootptr(int *x, int *y); +static long getstate(Window w); +static int gettextprop(Window w, Atom atom, char *text, unsigned int size); +static void grabbuttons(Client *c, int focused); +static void grabkeys(void); +static void incnmaster(const Arg *arg); +static void keypress(XEvent *e); +static void killclient(const Arg *arg); +static void manage(Window w, XWindowAttributes *wa); +static void mappingnotify(XEvent *e); +static void maprequest(XEvent *e); +static void monocle(Monitor *m); +static void motionnotify(XEvent *e); +static void movemouse(const Arg *arg); +static Client *nexttiled(Client *c); +static void pop(Client *c); +static void propertynotify(XEvent *e); +static void quit(const Arg *arg); +static Monitor *recttomon(int x, int y, int w, int h); +static void resize(Client *c, int x, int y, int w, int h, int interact); +static void resizeclient(Client *c, int x, int y, int w, int h); +static void resizemouse(const Arg *arg); +static void restack(Monitor *m); +static void run(void); +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 setfocus(Client *c); +static void setfullscreen(Client *c, int fullscreen); +static void setlayout(const Arg *arg); +static void setmfact(const Arg *arg); +static void setup(void); +static void seturgent(Client *c, int urg); +static void showhide(Client *c); +static void spawn(const Arg *arg); +static void tag(const Arg *arg); +static void tagmon(const Arg *arg); +static void tile(Monitor *m); +static void togglebar(const Arg *arg); +static void togglefloating(const Arg *arg); +static void toggletag(const Arg *arg); +static void toggleview(const Arg *arg); +static void unfocus(Client *c, int setfocus); +static void unmanage(Client *c, int destroyed); +static void unmapnotify(XEvent *e); +static void updatebarpos(Monitor *m); +static void updatebars(void); +static void updateclientlist(void); +static int updategeom(void); +static void updatenumlockmask(void); +static void updatesizehints(Client *c); +static void updatestatus(void); +static void updatetitle(Client *c); +static void updatewindowtype(Client *c); +static void updatewmhints(Client *c); +static void view(const Arg *arg); +static Client *wintoclient(Window w); +static Monitor *wintomon(Window w); +static int xerror(Display *dpy, XErrorEvent *ee); +static int xerrordummy(Display *dpy, XErrorEvent *ee); +static int xerrorstart(Display *dpy, XErrorEvent *ee); +static void zoom(const Arg *arg); + +/* variables */ +static const char broken[] = "broken"; +static char stext[256]; +static int screen; +static int sw, sh; /* X display screen geometry width, height */ +static int bh; /* bar height */ +static int lrpad; /* sum of left and right padding for text */ +static int (*xerrorxlib)(Display *, XErrorEvent *); +static unsigned int numlockmask = 0; +static void (*handler[LASTEvent]) (XEvent *) = { + [ButtonPress] = buttonpress, + [ClientMessage] = clientmessage, + [ConfigureRequest] = configurerequest, + [ConfigureNotify] = configurenotify, + [DestroyNotify] = destroynotify, + [EnterNotify] = enternotify, + [Expose] = expose, + [FocusIn] = focusin, + [KeyPress] = keypress, + [MappingNotify] = mappingnotify, + [MapRequest] = maprequest, + [MotionNotify] = motionnotify, + [PropertyNotify] = propertynotify, + [UnmapNotify] = unmapnotify +}; +static Atom wmatom[WMLast], netatom[NetLast]; +static int running = 1; +static Cur *cursor[CurLast]; +static Clr **scheme; +static Display *dpy; +static Drw *drw; +static Monitor *mons, *selmon; +static Window root, wmcheckwin; + +/* configuration, allows nested code to access above variables */ +#include "config.h" + +/* compile-time check if all tags fit into an unsigned int bit array. */ +struct NumTags { char limitexceeded[LENGTH(tags) > 31 ? -1 : 1]; }; + +/* function implementations */ +void +applyrules(Client *c) +{ + const char *class, *instance; + unsigned int i; + const Rule *r; + Monitor *m; + XClassHint ch = { NULL, NULL }; + + /* rule matching */ + c->isfloating = 0; + c->tags = 0; + XGetClassHint(dpy, c->win, &ch); + class = ch.res_class ? ch.res_class : broken; + instance = ch.res_name ? ch.res_name : broken; + + for (i = 0; i < LENGTH(rules); i++) { + r = &rules[i]; + if ((!r->title || strstr(c->name, r->title)) + && (!r->class || strstr(class, r->class)) + && (!r->instance || strstr(instance, r->instance))) + { + c->isfloating = r->isfloating; + c->tags |= r->tags; + for (m = mons; m && m->num != r->monitor; m = m->next); + if (m) + c->mon = m; + } + } + if (ch.res_class) + XFree(ch.res_class); + if (ch.res_name) + XFree(ch.res_name); + c->tags = c->tags & TAGMASK ? c->tags & TAGMASK : c->mon->tagset[c->mon->seltags]; +} + +int +applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact) +{ + int baseismin; + Monitor *m = c->mon; + + /* set minimum possible */ + *w = MAX(1, *w); + *h = MAX(1, *h); + if (interact) { + if (*x > sw) + *x = sw - WIDTH(c); + if (*y > sh) + *y = sh - HEIGHT(c); + if (*x + *w + 2 * c->bw < 0) + *x = 0; + if (*y + *h + 2 * c->bw < 0) + *y = 0; + } else { + if (*x >= m->wx + m->ww) + *x = m->wx + m->ww - WIDTH(c); + if (*y >= m->wy + m->wh) + *y = m->wy + m->wh - HEIGHT(c); + if (*x + *w + 2 * c->bw <= m->wx) + *x = m->wx; + if (*y + *h + 2 * c->bw <= m->wy) + *y = m->wy; + } + if (*h < bh) + *h = bh; + if (*w < bh) + *w = bh; + if (resizehints || c->isfloating || !c->mon->lt[c->mon->sellt]->arrange) { + if (!c->hintsvalid) + updatesizehints(c); + /* see last two sentences in ICCCM 4.1.2.3 */ + baseismin = c->basew == c->minw && c->baseh == c->minh; + if (!baseismin) { /* temporarily remove base dimensions */ + *w -= c->basew; + *h -= c->baseh; + } + /* adjust for aspect limits */ + if (c->mina > 0 && c->maxa > 0) { + if (c->maxa < (float)*w / *h) + *w = *h * c->maxa + 0.5; + else if (c->mina < (float)*h / *w) + *h = *w * c->mina + 0.5; + } + if (baseismin) { /* increment calculation requires this */ + *w -= c->basew; + *h -= c->baseh; + } + /* adjust for increment value */ + if (c->incw) + *w -= *w % c->incw; + if (c->inch) + *h -= *h % c->inch; + /* restore base dimensions */ + *w = MAX(*w + c->basew, c->minw); + *h = MAX(*h + c->baseh, c->minh); + if (c->maxw) + *w = MIN(*w, c->maxw); + if (c->maxh) + *h = MIN(*h, c->maxh); + } + return *x != c->x || *y != c->y || *w != c->w || *h != c->h; +} + +void +arrange(Monitor *m) +{ + if (m) + showhide(m->stack); + else for (m = mons; m; m = m->next) + showhide(m->stack); + if (m) { + arrangemon(m); + restack(m); + } else for (m = mons; m; m = m->next) + arrangemon(m); +} + +void +arrangemon(Monitor *m) +{ + strncpy(m->ltsymbol, m->lt[m->sellt]->symbol, sizeof m->ltsymbol); + if (m->lt[m->sellt]->arrange) + m->lt[m->sellt]->arrange(m); +} + +void +attach(Client *c) +{ + c->next = c->mon->clients; + c->mon->clients = c; +} + +void +attachstack(Client *c) +{ + c->snext = c->mon->stack; + c->mon->stack = c; +} + +void +buttonpress(XEvent *e) +{ + unsigned int i, x, click; + Arg arg = {0}; + Client *c; + Monitor *m; + XButtonPressedEvent *ev = &e->xbutton; + + click = ClkRootWin; + /* focus monitor if necessary */ + if ((m = wintomon(ev->window)) && m != selmon) { + unfocus(selmon->sel, 1); + selmon = m; + focus(NULL); + } + if (ev->window == selmon->barwin) { + i = x = 0; + do + x += TEXTW(tags[i]); + while (ev->x >= x && ++i < LENGTH(tags)); + if (i < LENGTH(tags)) { + click = ClkTagBar; + arg.ui = 1 << i; + } else if (ev->x < x + TEXTW(selmon->ltsymbol)) + click = ClkLtSymbol; + else if (ev->x > selmon->ww - (int)TEXTW(stext) + lrpad - 2) + click = ClkStatusText; + else + click = ClkWinTitle; + } else if ((c = wintoclient(ev->window))) { + focus(c); + restack(selmon); + XAllowEvents(dpy, ReplayPointer, CurrentTime); + click = ClkClientWin; + } + for (i = 0; i < LENGTH(buttons); i++) + if (click == buttons[i].click && buttons[i].func && buttons[i].button == ev->button + && CLEANMASK(buttons[i].mask) == CLEANMASK(ev->state)) + buttons[i].func(click == ClkTagBar && buttons[i].arg.i == 0 ? &arg : &buttons[i].arg); +} + +void +checkotherwm(void) +{ + xerrorxlib = XSetErrorHandler(xerrorstart); + /* this causes an error if some other window manager is running */ + XSelectInput(dpy, DefaultRootWindow(dpy), SubstructureRedirectMask); + XSync(dpy, False); + XSetErrorHandler(xerror); + XSync(dpy, False); +} + +void +cleanup(void) +{ + Arg a = {.ui = ~0}; + Layout foo = { "", NULL }; + Monitor *m; + size_t i; + + view(&a); + selmon->lt[selmon->sellt] = &foo; + for (m = mons; m; m = m->next) + while (m->stack) + unmanage(m->stack, 0); + XUngrabKey(dpy, AnyKey, AnyModifier, root); + while (mons) + cleanupmon(mons); + for (i = 0; i < CurLast; i++) + drw_cur_free(drw, cursor[i]); + for (i = 0; i < LENGTH(colors); i++) + drw_scm_free(drw, scheme[i], 3); + free(scheme); + XDestroyWindow(dpy, wmcheckwin); + drw_free(drw); + XSync(dpy, False); + XSetInputFocus(dpy, PointerRoot, RevertToPointerRoot, CurrentTime); + XDeleteProperty(dpy, root, netatom[NetActiveWindow]); +} + +void +cleanupmon(Monitor *mon) +{ + Monitor *m; + + if (mon == mons) + mons = mons->next; + else { + for (m = mons; m && m->next != mon; m = m->next); + m->next = mon->next; + } + XUnmapWindow(dpy, mon->barwin); + XDestroyWindow(dpy, mon->barwin); + free(mon); +} + +void +clientmessage(XEvent *e) +{ + XClientMessageEvent *cme = &e->xclient; + Client *c = wintoclient(cme->window); + + if (!c) + return; + if (cme->message_type == netatom[NetWMState]) { + if (cme->data.l[1] == netatom[NetWMFullscreen] + || cme->data.l[2] == netatom[NetWMFullscreen]) + setfullscreen(c, (cme->data.l[0] == 1 /* _NET_WM_STATE_ADD */ + || (cme->data.l[0] == 2 /* _NET_WM_STATE_TOGGLE */ && !c->isfullscreen))); + } else if (cme->message_type == netatom[NetActiveWindow]) { + if (c != selmon->sel && !c->isurgent) + seturgent(c, 1); + } +} + +void +configure(Client *c) +{ + XConfigureEvent ce; + + ce.type = ConfigureNotify; + ce.display = dpy; + ce.event = c->win; + ce.window = c->win; + ce.x = c->x; + ce.y = c->y; + ce.width = c->w; + ce.height = c->h; + ce.border_width = c->bw; + ce.above = None; + ce.override_redirect = False; + XSendEvent(dpy, c->win, False, StructureNotifyMask, (XEvent *)&ce); +} + +void +configurenotify(XEvent *e) +{ + Monitor *m; + Client *c; + XConfigureEvent *ev = &e->xconfigure; + int dirty; + + /* TODO: updategeom handling sucks, needs to be simplified */ + if (ev->window == root) { + dirty = (sw != ev->width || sh != ev->height); + sw = ev->width; + sh = ev->height; + if (updategeom() || dirty) { + drw_resize(drw, sw, bh); + updatebars(); + for (m = mons; m; m = m->next) { + for (c = m->clients; c; c = c->next) + if (c->isfullscreen) + resizeclient(c, m->mx, m->my, m->mw, m->mh); + XMoveResizeWindow(dpy, m->barwin, m->wx, m->by, m->ww, bh); + } + focus(NULL); + arrange(NULL); + } + } +} + +void +configurerequest(XEvent *e) +{ + Client *c; + Monitor *m; + XConfigureRequestEvent *ev = &e->xconfigurerequest; + XWindowChanges wc; + + if ((c = wintoclient(ev->window))) { + if (ev->value_mask & CWBorderWidth) + c->bw = ev->border_width; + else if (c->isfloating || !selmon->lt[selmon->sellt]->arrange) { + m = c->mon; + if (ev->value_mask & CWX) { + c->oldx = c->x; + c->x = m->mx + ev->x; + } + if (ev->value_mask & CWY) { + c->oldy = c->y; + c->y = m->my + ev->y; + } + if (ev->value_mask & CWWidth) { + c->oldw = c->w; + c->w = ev->width; + } + if (ev->value_mask & CWHeight) { + c->oldh = c->h; + c->h = ev->height; + } + if ((c->x + c->w) > m->mx + m->mw && c->isfloating) + c->x = m->mx + (m->mw / 2 - WIDTH(c) / 2); /* center in x direction */ + if ((c->y + c->h) > m->my + m->mh && c->isfloating) + c->y = m->my + (m->mh / 2 - HEIGHT(c) / 2); /* center in y direction */ + if ((ev->value_mask & (CWX|CWY)) && !(ev->value_mask & (CWWidth|CWHeight))) + configure(c); + if (ISVISIBLE(c)) + XMoveResizeWindow(dpy, c->win, c->x, c->y, c->w, c->h); + } else + configure(c); + } else { + wc.x = ev->x; + wc.y = ev->y; + wc.width = ev->width; + wc.height = ev->height; + wc.border_width = ev->border_width; + wc.sibling = ev->above; + wc.stack_mode = ev->detail; + XConfigureWindow(dpy, ev->window, ev->value_mask, &wc); + } + XSync(dpy, False); +} + +Monitor * +createmon(void) +{ + Monitor *m; + + m = ecalloc(1, sizeof(Monitor)); + m->tagset[0] = m->tagset[1] = 1; + m->mfact = mfact; + m->nmaster = nmaster; + m->showbar = showbar; + m->topbar = topbar; + m->lt[0] = &layouts[0]; + m->lt[1] = &layouts[1 % LENGTH(layouts)]; + strncpy(m->ltsymbol, layouts[0].symbol, sizeof m->ltsymbol); + return m; +} + +void +destroynotify(XEvent *e) +{ + Client *c; + XDestroyWindowEvent *ev = &e->xdestroywindow; + + if ((c = wintoclient(ev->window))) + unmanage(c, 1); +} + +void +detach(Client *c) +{ + Client **tc; + + for (tc = &c->mon->clients; *tc && *tc != c; tc = &(*tc)->next); + *tc = c->next; +} + +void +detachstack(Client *c) +{ + Client **tc, *t; + + for (tc = &c->mon->stack; *tc && *tc != c; tc = &(*tc)->snext); + *tc = c->snext; + + if (c == c->mon->sel) { + for (t = c->mon->stack; t && !ISVISIBLE(t); t = t->snext); + c->mon->sel = t; + } +} + +Monitor * +dirtomon(int dir) +{ + Monitor *m = NULL; + + if (dir > 0) { + if (!(m = selmon->next)) + m = mons; + } else if (selmon == mons) + for (m = mons; m->next; m = m->next); + else + for (m = mons; m->next != selmon; m = m->next); + return m; +} + +void +drawbar(Monitor *m) +{ + int x, w, tw = 0; + int boxs = drw->fonts->h / 9; + int boxw = drw->fonts->h / 6 + 2; + unsigned int i, occ = 0, urg = 0; + Client *c; + + if (!m->showbar) + return; + + /* draw status first so it can be overdrawn by tags later */ + if (m == selmon) { /* status is only drawn on selected monitor */ + 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); + } + + for (c = m->clients; c; c = c->next) { + occ |= c->tags; + if (c->isurgent) + urg |= c->tags; + } + x = 0; + for (i = 0; i < LENGTH(tags); i++) { + w = TEXTW(tags[i]); + drw_setscheme(drw, scheme[m->tagset[m->seltags] & 1 << i ? SchemeSel : SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, tags[i], urg & 1 << i); + if (occ & 1 << i) + drw_rect(drw, x + boxs, boxs, boxw, boxw, + m == selmon && selmon->sel && selmon->sel->tags & 1 << i, + urg & 1 << i); + x += w; + } + w = TEXTW(m->ltsymbol); + drw_setscheme(drw, scheme[SchemeNorm]); + x = drw_text(drw, x, 0, w, bh, lrpad / 2, m->ltsymbol, 0); + + if ((w = m->ww - tw - x) > bh) { + if (m->sel) { + drw_setscheme(drw, scheme[m == selmon ? SchemeSel : SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, m->sel->name, 0); + if (m->sel->isfloating) + drw_rect(drw, x + boxs, boxs, boxw, boxw, m->sel->isfixed, 0); + } else { + drw_setscheme(drw, scheme[SchemeNorm]); + drw_rect(drw, x, 0, w, bh, 1, 1); + } + } + drw_map(drw, m->barwin, 0, 0, m->ww, bh); +} + +void +drawbars(void) +{ + Monitor *m; + + for (m = mons; m; m = m->next) + drawbar(m); +} + +void +enternotify(XEvent *e) +{ + Client *c; + Monitor *m; + XCrossingEvent *ev = &e->xcrossing; + + if ((ev->mode != NotifyNormal || ev->detail == NotifyInferior) && ev->window != root) + return; + c = wintoclient(ev->window); + m = c ? c->mon : wintomon(ev->window); + if (m != selmon) { + unfocus(selmon->sel, 1); + selmon = m; + } else if (!c || c == selmon->sel) + return; + focus(c); +} + +void +expose(XEvent *e) +{ + Monitor *m; + XExposeEvent *ev = &e->xexpose; + + if (ev->count == 0 && (m = wintomon(ev->window))) + drawbar(m); +} + +void +focus(Client *c) +{ + if (!c || !ISVISIBLE(c)) + for (c = selmon->stack; c && !ISVISIBLE(c); c = c->snext); + if (selmon->sel && selmon->sel != c) + unfocus(selmon->sel, 0); + if (c) { + if (c->mon != selmon) + selmon = c->mon; + if (c->isurgent) + seturgent(c, 0); + detachstack(c); + attachstack(c); + grabbuttons(c, 1); + XSetWindowBorder(dpy, c->win, scheme[SchemeSel][ColBorder].pixel); + setfocus(c); + } else { + XSetInputFocus(dpy, root, RevertToPointerRoot, CurrentTime); + XDeleteProperty(dpy, root, netatom[NetActiveWindow]); + } + selmon->sel = c; + drawbars(); +} + +/* there are some broken focus acquiring clients needing extra handling */ +void +focusin(XEvent *e) +{ + XFocusChangeEvent *ev = &e->xfocus; + + if (selmon->sel && ev->window != selmon->sel->win) + setfocus(selmon->sel); +} + +void +focusmon(const Arg *arg) +{ + Monitor *m; + + if (!mons->next) + return; + if ((m = dirtomon(arg->i)) == selmon) + return; + unfocus(selmon->sel, 0); + selmon = m; + focus(NULL); +} + +void +focusstack(const Arg *arg) +{ + Client *c = NULL, *i; + + if (!selmon->sel || (selmon->sel->isfullscreen && lockfullscreen)) + return; + if (arg->i > 0) { + for (c = selmon->sel->next; c && !ISVISIBLE(c); c = c->next); + if (!c) + for (c = selmon->clients; c && !ISVISIBLE(c); c = c->next); + } else { + for (i = selmon->clients; i != selmon->sel; i = i->next) + if (ISVISIBLE(i)) + c = i; + if (!c) + for (; i; i = i->next) + if (ISVISIBLE(i)) + c = i; + } + if (c) { + focus(c); + restack(selmon); + } +} + +Atom +getatomprop(Client *c, Atom prop) +{ + int format; + unsigned long nitems, dl; + unsigned char *p = NULL; + Atom da, atom = None; + + if (XGetWindowProperty(dpy, c->win, prop, 0L, sizeof atom, False, XA_ATOM, + &da, &format, &nitems, &dl, &p) == Success && p) { + if (nitems > 0 && format == 32) + atom = *(long *)p; + XFree(p); + } + return atom; +} + +int +getrootptr(int *x, int *y) +{ + int di; + unsigned int dui; + Window dummy; + + return XQueryPointer(dpy, root, &dummy, &dummy, x, y, &di, &di, &dui); +} + +long +getstate(Window w) +{ + int format; + long result = -1; + unsigned char *p = NULL; + unsigned long n, extra; + Atom real; + + if (XGetWindowProperty(dpy, w, wmatom[WMState], 0L, 2L, False, wmatom[WMState], + &real, &format, &n, &extra, &p) != Success) + return -1; + if (n != 0 && format == 32) + result = *(long *)p; + XFree(p); + return result; +} + +int +gettextprop(Window w, Atom atom, char *text, unsigned int size) +{ + char **list = NULL; + int n; + XTextProperty name; + + if (!text || size == 0) + return 0; + text[0] = '\0'; + if (!XGetTextProperty(dpy, w, &name, atom) || !name.nitems) + return 0; + if (name.encoding == XA_STRING) { + strncpy(text, (char *)name.value, size - 1); + } else if (XmbTextPropertyToTextList(dpy, &name, &list, &n) >= Success && n > 0 && *list) { + strncpy(text, *list, size - 1); + XFreeStringList(list); + } + text[size - 1] = '\0'; + XFree(name.value); + return 1; +} + +void +grabbuttons(Client *c, int focused) +{ + updatenumlockmask(); + { + unsigned int i, j; + unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; + XUngrabButton(dpy, AnyButton, AnyModifier, c->win); + if (!focused) + XGrabButton(dpy, AnyButton, AnyModifier, c->win, False, + BUTTONMASK, GrabModeSync, GrabModeSync, None, None); + for (i = 0; i < LENGTH(buttons); i++) + if (buttons[i].click == ClkClientWin) + for (j = 0; j < LENGTH(modifiers); j++) + XGrabButton(dpy, buttons[i].button, + buttons[i].mask | modifiers[j], + c->win, False, BUTTONMASK, + GrabModeAsync, GrabModeSync, None, None); + } +} + +void +grabkeys(void) +{ + updatenumlockmask(); + { + unsigned int i, j, k; + unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; + int start, end, skip; + KeySym *syms; + + XUngrabKey(dpy, AnyKey, AnyModifier, root); + XDisplayKeycodes(dpy, &start, &end); + syms = XGetKeyboardMapping(dpy, start, end - start + 1, &skip); + if (!syms) + return; + for (k = start; k <= end; k++) + for (i = 0; i < LENGTH(keys); i++) + /* skip modifier codes, we do that ourselves */ + if (keys[i].keysym == syms[(k - start) * skip]) + for (j = 0; j < LENGTH(modifiers); j++) + XGrabKey(dpy, k, + keys[i].mod | modifiers[j], + root, True, + GrabModeAsync, GrabModeAsync); + XFree(syms); + } +} + +void +incnmaster(const Arg *arg) +{ + selmon->nmaster = MAX(selmon->nmaster + arg->i, 0); + arrange(selmon); +} + +#ifdef XINERAMA +static int +isuniquegeom(XineramaScreenInfo *unique, size_t n, XineramaScreenInfo *info) +{ + while (n--) + if (unique[n].x_org == info->x_org && unique[n].y_org == info->y_org + && unique[n].width == info->width && unique[n].height == info->height) + return 0; + return 1; +} +#endif /* XINERAMA */ + +void +keypress(XEvent *e) +{ + unsigned int i; + KeySym keysym; + XKeyEvent *ev; + + ev = &e->xkey; + keysym = XKeycodeToKeysym(dpy, (KeyCode)ev->keycode, 0); + for (i = 0; i < LENGTH(keys); i++) + if (keysym == keys[i].keysym + && CLEANMASK(keys[i].mod) == CLEANMASK(ev->state) + && keys[i].func) + keys[i].func(&(keys[i].arg)); +} + +void +killclient(const Arg *arg) +{ + if (!selmon->sel) + return; + if (!sendevent(selmon->sel, wmatom[WMDelete])) { + XGrabServer(dpy); + XSetErrorHandler(xerrordummy); + XSetCloseDownMode(dpy, DestroyAll); + XKillClient(dpy, selmon->sel->win); + XSync(dpy, False); + XSetErrorHandler(xerror); + XUngrabServer(dpy); + } +} + +void +manage(Window w, XWindowAttributes *wa) +{ + Client *c, *t = NULL; + Window trans = None; + XWindowChanges wc; + + c = ecalloc(1, sizeof(Client)); + c->win = w; + /* geometry */ + c->x = c->oldx = wa->x; + c->y = c->oldy = wa->y; + c->w = c->oldw = wa->width; + c->h = c->oldh = wa->height; + c->oldbw = wa->border_width; + + updatetitle(c); + if (XGetTransientForHint(dpy, w, &trans) && (t = wintoclient(trans))) { + c->mon = t->mon; + c->tags = t->tags; + } else { + c->mon = selmon; + applyrules(c); + } + + if (c->x + WIDTH(c) > c->mon->wx + c->mon->ww) + c->x = c->mon->wx + c->mon->ww - WIDTH(c); + if (c->y + HEIGHT(c) > c->mon->wy + c->mon->wh) + c->y = c->mon->wy + c->mon->wh - HEIGHT(c); + c->x = MAX(c->x, c->mon->wx); + c->y = MAX(c->y, c->mon->wy); + c->bw = borderpx; + + wc.border_width = c->bw; + XConfigureWindow(dpy, w, CWBorderWidth, &wc); + XSetWindowBorder(dpy, w, scheme[SchemeNorm][ColBorder].pixel); + configure(c); /* propagates border_width, if size doesn't change */ + updatewindowtype(c); + updatesizehints(c); + updatewmhints(c); + XSelectInput(dpy, w, EnterWindowMask|FocusChangeMask|PropertyChangeMask|StructureNotifyMask); + grabbuttons(c, 0); + if (!c->isfloating) + c->isfloating = c->oldstate = trans != None || c->isfixed; + if (c->isfloating) + XRaiseWindow(dpy, c->win); + attach(c); + attachstack(c); + XChangeProperty(dpy, root, netatom[NetClientList], XA_WINDOW, 32, PropModeAppend, + (unsigned char *) &(c->win), 1); + XMoveResizeWindow(dpy, c->win, c->x + 2 * sw, c->y, c->w, c->h); /* some windows require this */ + setclientstate(c, NormalState); + if (c->mon == selmon) + unfocus(selmon->sel, 0); + c->mon->sel = c; + arrange(c->mon); + XMapWindow(dpy, c->win); + focus(NULL); +} + +void +mappingnotify(XEvent *e) +{ + XMappingEvent *ev = &e->xmapping; + + XRefreshKeyboardMapping(ev); + if (ev->request == MappingKeyboard) + grabkeys(); +} + +void +maprequest(XEvent *e) +{ + static XWindowAttributes wa; + XMapRequestEvent *ev = &e->xmaprequest; + + if (!XGetWindowAttributes(dpy, ev->window, &wa) || wa.override_redirect) + return; + if (!wintoclient(ev->window)) + manage(ev->window, &wa); +} + +void +monocle(Monitor *m) +{ + unsigned int n = 0; + Client *c; + + for (c = m->clients; c; c = c->next) + if (ISVISIBLE(c)) + n++; + if (n > 0) /* override layout symbol */ + snprintf(m->ltsymbol, sizeof m->ltsymbol, "[%d]", n); + for (c = nexttiled(m->clients); c; c = nexttiled(c->next)) + resize(c, m->wx, m->wy, m->ww - 2 * c->bw, m->wh - 2 * c->bw, 0); +} + +void +motionnotify(XEvent *e) +{ + static Monitor *mon = NULL; + Monitor *m; + XMotionEvent *ev = &e->xmotion; + + if (ev->window != root) + return; + if ((m = recttomon(ev->x_root, ev->y_root, 1, 1)) != mon && mon) { + unfocus(selmon->sel, 1); + selmon = m; + focus(NULL); + } + mon = m; +} + +void +movemouse(const Arg *arg) +{ + int x, y, ocx, ocy, nx, ny; + Client *c; + Monitor *m; + XEvent ev; + Time lasttime = 0; + + if (!(c = selmon->sel)) + return; + if (c->isfullscreen) /* no support moving fullscreen windows by mouse */ + return; + restack(selmon); + ocx = c->x; + ocy = c->y; + if (XGrabPointer(dpy, root, False, MOUSEMASK, GrabModeAsync, GrabModeAsync, + None, cursor[CurMove]->cursor, CurrentTime) != GrabSuccess) + return; + if (!getrootptr(&x, &y)) + return; + do { + XMaskEvent(dpy, MOUSEMASK|ExposureMask|SubstructureRedirectMask, &ev); + switch(ev.type) { + case ConfigureRequest: + case Expose: + case MapRequest: + handler[ev.type](&ev); + break; + case MotionNotify: + if ((ev.xmotion.time - lasttime) <= (1000 / refreshrate)) + continue; + lasttime = ev.xmotion.time; + + nx = ocx + (ev.xmotion.x - x); + ny = ocy + (ev.xmotion.y - y); + if (abs(selmon->wx - nx) < snap) + nx = selmon->wx; + else if (abs((selmon->wx + selmon->ww) - (nx + WIDTH(c))) < snap) + nx = selmon->wx + selmon->ww - WIDTH(c); + if (abs(selmon->wy - ny) < snap) + ny = selmon->wy; + else if (abs((selmon->wy + selmon->wh) - (ny + HEIGHT(c))) < snap) + ny = selmon->wy + selmon->wh - HEIGHT(c); + if (!c->isfloating && selmon->lt[selmon->sellt]->arrange + && (abs(nx - c->x) > snap || abs(ny - c->y) > snap)) + togglefloating(NULL); + if (!selmon->lt[selmon->sellt]->arrange || c->isfloating) + resize(c, nx, ny, c->w, c->h, 1); + break; + } + } while (ev.type != ButtonRelease); + XUngrabPointer(dpy, CurrentTime); + if ((m = recttomon(c->x, c->y, c->w, c->h)) != selmon) { + sendmon(c, m); + selmon = m; + focus(NULL); + } +} + +Client * +nexttiled(Client *c) +{ + for (; c && (c->isfloating || !ISVISIBLE(c)); c = c->next); + return c; +} + +void +pop(Client *c) +{ + detach(c); + attach(c); + focus(c); + arrange(c->mon); +} + +void +propertynotify(XEvent *e) +{ + Client *c; + Window trans; + XPropertyEvent *ev = &e->xproperty; + + if ((ev->window == root) && (ev->atom == XA_WM_NAME)) + updatestatus(); + else if (ev->state == PropertyDelete) + return; /* ignore */ + else if ((c = wintoclient(ev->window))) { + switch(ev->atom) { + default: break; + case XA_WM_TRANSIENT_FOR: + if (!c->isfloating && (XGetTransientForHint(dpy, c->win, &trans)) && + (c->isfloating = (wintoclient(trans)) != NULL)) + arrange(c->mon); + break; + case XA_WM_NORMAL_HINTS: + c->hintsvalid = 0; + break; + case XA_WM_HINTS: + updatewmhints(c); + drawbars(); + break; + } + if (ev->atom == XA_WM_NAME || ev->atom == netatom[NetWMName]) { + updatetitle(c); + if (c == c->mon->sel) + drawbar(c->mon); + } + if (ev->atom == netatom[NetWMWindowType]) + updatewindowtype(c); + } +} + +void +quit(const Arg *arg) +{ + running = 0; +} + +Monitor * +recttomon(int x, int y, int w, int h) +{ + Monitor *m, *r = selmon; + int a, area = 0; + + for (m = mons; m; m = m->next) + if ((a = INTERSECT(x, y, w, h, m)) > area) { + area = a; + r = m; + } + return r; +} + +void +resize(Client *c, int x, int y, int w, int h, int interact) +{ + if (applysizehints(c, &x, &y, &w, &h, interact)) + resizeclient(c, x, y, w, h); +} + +void +resizeclient(Client *c, int x, int y, int w, int h) +{ + XWindowChanges wc; + + c->oldx = c->x; c->x = wc.x = x; + c->oldy = c->y; c->y = wc.y = y; + c->oldw = c->w; c->w = wc.width = w; + c->oldh = c->h; c->h = wc.height = h; + wc.border_width = c->bw; + XConfigureWindow(dpy, c->win, CWX|CWY|CWWidth|CWHeight|CWBorderWidth, &wc); + configure(c); + XSync(dpy, False); +} + +void +resizemouse(const Arg *arg) +{ + int ocx, ocy, nw, nh; + Client *c; + Monitor *m; + XEvent ev; + Time lasttime = 0; + + if (!(c = selmon->sel)) + return; + if (c->isfullscreen) /* no support resizing fullscreen windows by mouse */ + return; + restack(selmon); + ocx = c->x; + ocy = c->y; + if (XGrabPointer(dpy, root, False, MOUSEMASK, GrabModeAsync, GrabModeAsync, + None, cursor[CurResize]->cursor, CurrentTime) != GrabSuccess) + return; + XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w + c->bw - 1, c->h + c->bw - 1); + do { + XMaskEvent(dpy, MOUSEMASK|ExposureMask|SubstructureRedirectMask, &ev); + switch(ev.type) { + case ConfigureRequest: + case Expose: + case MapRequest: + handler[ev.type](&ev); + break; + case MotionNotify: + if ((ev.xmotion.time - lasttime) <= (1000 / refreshrate)) + continue; + lasttime = ev.xmotion.time; + + nw = MAX(ev.xmotion.x - ocx - 2 * c->bw + 1, 1); + nh = MAX(ev.xmotion.y - ocy - 2 * c->bw + 1, 1); + if (c->mon->wx + nw >= selmon->wx && c->mon->wx + nw <= selmon->wx + selmon->ww + && c->mon->wy + nh >= selmon->wy && c->mon->wy + nh <= selmon->wy + selmon->wh) + { + if (!c->isfloating && selmon->lt[selmon->sellt]->arrange + && (abs(nw - c->w) > snap || abs(nh - c->h) > snap)) + togglefloating(NULL); + } + if (!selmon->lt[selmon->sellt]->arrange || c->isfloating) + resize(c, c->x, c->y, nw, nh, 1); + break; + } + } while (ev.type != ButtonRelease); + XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w + c->bw - 1, c->h + c->bw - 1); + XUngrabPointer(dpy, CurrentTime); + while (XCheckMaskEvent(dpy, EnterWindowMask, &ev)); + if ((m = recttomon(c->x, c->y, c->w, c->h)) != selmon) { + sendmon(c, m); + selmon = m; + focus(NULL); + } +} + +void +restack(Monitor *m) +{ + Client *c; + XEvent ev; + XWindowChanges wc; + + drawbar(m); + if (!m->sel) + return; + if (m->sel->isfloating || !m->lt[m->sellt]->arrange) + XRaiseWindow(dpy, m->sel->win); + if (m->lt[m->sellt]->arrange) { + wc.stack_mode = Below; + wc.sibling = m->barwin; + for (c = m->stack; c; c = c->snext) + if (!c->isfloating && ISVISIBLE(c)) { + XConfigureWindow(dpy, c->win, CWSibling|CWStackMode, &wc); + wc.sibling = c->win; + } + } + XSync(dpy, False); + while (XCheckMaskEvent(dpy, EnterWindowMask, &ev)); +} + +void +run(void) +{ + XEvent ev; + /* main event loop */ + XSync(dpy, False); + while (running && !XNextEvent(dpy, &ev)) + if (handler[ev.type]) + handler[ev.type](&ev); /* call handler */ +} + +void +scan(void) +{ + unsigned int i, num; + Window d1, d2, *wins = NULL; + XWindowAttributes wa; + + if (XQueryTree(dpy, root, &d1, &d2, &wins, &num)) { + for (i = 0; i < num; i++) { + if (!XGetWindowAttributes(dpy, wins[i], &wa) + || wa.override_redirect || XGetTransientForHint(dpy, wins[i], &d1)) + continue; + if (wa.map_state == IsViewable || getstate(wins[i]) == IconicState) + manage(wins[i], &wa); + } + for (i = 0; i < num; i++) { /* now the transients */ + if (!XGetWindowAttributes(dpy, wins[i], &wa)) + continue; + if (XGetTransientForHint(dpy, wins[i], &d1) + && (wa.map_state == IsViewable || getstate(wins[i]) == IconicState)) + manage(wins[i], &wa); + } + if (wins) + XFree(wins); + } +} + +void +sendmon(Client *c, Monitor *m) +{ + if (c->mon == m) + return; + unfocus(c, 1); + detach(c); + detachstack(c); + c->mon = m; + c->tags = m->tagset[m->seltags]; /* assign tags of target monitor */ + attach(c); + attachstack(c); + if (c->isfullscreen) + resizeclient(c, m->mx, m->my, m->mw, m->mh); + focus(NULL); + arrange(NULL); +} + +void +setclientstate(Client *c, long state) +{ + long data[] = { state, None }; + + XChangeProperty(dpy, c->win, wmatom[WMState], wmatom[WMState], 32, + PropModeReplace, (unsigned char *)data, 2); +} + +int +sendevent(Client *c, Atom proto) +{ + int n; + Atom *protocols; + int exists = 0; + XEvent ev; + + if (XGetWMProtocols(dpy, c->win, &protocols, &n)) { + while (!exists && n--) + exists = protocols[n] == proto; + XFree(protocols); + } + if (exists) { + ev.type = ClientMessage; + ev.xclient.window = c->win; + ev.xclient.message_type = wmatom[WMProtocols]; + ev.xclient.format = 32; + ev.xclient.data.l[0] = proto; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(dpy, c->win, False, NoEventMask, &ev); + } + return exists; +} + +void +setfocus(Client *c) +{ + if (!c->neverfocus) + XSetInputFocus(dpy, c->win, RevertToPointerRoot, CurrentTime); + XChangeProperty(dpy, root, netatom[NetActiveWindow], XA_WINDOW, 32, + PropModeReplace, (unsigned char *)&c->win, 1); + sendevent(c, wmatom[WMTakeFocus]); +} + +void +setfullscreen(Client *c, int fullscreen) +{ + if (fullscreen && !c->isfullscreen) { + XChangeProperty(dpy, c->win, netatom[NetWMState], XA_ATOM, 32, + PropModeReplace, (unsigned char*)&netatom[NetWMFullscreen], 1); + c->isfullscreen = 1; + c->oldstate = c->isfloating; + c->oldbw = c->bw; + c->bw = 0; + c->isfloating = 1; + resizeclient(c, c->mon->mx, c->mon->my, c->mon->mw, c->mon->mh); + XRaiseWindow(dpy, c->win); + } else if (!fullscreen && c->isfullscreen){ + XChangeProperty(dpy, c->win, netatom[NetWMState], XA_ATOM, 32, + PropModeReplace, (unsigned char*)0, 0); + c->isfullscreen = 0; + c->isfloating = c->oldstate; + c->bw = c->oldbw; + c->x = c->oldx; + c->y = c->oldy; + c->w = c->oldw; + c->h = c->oldh; + resizeclient(c, c->x, c->y, c->w, c->h); + arrange(c->mon); + } +} + +void +setlayout(const Arg *arg) +{ + if (!arg || !arg->v || arg->v != selmon->lt[selmon->sellt]) + selmon->sellt ^= 1; + if (arg && arg->v) + selmon->lt[selmon->sellt] = (Layout *)arg->v; + strncpy(selmon->ltsymbol, selmon->lt[selmon->sellt]->symbol, sizeof selmon->ltsymbol); + if (selmon->sel) + arrange(selmon); + else + drawbar(selmon); +} + +/* arg > 1.0 will set mfact absolutely */ +void +setmfact(const Arg *arg) +{ + float f; + + if (!arg || !selmon->lt[selmon->sellt]->arrange) + return; + f = arg->f < 1.0 ? arg->f + selmon->mfact : arg->f - 1.0; + if (f < 0.05 || f > 0.95) + return; + selmon->mfact = f; + arrange(selmon); +} + +void +setup(void) +{ + int i; + XSetWindowAttributes wa; + Atom utf8string; + struct sigaction sa; + + /* do not transform children into zombies when they terminate */ + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_NOCLDSTOP | SA_NOCLDWAIT | SA_RESTART; + sa.sa_handler = SIG_IGN; + sigaction(SIGCHLD, &sa, NULL); + + /* clean up any zombies (inherited from .xinitrc etc) immediately */ + while (waitpid(-1, NULL, WNOHANG) > 0); + + /* init screen */ + screen = DefaultScreen(dpy); + sw = DisplayWidth(dpy, screen); + sh = DisplayHeight(dpy, screen); + root = RootWindow(dpy, screen); + drw = drw_create(dpy, screen, root, sw, sh); + if (!drw_fontset_create(drw, fonts, LENGTH(fonts))) + die("no fonts could be loaded."); + lrpad = drw->fonts->h; + bh = drw->fonts->h + 2; + updategeom(); + /* init atoms */ + utf8string = XInternAtom(dpy, "UTF8_STRING", False); + wmatom[WMProtocols] = XInternAtom(dpy, "WM_PROTOCOLS", False); + wmatom[WMDelete] = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + wmatom[WMState] = XInternAtom(dpy, "WM_STATE", False); + wmatom[WMTakeFocus] = XInternAtom(dpy, "WM_TAKE_FOCUS", False); + netatom[NetActiveWindow] = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); + netatom[NetSupported] = XInternAtom(dpy, "_NET_SUPPORTED", False); + netatom[NetWMName] = XInternAtom(dpy, "_NET_WM_NAME", False); + netatom[NetWMState] = XInternAtom(dpy, "_NET_WM_STATE", False); + netatom[NetWMCheck] = XInternAtom(dpy, "_NET_SUPPORTING_WM_CHECK", False); + netatom[NetWMFullscreen] = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); + 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); + /* init cursors */ + cursor[CurNormal] = drw_cur_create(drw, XC_left_ptr); + cursor[CurResize] = drw_cur_create(drw, XC_sizing); + cursor[CurMove] = drw_cur_create(drw, XC_fleur); + /* init appearance */ + scheme = ecalloc(LENGTH(colors), sizeof(Clr *)); + for (i = 0; i < LENGTH(colors); i++) + scheme[i] = drw_scm_create(drw, colors[i], 3); + /* init bars */ + updatebars(); + updatestatus(); + /* supporting window for NetWMCheck */ + wmcheckwin = XCreateSimpleWindow(dpy, root, 0, 0, 1, 1, 0, 0, 0); + XChangeProperty(dpy, wmcheckwin, netatom[NetWMCheck], XA_WINDOW, 32, + PropModeReplace, (unsigned char *) &wmcheckwin, 1); + XChangeProperty(dpy, wmcheckwin, netatom[NetWMName], utf8string, 8, + PropModeReplace, (unsigned char *) "dwm", 3); + XChangeProperty(dpy, root, netatom[NetWMCheck], XA_WINDOW, 32, + PropModeReplace, (unsigned char *) &wmcheckwin, 1); + /* EWMH support per view */ + XChangeProperty(dpy, root, netatom[NetSupported], XA_ATOM, 32, + PropModeReplace, (unsigned char *) netatom, NetLast); + XDeleteProperty(dpy, root, netatom[NetClientList]); + /* select events */ + wa.cursor = cursor[CurNormal]->cursor; + wa.event_mask = SubstructureRedirectMask|SubstructureNotifyMask + |ButtonPressMask|PointerMotionMask|EnterWindowMask + |LeaveWindowMask|StructureNotifyMask|PropertyChangeMask; + XChangeWindowAttributes(dpy, root, CWEventMask|CWCursor, &wa); + XSelectInput(dpy, root, wa.event_mask); + grabkeys(); + focus(NULL); +} + +void +seturgent(Client *c, int urg) +{ + XWMHints *wmh; + + c->isurgent = urg; + if (!(wmh = XGetWMHints(dpy, c->win))) + return; + wmh->flags = urg ? (wmh->flags | XUrgencyHint) : (wmh->flags & ~XUrgencyHint); + XSetWMHints(dpy, c->win, wmh); + XFree(wmh); +} + +void +showhide(Client *c) +{ + if (!c) + return; + if (ISVISIBLE(c)) { + /* show clients top down */ + XMoveWindow(dpy, c->win, c->x, c->y); + if ((!c->mon->lt[c->mon->sellt]->arrange || c->isfloating) && !c->isfullscreen) + resize(c, c->x, c->y, c->w, c->h, 0); + showhide(c->snext); + } else { + /* hide clients bottom up */ + showhide(c->snext); + XMoveWindow(dpy, c->win, WIDTH(c) * -2, c->y); + } +} + +void +spawn(const Arg *arg) +{ + struct sigaction sa; + + if (arg->v == dmenucmd) + dmenumon[0] = '0' + selmon->num; + if (fork() == 0) { + if (dpy) + close(ConnectionNumber(dpy)); + setsid(); + + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_handler = SIG_DFL; + sigaction(SIGCHLD, &sa, NULL); + + execvp(((char **)arg->v)[0], (char **)arg->v); + die("dwm: execvp '%s' failed:", ((char **)arg->v)[0]); + } +} + +void +tag(const Arg *arg) +{ + if (selmon->sel && arg->ui & TAGMASK) { + selmon->sel->tags = arg->ui & TAGMASK; + focus(NULL); + arrange(selmon); + } +} + +void +tagmon(const Arg *arg) +{ + if (!selmon->sel || !mons->next) + return; + sendmon(selmon->sel, dirtomon(arg->i)); +} + +void +tile(Monitor *m) +{ + unsigned int i, n, h, mw, my, ty; + Client *c; + + for (n = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), n++); + if (n == 0) + return; + + 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); + } 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); + } +} + +void +togglebar(const Arg *arg) +{ + selmon->showbar = !selmon->showbar; + updatebarpos(selmon); + XMoveResizeWindow(dpy, selmon->barwin, selmon->wx, selmon->by, selmon->ww, bh); + arrange(selmon); +} + +void +togglefloating(const Arg *arg) +{ + if (!selmon->sel) + return; + if (selmon->sel->isfullscreen) /* no support for fullscreen windows */ + return; + selmon->sel->isfloating = !selmon->sel->isfloating || selmon->sel->isfixed; + if (selmon->sel->isfloating) + resize(selmon->sel, selmon->sel->x, selmon->sel->y, + selmon->sel->w, selmon->sel->h, 0); + arrange(selmon); +} + +void +toggletag(const Arg *arg) +{ + unsigned int newtags; + + if (!selmon->sel) + return; + newtags = selmon->sel->tags ^ (arg->ui & TAGMASK); + if (newtags) { + selmon->sel->tags = newtags; + focus(NULL); + arrange(selmon); + } +} + +void +toggleview(const Arg *arg) +{ + unsigned int newtagset = selmon->tagset[selmon->seltags] ^ (arg->ui & TAGMASK); + + if (newtagset) { + selmon->tagset[selmon->seltags] = newtagset; + focus(NULL); + arrange(selmon); + } +} + +void +unfocus(Client *c, int setfocus) +{ + if (!c) + return; + grabbuttons(c, 0); + XSetWindowBorder(dpy, c->win, scheme[SchemeNorm][ColBorder].pixel); + if (setfocus) { + XSetInputFocus(dpy, root, RevertToPointerRoot, CurrentTime); + XDeleteProperty(dpy, root, netatom[NetActiveWindow]); + } +} + +void +unmanage(Client *c, int destroyed) +{ + Monitor *m = c->mon; + XWindowChanges wc; + + detach(c); + detachstack(c); + if (!destroyed) { + wc.border_width = c->oldbw; + XGrabServer(dpy); /* avoid race conditions */ + XSetErrorHandler(xerrordummy); + XSelectInput(dpy, c->win, NoEventMask); + XConfigureWindow(dpy, c->win, CWBorderWidth, &wc); /* restore border */ + XUngrabButton(dpy, AnyButton, AnyModifier, c->win); + setclientstate(c, WithdrawnState); + XSync(dpy, False); + XSetErrorHandler(xerror); + XUngrabServer(dpy); + } + free(c); + focus(NULL); + updateclientlist(); + arrange(m); +} + +void +unmapnotify(XEvent *e) +{ + Client *c; + XUnmapEvent *ev = &e->xunmap; + + if ((c = wintoclient(ev->window))) { + if (ev->send_event) + setclientstate(c, WithdrawnState); + else + unmanage(c, 0); + } +} + +void +updatebars(void) +{ + Monitor *m; + XSetWindowAttributes wa = { + .override_redirect = True, + .background_pixmap = ParentRelative, + .event_mask = ButtonPressMask|ExposureMask + }; + XClassHint ch = {"dwm", "dwm"}; + for (m = mons; m; m = m->next) { + if (m->barwin) + continue; + m->barwin = XCreateWindow(dpy, root, m->wx, m->by, m->ww, bh, 0, DefaultDepth(dpy, screen), + CopyFromParent, DefaultVisual(dpy, screen), + CWOverrideRedirect|CWBackPixmap|CWEventMask, &wa); + XDefineCursor(dpy, m->barwin, cursor[CurNormal]->cursor); + XMapRaised(dpy, m->barwin); + XSetClassHint(dpy, m->barwin, &ch); + } +} + +void +updatebarpos(Monitor *m) +{ + m->wy = m->my; + m->wh = m->mh; + if (m->showbar) { + m->wh -= bh; + m->by = m->topbar ? m->wy : m->wy + m->wh; + m->wy = m->topbar ? m->wy + bh : m->wy; + } else + m->by = -bh; +} + +void +updateclientlist(void) +{ + Client *c; + Monitor *m; + + XDeleteProperty(dpy, root, netatom[NetClientList]); + for (m = mons; m; m = m->next) + for (c = m->clients; c; c = c->next) + XChangeProperty(dpy, root, netatom[NetClientList], + XA_WINDOW, 32, PropModeAppend, + (unsigned char *) &(c->win), 1); +} + +int +updategeom(void) +{ + int dirty = 0; + +#ifdef XINERAMA + if (XineramaIsActive(dpy)) { + int i, j, n, nn; + Client *c; + Monitor *m; + XineramaScreenInfo *info = XineramaQueryScreens(dpy, &nn); + XineramaScreenInfo *unique = NULL; + + for (n = 0, m = mons; m; m = m->next, n++); + /* only consider unique geometries as separate screens */ + unique = ecalloc(nn, sizeof(XineramaScreenInfo)); + for (i = 0, j = 0; i < nn; i++) + if (isuniquegeom(unique, j, &info[i])) + memcpy(&unique[j++], &info[i], sizeof(XineramaScreenInfo)); + XFree(info); + nn = j; + + /* new monitors if nn > n */ + for (i = n; i < nn; i++) { + for (m = mons; m && m->next; m = m->next); + if (m) + m->next = createmon(); + else + mons = createmon(); + } + for (i = 0, m = mons; i < nn && m; m = m->next, i++) + if (i >= n + || unique[i].x_org != m->mx || unique[i].y_org != m->my + || unique[i].width != m->mw || unique[i].height != m->mh) + { + dirty = 1; + m->num = i; + m->mx = m->wx = unique[i].x_org; + m->my = m->wy = unique[i].y_org; + m->mw = m->ww = unique[i].width; + m->mh = m->wh = unique[i].height; + updatebarpos(m); + } + /* removed monitors if n > nn */ + for (i = nn; i < n; i++) { + for (m = mons; m && m->next; m = m->next); + while ((c = m->clients)) { + dirty = 1; + m->clients = c->next; + detachstack(c); + c->mon = mons; + attach(c); + attachstack(c); + } + if (m == selmon) + selmon = mons; + cleanupmon(m); + } + free(unique); + } else +#endif /* XINERAMA */ + { /* default monitor setup */ + if (!mons) + mons = createmon(); + if (mons->mw != sw || mons->mh != sh) { + dirty = 1; + mons->mw = mons->ww = sw; + mons->mh = mons->wh = sh; + updatebarpos(mons); + } + } + if (dirty) { + selmon = mons; + selmon = wintomon(root); + } + return dirty; +} + +void +updatenumlockmask(void) +{ + unsigned int i, j; + XModifierKeymap *modmap; + + numlockmask = 0; + modmap = XGetModifierMapping(dpy); + for (i = 0; i < 8; i++) + for (j = 0; j < modmap->max_keypermod; j++) + if (modmap->modifiermap[i * modmap->max_keypermod + j] + == XKeysymToKeycode(dpy, XK_Num_Lock)) + numlockmask = (1 << i); + XFreeModifiermap(modmap); +} + +void +updatesizehints(Client *c) +{ + long msize; + XSizeHints size; + + if (!XGetWMNormalHints(dpy, c->win, &size, &msize)) + /* size is uninitialized, ensure that size.flags aren't used */ + size.flags = PSize; + if (size.flags & PBaseSize) { + c->basew = size.base_width; + c->baseh = size.base_height; + } else if (size.flags & PMinSize) { + c->basew = size.min_width; + c->baseh = size.min_height; + } else + c->basew = c->baseh = 0; + if (size.flags & PResizeInc) { + c->incw = size.width_inc; + c->inch = size.height_inc; + } else + c->incw = c->inch = 0; + if (size.flags & PMaxSize) { + c->maxw = size.max_width; + c->maxh = size.max_height; + } else + c->maxw = c->maxh = 0; + if (size.flags & PMinSize) { + c->minw = size.min_width; + c->minh = size.min_height; + } else if (size.flags & PBaseSize) { + c->minw = size.base_width; + c->minh = size.base_height; + } else + c->minw = c->minh = 0; + if (size.flags & PAspect) { + c->mina = (float)size.min_aspect.y / size.min_aspect.x; + c->maxa = (float)size.max_aspect.x / size.max_aspect.y; + } else + c->maxa = c->mina = 0.0; + c->isfixed = (c->maxw && c->maxh && c->maxw == c->minw && c->maxh == c->minh); + c->hintsvalid = 1; +} + +void +updatestatus(void) +{ + if (!gettextprop(root, XA_WM_NAME, stext, sizeof(stext))) + strcpy(stext, "dwm-"VERSION); + drawbar(selmon); +} + +void +updatetitle(Client *c) +{ + if (!gettextprop(c->win, netatom[NetWMName], c->name, sizeof c->name)) + gettextprop(c->win, XA_WM_NAME, c->name, sizeof c->name); + if (c->name[0] == '\0') /* hack to mark broken clients */ + strcpy(c->name, broken); +} + +void +updatewindowtype(Client *c) +{ + Atom state = getatomprop(c, netatom[NetWMState]); + Atom wtype = getatomprop(c, netatom[NetWMWindowType]); + + if (state == netatom[NetWMFullscreen]) + setfullscreen(c, 1); + if (wtype == netatom[NetWMWindowTypeDialog]) + c->isfloating = 1; +} + +void +updatewmhints(Client *c) +{ + XWMHints *wmh; + + if ((wmh = XGetWMHints(dpy, c->win))) { + if (c == selmon->sel && wmh->flags & XUrgencyHint) { + wmh->flags &= ~XUrgencyHint; + XSetWMHints(dpy, c->win, wmh); + } else + c->isurgent = (wmh->flags & XUrgencyHint) ? 1 : 0; + if (wmh->flags & InputHint) + c->neverfocus = !wmh->input; + else + c->neverfocus = 0; + XFree(wmh); + } +} + +void +view(const Arg *arg) +{ + if ((arg->ui & TAGMASK) == selmon->tagset[selmon->seltags]) + return; + selmon->seltags ^= 1; /* toggle sel tagset */ + if (arg->ui & TAGMASK) + selmon->tagset[selmon->seltags] = arg->ui & TAGMASK; + focus(NULL); + arrange(selmon); +} + +Client * +wintoclient(Window w) +{ + Client *c; + Monitor *m; + + for (m = mons; m; m = m->next) + for (c = m->clients; c; c = c->next) + if (c->win == w) + return c; + return NULL; +} + +Monitor * +wintomon(Window w) +{ + int x, y; + Client *c; + Monitor *m; + + if (w == root && getrootptr(&x, &y)) + return recttomon(x, y, 1, 1); + for (m = mons; m; m = m->next) + if (w == m->barwin) + return m; + if ((c = wintoclient(w))) + return c->mon; + return selmon; +} + +/* There's no way to check accesses to destroyed windows, thus those cases are + * ignored (especially on UnmapNotify's). Other types of errors call Xlibs + * default error handler, which may call exit. */ +int +xerror(Display *dpy, 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; + fprintf(stderr, "dwm: fatal error: request code=%d, error code=%d\n", + ee->request_code, ee->error_code); + return xerrorxlib(dpy, ee); /* may call exit */ +} + +int +xerrordummy(Display *dpy, XErrorEvent *ee) +{ + return 0; +} + +/* Startup Error handler to check if another window manager + * is already running. */ +int +xerrorstart(Display *dpy, XErrorEvent *ee) +{ + die("dwm: another window manager is already running"); + return -1; +} + +void +zoom(const Arg *arg) +{ + Client *c = selmon->sel; + + if (!selmon->lt[selmon->sellt]->arrange || !c || c->isfloating) + return; + if (c == nexttiled(selmon->clients) && !(c = nexttiled(c->next))) + return; + pop(c); +} + +int +main(int argc, char *argv[]) +{ + if (argc == 2 && !strcmp("-v", argv[1])) + die("dwm-"VERSION); + else if (argc != 1) + die("usage: dwm [-v]"); + if (!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + fputs("warning: no locale support\n", stderr); + if (!(dpy = XOpenDisplay(NULL))) + die("dwm: cannot open display"); + checkotherwm(); + setup(); +#ifdef __OpenBSD__ + if (pledge("stdio rpath proc exec", NULL) == -1) + die("pledge"); +#endif /* __OpenBSD__ */ + scan(); + run(); + cleanup(); + XCloseDisplay(dpy); + return EXIT_SUCCESS; +} diff --git a/dwm.png b/dwm.png new file mode 100644 index 0000000000000000000000000000000000000000..b1f9ba7e5f4cc7350ee2392ebcea5fcbe00fb49b GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0y~yU^u|Qz!1Q}1Y+HIE6l*az?S6g?!xdN1Q+aGKAC}m zfwRCPvY3H^TNs2H8D`CqU|?WiFY)wsWxvKQ%wx!QPg+ZWfq_A`#5JPCIX^cyHLrxh zxhOTUBsE2$JhLQ2AtWPJ!QIn0;C+f}9s>izO-~ockcwMx?>cfd81OJSD*yX``Xg^6 zTaZz4SYYHW?+Yi+q)+P5khOpNZ+qolCYF{40Rbi^MnwgdmIen04h}{|1pxsjCkGA= zh?J9q0!T_g04&AA!nDZw&Oe5$@0m;M>sM6OY71;LKv54jO6E(b|BHY2*SA&9y8V3S z8iyN)!0OOUb8_ejn#VrNZ2je0``gbZufgI1u!%>m)^#5eUAZ~`sxkuu1B0ilpUXO@ GgeCxv)opSB literal 0 HcmV?d00001 diff --git a/transient.c b/transient.c new file mode 100644 index 0000000..040adb5 --- /dev/null +++ b/transient.c @@ -0,0 +1,42 @@ +/* cc transient.c -o transient -lX11 */ + +#include +#include +#include +#include + +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); +} diff --git a/util.c b/util.c new file mode 100644 index 0000000..8e26a51 --- /dev/null +++ b/util.c @@ -0,0 +1,37 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#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; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..c0a50d4 --- /dev/null +++ b/util.h @@ -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);