initial commit

This commit is contained in:
2026-06-11 09:50:08 +03:00
parent 6ce460fcb1
commit 8bf868c111
5 changed files with 331 additions and 5 deletions
+20 -5
View File
@@ -1,9 +1,24 @@
Copyright (c) 2026 amin BSD 2-Clause License
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Copyright (c) 2026, Ämin Baumeler
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+4
View File
@@ -0,0 +1,4 @@
install:
install ./punch ~/.local/bin/
install ./punch.bash ~/.local/share/bash-completion/completions/
install ./punch.1.gz ~/.local/share/man/man1/
Executable
+293
View File
@@ -0,0 +1,293 @@
#!/bin/sh
set -eu
cmd=$(basename $0)
PUNCHWALLET=${PUNCHWALLET:-$HOME/.local/share/$cmd/wallet}
statefile=$HOME/.local/share/$cmd/state
# Helper function to show an error and quit
err() {
printf "Error: %s\n" "$1" > /dev/stderr
exit 1
}
# List of memory states (cannot contain white spaces)
STATE_PAUSED="paused"
STATE_RUNNING="running"
# Create missing directories and state file
[ -d "$PUNCHWALLET" ] || mkdir -p "$PUNCHWALLET" || err "Failed to create wallet $PUNCHWALLET"
statedir=$(dirname "$statefile")
[ -d "$statedir" ] || mkdir -p "$statedir" || err "Failed to create directory to keep the state"
[ -r "$statefile" ] || touch "$statefile" || err "Could not create state file"
# Functions that define the respective commands
help() {
printf "\
Usage: %s <command>
Punch and inspect your cards for time keeping.
Commands:
ls List available cards in your wallet
today [<card> ..] Show time spent at \$DATE (today)
week [<card> ..] Show time spent in the week containing \$DATE (today)
cat [<card> ..] In detail show time spent at \$DATE (today)
elapsed Show time elapsed in current timekeeping
rm <card> [<card> ..] Remove the specified cards
start <card> Start timekeeping in the specified card
pause Pause timekeeping
resume Resume timekeeping for paused card
toggle Toggle between paused and running states
stop Stop timekeeping
state Print current state
By design, this application does not allow for double time keeping.
The cards are stored in the wallet \$PUNCHWALLET which is set to
~/.local/share/%s/wallet by default.
" "$cmd" "$cmd"
exit 0
}
## State machine
# If timekeeping is currently active, then this function prints the card name
# and returns true. Otherwise, it prints nothing and returns false.
__is_running() {
grep -q "$STATE_RUNNING" "$statefile" && cut -d ' ' -f 2- "$statefile"
}
# Same as __is_running but for the paused state
__is_paused() {
grep -q "^$STATE_PAUSED " "$statefile" && cut -d ' ' -f 2- "$statefile"
}
# Set the current state
__set_running() {
printf "%s %s" "$STATE_RUNNING" "$1" > "$statefile"
}
__set_paused() {
printf "%s %s" "$STATE_PAUSED" "$1" > "$statefile"
}
__set_stopped() {
printf "" > "$statefile"
}
## Database
# Check cards
# This function aborts if a card is not given and prints the card names with
# duplicates removed.
__check_cards() {
[ "${1:-}" ] || err "No card specified"
for card in $(echo "$@" | tr ' ' "\n" | sort -u); do
[ ! -f "$PUNCHWALLET/$card" ] && err "Card \"$1\" does not exist"
echo "$card"
done
}
# Write time to punch card
# @argument $1: punch card
# @argument $2: if set, write end time
__time_write() {
printf "%s%s" "${2:+ }" "$(date +"%s")" >> "$PUNCHWALLET/$1"
[ ! "${2:-}" ] || (printf '\n' >> "$PUNCHWALLET/$1")
}
# Get times of specified cards
# This returns "closed" time spans in the sense that each line has two numbers,
# start and end time (even if some timekeeping process is still ongoing; just
# put `now`).
# @argument $1: start time
# @argument $2: range, any of {'day', 'week'}
# @argument $3, ...: cards
__times_retrieve() {
start=$1
shift
range=$((24 * 60 * 60))
[ "$1" = "week" ] && range=$((7 * range))
shift
# Check availability and duplicates
# Retrieve times
now=$(date +"%s")
[ $now -lt $start ] && return
for card in $@; do
cat "$PUNCHWALLET/$card"
echo ""
done | sort -u | awk -vstart="$start" -vend="$((start + range))" -vnow="$now" '
NF == 0 { next }
$1 > end { exit }
NF == 1 { $2 = now }
$2 < start { next }
$1 < start { $1 = start }
$2 > end { $2 = end }
{ print }'
}
## Commands
# List available cards
list_cards() {
ls "$PUNCHWALLET"
}
# Show time spent today
show_today() {
[ "$#" -eq 0 ] && set - $(ls "$PUNCHWALLET")
cards=$(__check_cards "$@")
daystart=$(date --date "$DATE 00:00" +"%s")
__times_retrieve "$daystart" "day" "$cards" | \
awk '{total = total + $2 - $1} END { if (total) print "@"total }' | \
date --utc -f /dev/stdin +"%H:%M"
}
# Print weekly summary
show_week() {
[ "$#" -eq 0 ] && set - $(ls "$PUNCHWALLET")
cards=$(__check_cards "$@")
echo " Mon Tue Wed Thu Fri Sat Sun"
startofweek=$(date --date "$DATE - $(date --date "$DATE" +"%u") days + 1 day 00:00:00" +"%s")
now=$(date +"%s")
# Filter times, split at midnight, sort, sum, print, and also add "now now"
# as a trick to fill the times until today
__times_retrieve "$startofweek" "week" "$cards" | \
awk -vstart="$startofweek" -vsecperday="$((60 * 60 * 24))" -vnow="$now" '{
t0 = $1 - start
t1 = $2 - start
day0 = int(t0 / secperday)
day1 = int(t1 / secperday)
delta = 0
while(day0 + delta < day1) {
delta = delta + 1
p = (day0+delta)*secperday+start-1
print $1, p
$1 = p + 1
}
print $1, $2
}
END { for(d=0; d<7 && start + d * secperday < now; d++)
print start + d * secperday, start + d * secperday }' | \
sort | \
awk -vstart="$startofweek" -vsecperday="$((60 * 60 * 24))" '
BEGIN { total = 0 }
{
day = int(($1 - start) / secperday)
if (day > currentday) {
print "@"total
currentday = day
total = 0
}
total = total + $2 - $1
}
END {print "@"total}' | \
date --utc -f /dev/stdin +"%H:%M" | xargs -I {} printf "%s " {}
printf "\n"
}
# Show details of today
show_details() {
[ "$#" -eq 0 ] && set - $(ls "$PUNCHWALLET")
cards=$(__check_cards "$@")
daystart=$(date --date "$DATE 00:00" +"%s")
tmpf=$(mktemp)
__times_retrieve "$daystart" "day" "$cards" > "$tmpf"
# Start times
fstart=$(mktemp)
awk '{print "@"$1}' < "$tmpf" | date -f /dev/stdin +"%H:%M" > "$fstart"
# duration
fdur=$(mktemp)
awk '{print "@"($2-$1)}' < "$tmpf" | date --utc -f /dev/stdin +"%H:%M:%S" | \
sed 's/^\(.*\)$/(up \1)/'> "$fdur"
# combine
paste "$fstart" "$fdur"
# clean up
rm -f "$tmpf" "$fstart" "$fdur"
}
# Delete punch card
remove_card() {
[ "${1:-}" ] || err "No card specified"
for card in $@; do
[ ! -f "$PUNCHWALLET/$card" ] && err "Card \"$card\" does not exist"
rm -i "$PUNCHWALLET/$card"
done
# Update state if necessary
active=$(__is_running || __is_paused)
for card in $@; do
if [ "$active" = "$card" ]; then
__set_stopped
break
fi
done
}
# Pause currently running time-keeping
pause_timekeeping() {
card=$(__is_running)
__time_write "$card" end
__set_paused "$card"
}
# Resume paused time-keeping
resume_timekeeping() {
card=$(__is_paused)
__time_write "$card"
__set_running "$card"
}
# Toggle pause/resume
toggle_timekeeping() {
__is_running && pause_timekeeping || (__is_paused && resume_timekeeping)
}
# Stop time-keeping
stop_timekeeping() {
__is_running && pause_timekeeping || true
__is_paused && __set_stopped
}
# Punch card
punch_card() {
[ "${1:-}" ] || err "No card specified"
echo "$1" | grep -q '[^A-Za-z0-9]' && err "Use only alphanumeric characters"
__is_running && stop_timekeeping || true
__time_write "$1"
__set_running "$1"
}
# Show elapsed time
show_elapsed() {
card=$(__is_running)
t0=$(tail -1 "$PUNCHWALLET/$card")
t1=$(date +"%s")
date --date @$((t1 - t0)) --utc +"%T"
}
# Show current state
show_state() {
cat "$statefile"
}
# Run command
DATE=${DATE:-today}
case "${1:-}" in
ls) list_cards ;;
today) shift; show_today "$@" ;;
week) shift; show_week "$@" ;;
cat) shift; show_details "$@" ;;
rm) shift; remove_card "$@" ;;
start) punch_card "${2:-}" > /dev/null ;;
pause) pause_timekeeping > /dev/null ;;
resume) resume_timekeeping > /dev/null ;;
toggle) toggle_timekeeping > /dev/null ;;
stop) stop_timekeeping > /dev/null ;;
state) show_state ;;
elapsed) show_elapsed ;;
*) help ;;
esac
BIN
View File
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
__punch_complete() {
local cmd=$1
local cur=$2
local prev=$3
if [ $COMP_CWORD -eq 1 ]; then
COMPREPLY=( $(compgen -W "$(punch|grep '^ '|sed 's/^ //'|cut -d' ' -f1)" -- $cur) )
else
case "${COMP_WORDS[1]}" in
start) [ $COMP_CWORD -eq 2 ] && COMPREPLY=( $(compgen -W "$(punch ls)" -- $cur) ) ;;
today|week|cat|rm) COMPREPLY=( $(compgen -W "$(punch ls)" -- $cur) ) ;;
esac
fi
} &&
complete -F __punch_complete punch