clipmenu

Simple clipboard management using dmenu
git clone https://git.sinitax.com/cdown/clipmenu
Log | Files | Refs | README | LICENSE | sfeed.txt

commit 0011a2c3b981157f7decc2eac2c3db4adc04f77d
parent bcbe7b144598db4a103f14e8408c4b7327d6d5e1
Author: Chris Down <chris@chrisdown.name>
Date:   Wed,  3 Jun 2020 22:09:03 +0100

Merge branch 'release/6.1.0'

Diffstat:
M.travis.yml | 2+-
AMakefile | 14++++++++++++++
MREADME.md | 53+++++++++++++++++++++++++++++++++++++++--------------
Aclipctl | 27+++++++++++++++++++++++++++
Mclipdel | 12++++++++----
Mclipmenu | 14+++++++++++++-
Mclipmenud | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mtests/test-clipmenu | 2++
8 files changed, 157 insertions(+), 26 deletions(-)

diff --git a/.travis.yml b/.travis.yml @@ -3,7 +3,7 @@ language: bash dist: xenial script: - - shellcheck -s bash clipmenu clipmenud clipdel clipfsck + - shellcheck -s bash clipmenu clipmenud clipdel clipfsck clipctl - tests/test-clipmenu matrix: diff --git a/Makefile b/Makefile @@ -0,0 +1,14 @@ +# `dmenu` is not a hard dependency, but you need it unless +# you plan to set CM_LAUNCHER to another value like `rofi` +REQUIRED_BINS := xsel clipnotify +$(foreach bin,$(REQUIRED_BINS),\ + $(if $(shell command -v $(bin) 2> /dev/null),$(info Found `$(bin)`),$(error Missing Dep. Please install `$(bin)`))) + +.PHONY: install + +install: + install -D -m755 clipmenu /usr/bin/clipmenu + install -D -m755 clipmenud /usr/bin/clipmenud + install -D -m755 clipdel /usr/bin/clipdel + install -D -m755 clipctl /usr/bin/clipctl + install -D -m644 init/clipmenud.service /usr/lib/systemd/user/clipmenud.service diff --git a/README.md b/README.md @@ -10,13 +10,10 @@ clipmenu is a simple clipboard manager using [dmenu][] (or [rofi][] with # Usage Start `clipmenud`, then run `clipmenu` to select something to put on the -clipboard. +clipboard. For systemd users, a user service called `clipmenud` is packaged as +part of the project. -A systemd user service for starting clipmenud is included at -[init/clipmenud.service](https://github.com/cdown/clipmenu/blob/develop/init/clipmenud.service). -You can then start clipmenud like this: - - systemctl --user start clipmenud +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 @@ -24,25 +21,53 @@ 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' -If you prefer to collect clips on demand rather than running clipmenud as a -daemon, you can bind a key to the following command for one-off collection: - - CM_ONESHOT=1 clipmenud - For a full list of environment variables that clipmenud can take, please see `clipmenud --help`. +# Features + +The behavior of `clipmenud` can be customized through environment variables. +Despite being only <300 lines, clipmenu has many useful features, including: + +* Customising the maximum number of clips stored (default 1000) +* Disabling clip collection temporarily with `clipctl disable`, reenabling with + `clipctl enable` +* Not storing clipboard changes from certain applications, like password + managers +* Taking direct ownership of the clipboard +* ...and much more. + +Check `clipmenud --help` to view all possible environment variables and what +they do. If you manage `clipmenud` with `systemd`, you can override the +defaults by using `systemctl --user edit clipmenud` to generate an override +file. + +# Supported launchers + +Any dmenu-compliant application will work, but here are `CM_LAUNCHER` +configurations that are known to work: + +- `dmenu` (the default) +- `fzf` +- `rofi` +- `rofi-script`, for [rofi's script + mode](https://github.com/davatorium/rofi-scripts/tree/master/mode-scripts) + # Installation Several distributions, including Arch and Nix, provide clipmenu as an official package called `clipmenu`. -If your distribution doesn't provide a package, you can run the scripts -standalone (or better yet, package them!). +## 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!). You +will need `xsel` and `clipnotify` installed, and also `dmenu` unless you plan +to use a different launcher. # How does it work? -clipmenud is less than 200 lines, and clipmenu is less than 100, so hopefully +clipmenud is less than 300 lines, and clipmenu is less than 100, so hopefully it should be fairly self-explanatory. However, at the most basic level: ## clipmenud diff --git a/clipctl b/clipctl @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +if [[ -z $1 ]] || [[ $1 == --help ]] || [[ $1 == -h ]]; then + cat << 'EOF' +clipctl provides controls for the clipmenud daemon. + +You can temporarily disable clip collection without stopping clipmenud entirely +by running "clipctl disable". You can then reenable with "clipctl enable". +EOF + exit 0 +fi + +_CLIPMENUD_PID=$(pgrep -u "$(id -u)" -nf 'clipmenud$') + +if [[ -z "$_CLIPMENUD_PID" ]]; then + echo "clipmenud is not running" + exit 2 +fi + +case $1 in + enable) kill -USR2 "$_CLIPMENUD_PID" ;; + disable) kill -USR1 "$_CLIPMENUD_PID" ;; + *) + printf 'Unknown command: %s\n' "$1" + exit 1 + ;; +esac diff --git a/clipdel b/clipdel @@ -19,7 +19,8 @@ lock_timeout=2 if [[ $1 == --help ]] || [[ $1 == -h ]]; then cat << 'EOF' clipdel deletes clipmenu entries matching a regex. By default, just lists what -it would delete, pass -d to do it for real. +it would delete, pass -d to do it for real. If no pattern is passed as an argument, +it will try to read one from standard input. ".*" is special, it will just nuke the entire data directory, including the line caches and all other state. @@ -40,9 +41,12 @@ if ! [[ -f $cache_file ]]; then exit 0 # Well, this is a kind of success... fi -# https://github.com/koalaman/shellcheck/issues/1141 -# shellcheck disable=SC2124 -raw_pattern=$1 +if [[ -n $1 ]]; then + raw_pattern=$1 +elif ! [[ -t 0 ]]; then + IFS= read -r raw_pattern +fi + esc_pattern=${raw_pattern//\#/'\#'} # We use 2 separate sed commands so "esc_pattern" matches only the 'clip' text diff --git a/clipmenu b/clipmenu @@ -23,10 +23,18 @@ Environment variables: - $CM_DIR: specify the base directory to store the cache dir in (default: $XDG_RUNTIME_DIR, $TMPDIR, or /tmp) - $CM_HISTLENGTH: specify the number of lines to show in dmenu/rofi (default: 8) - $CM_LAUNCHER: specify a dmenu-compatible launcher (default: dmenu) +- $CM_OUTPUT_CLIP: if set, output clip selection to stdout EOF exit 0 fi + +# Blacklist of non-dmenu launchers +launcher_args=(-l "${CM_HISTLENGTH}") +if [[ "$CM_LAUNCHER" == fzf ]]; then + launcher_args=() +fi + # rofi supports dmenu-like arguments through the -dmenu flag [[ "$CM_LAUNCHER" == rofi ]] && set -- -dmenu "$@" @@ -42,7 +50,7 @@ if [[ "$CM_LAUNCHER" == rofi-script ]]; then exit fi else - chosen_line=$(list_clips | "$CM_LAUNCHER" -l "${CM_HISTLENGTH}" "$@") + chosen_line=$(list_clips | "$CM_LAUNCHER" "${launcher_args[@]}" "$@") fi [[ $chosen_line ]] || exit 1 @@ -52,3 +60,7 @@ file=$cache_dir/$(cksum <<< "$chosen_line") for selection in clipboard primary; do xsel --logfile /dev/null -i --"$selection" < "$file" done + +if (( CM_OUTPUT_CLIP )); then + cat "$file" +fi diff --git a/clipmenud b/clipmenud @@ -3,13 +3,13 @@ : "${CM_ONESHOT=0}" : "${CM_OWN_CLIPBOARD=0}" : "${CM_DEBUG=0}" -: "${CM_DIR="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}" +: "${CM_DIR:="${XDG_RUNTIME_DIR-"${TMPDIR-/tmp}"}"}" -: "${CM_MAX_CLIPS=1000}" +: "${CM_MAX_CLIPS:=1000}" # Buffer to batch to avoid calling too much. Only used if CM_MAX_CLIPS >0. CM_MAX_CLIPS_THRESH=$(( CM_MAX_CLIPS + 10 )) -: "${CM_SELECTIONS=clipboard primary}" +: "${CM_SELECTIONS:=clipboard primary}" read -r -a selections <<< "$CM_SELECTIONS" major_version=6 @@ -55,9 +55,34 @@ get_first_line() { debug() { (( CM_DEBUG )) && printf '%s\n' "$@" >&2; } +sig_disable() { + info "Received disable signal, suspending clipboard capture" + _CM_DISABLED=1 + _CM_FIRST_DISABLE=1 + [[ -v _CM_CLIPNOTIFY_PID ]] && kill "$_CM_CLIPNOTIFY_PID" +} + +sig_enable() { + if ! (( _CM_DISABLED )); then + info "Received enable signal but we're not disabled, so doing nothing" + return + fi + + # Still store the last data so we don't end up eventually putting it in the + # clipboard if it wasn't changed + for selection in "${selections[@]}"; do + data=$(_xsel -o --"$selection"; printf x) + last_data_sel[$selection]=${data%x} + done + + info "Received enable signal, resuming clipboard capture" + _CM_DISABLED=0 +} + if [[ $1 == --help ]] || [[ $1 == -h ]]; then cat << 'EOF' -clipmenud collects and caches what's on the clipboard. +clipmenud collects and caches what's on the clipboard. You can manage its +operation with clipctl. Environment variables: @@ -92,8 +117,29 @@ fi exec {lock_fd}> "$lock_file" +trap sig_disable USR1 +trap sig_enable USR2 + +# Kill all background processes on exit +trap 'trap - TERM; kill -- -$$' INT TERM EXIT + while true; do - (( CM_ONESHOT )) || clipnotify + if ! (( CM_ONESHOT )); then + # Make sure we're interruptible for the sig_{en,dis}able traps + clipnotify & + _CM_CLIPNOTIFY_PID="$!" + wait "$_CM_CLIPNOTIFY_PID" + fi + + if (( _CM_DISABLED )); then + # The first one will just be from interrupting `wait`, so don't print + if (( _CM_FIRST_DISABLE )); then + unset _CM_FIRST_DISABLE + else + info "Got a clipboard notification, but we are disabled, skipping" + fi + continue + fi if [[ $CM_IGNORE_WINDOW ]] && (( has_xdotool )); then windowname="$(xdotool getactivewindow getwindowname)" @@ -149,7 +195,8 @@ while true; do fi done - if (( CM_MAX_CLIPS )) && (( "$(wc -l < "$cache_file")" > CM_MAX_CLIPS_THRESH )); then + # The cache file may not exist if this is the first run and data is skipped + if (( CM_MAX_CLIPS )) && [[ -f "$cache_file" ]] && (( "$(wc -l < "$cache_file")" > CM_MAX_CLIPS_THRESH )); then info "Trimming clip cache to CM_MAX_CLIPS ($CM_MAX_CLIPS)" trunc_tmp=$(mktemp) tail -n "$CM_MAX_CLIPS" "$cache_file" | uniq > "$trunc_tmp" diff --git a/tests/test-clipmenu b/tests/test-clipmenu @@ -71,6 +71,8 @@ EOF temp=$(mktemp) +trap 'cat "$temp"' EXIT + /tmp/clipmenu --foo bar > "$temp" 2>&1 # Arguments are transparently passed to dmenu