#!/usr/bin/env bash
###############################################################################
# dcalc — Advanced Scientific Calculator via dmenu
#
# Maintainer: Kris Yotam <krisyotam@pm.me>
# License: MIT
# Created: 2026-02-15
#
# Features:
# - Arbitrary precision arithmetic (bc -l backend)
# - Trigonometric functions (sin, cos, tan, asin, acos, atan) — radians & degrees
# - Logarithms (ln, log10, log2)
# - Powers, roots, factorials
# - Constants (pi, e, phi, sqrt2, sqrt3, c, avogadro, planck)
# - Unit conversions (temperature, distance, weight, data)
# - Persistent history (~/.cache/dcalc_history)
# - Result auto-copied to clipboard
#
# Usage:
# dcalc # Launch calculator
# Type expression # e.g. sin(pi/4), log10(1000), 2^10
# Type "hist" # Browse calculation history
# Type "const" # List available constants
# Type "conv" # Unit conversion mode
# Type "clear" # Clear history
###############################################################################
HIST_FILE="${HOME}/.cache/dcalc_history"
mkdir -p "$(dirname "$HIST_FILE")"
touch "$HIST_FILE"
DMENU="dmenu -i -l 20 -p"
###############################################################################
# bc prelude — define functions and constants
###############################################################################
BC_PRELUDE='
scale=20
/* constants */
pi=4*a(1)
e=e(1)
phi=(1+sqrt(5))/2
sqrt2=sqrt(2)
sqrt3=sqrt(3)
/* trig (radians) */
define sin(x) { return s(x) }
define cos(x) { return c(x) }
define tan(x) { return s(x)/c(x) }
define asin(x) { return a(x/sqrt(1-x*x)) }
define acos(x) { if (x==1) return 0; return a(sqrt(1-x*x)/x) }
define atan(x) { return a(x) }
/* trig (degrees) */
define dsin(x) { return s(x*pi/180) }
define dcos(x) { return c(x*pi/180) }
define dtan(x) { return s(x*pi/180)/c(x*pi/180) }
define dasin(x) { return a(x/sqrt(1-x*x))*180/pi }
define dacos(x) { if (x==1) return 0; return a(sqrt(1-x*x)/x)*180/pi }
define datan(x) { return a(x)*180/pi }
/* degrees <-> radians */
define rad(x) { return x*pi/180 }
define deg(x) { return x*180/pi }
/* logarithms */
define ln(x) { return l(x) }
define log10(x) { return l(x)/l(10) }
define log2(x) { return l(x)/l(2) }
define log(x) { return l(x)/l(10) }
/* powers and roots */
define pow(x,y) { return e(y*l(x)) }
define exp(x) { return e(x) }
define sqrt(x) { return sqrt(x) }
define cbrt(x) { return e(l(x)/3) }
define nrt(x,n) { return e(l(x)/n) }
/* combinatorics */
define fact(n) { if (n<=1) return 1; return n*fact(n-1) }
define abs(x) { if (x<0) return -x; return x }
define ceil(x) { auto s; s=scale; scale=0; x=x/1; scale=s; return x }
define floor(x) { auto s; s=scale; scale=0; x=x/1; scale=s; return x }
define max(a,b) { if (a>b) return a; return b }
define min(a,b) { if (a<b) return a; return b }
/* hyperbolic */
define sinh(x) { return (e(x)-e(-x))/2 }
define cosh(x) { return (e(x)+e(-x))/2 }
define tanh(x) { return (e(x)-e(-x))/(e(x)+e(-x)) }
'
###############################################################################
# helpers
###############################################################################
calc() {
echo "${BC_PRELUDE}; $1" | bc -l 2>&1
}
show_constants() {
printf '%s\n' \
"pi = 3.14159265358979323846" \
"e = 2.71828182845904523536" \
"phi = 1.61803398874989484820" \
"sqrt2 = 1.41421356237309504880" \
"sqrt3 = 1.73205080756887729352" \
"c = 299792458 (m/s)" \
"avogadro= 6.02214076e23 (1/mol)" \
"planck = 6.62607015e-34 (J*s)" \
| $DMENU " Constants" > /dev/null
}
show_help() {
printf '%s\n' \
"── Arithmetic ──" \
" 2+3 4*5 10/3 2^10 15%4" \
"── Trig (radians) ──" \
" sin(x) cos(x) tan(x) asin(x) acos(x) atan(x)" \
"── Trig (degrees) ──" \
" dsin(x) dcos(x) dtan(x) dasin(x) dacos(x) datan(x)" \
"── Conversion ──" \
" rad(deg) deg(rad)" \
"── Logarithms ──" \
" ln(x) log10(x) log2(x)" \
"── Powers/Roots ──" \
" pow(x,y) exp(x) sqrt(x) cbrt(x) nrt(x,n)" \
"── Other ──" \
" fact(n) abs(x) ceil(x) floor(x) max(a,b) min(a,b)" \
"── Hyperbolic ──" \
" sinh(x) cosh(x) tanh(x)" \
"── Commands ──" \
" const hist conv clear help" \
| $DMENU " Help" > /dev/null
}
unit_convert() {
local category
category=$(printf '%s\n' \
" Temperature" \
" Distance" \
" Weight" \
" Data" \
" Time" \
" Speed" \
| $DMENU " Convert")
[ -z "$category" ] && return
local conversion
case "$category" in
*Temperature*)
conversion=$(printf '%s\n' \
"C→F" "F→C" "C→K" "K→C" "F→K" "K→F" \
| $DMENU " Temp")
[ -z "$conversion" ] && return
local val
val=$($DMENU " Value" < /dev/null)
[ -z "$val" ] && return
local result
case "$conversion" in
"C→F") result=$(calc "$val * 9/5 + 32") ;;
"F→C") result=$(calc "($val - 32) * 5/9") ;;
"C→K") result=$(calc "$val + 273.15") ;;
"K→C") result=$(calc "$val - 273.15") ;;
"F→K") result=$(calc "($val - 32) * 5/9 + 273.15") ;;
"K→F") result=$(calc "($val - 273.15) * 9/5 + 32") ;;
esac
;;
*Distance*)
conversion=$(printf '%s\n' \
"mi→km" "km→mi" "ft→m" "m→ft" "in→cm" "cm→in" "yd→m" "m→yd" \
| $DMENU " Dist")
[ -z "$conversion" ] && return
local val
val=$($DMENU " Value" < /dev/null)
[ -z "$val" ] && return
local result
case "$conversion" in
"mi→km") result=$(calc "$val * 1.60934") ;;
"km→mi") result=$(calc "$val / 1.60934") ;;
"ft→m") result=$(calc "$val * 0.3048") ;;
"m→ft") result=$(calc "$val / 0.3048") ;;
"in→cm") result=$(calc "$val * 2.54") ;;
"cm→in") result=$(calc "$val / 2.54") ;;
"yd→m") result=$(calc "$val * 0.9144") ;;
"m→yd") result=$(calc "$val / 0.9144") ;;
esac
;;
*Weight*)
conversion=$(printf '%s\n' \
"lb→kg" "kg→lb" "oz→g" "g→oz" "st→kg" "kg→st" \
| $DMENU " Weight")
[ -z "$conversion" ] && return
local val
val=$($DMENU " Value" < /dev/null)
[ -z "$val" ] && return
local result
case "$conversion" in
"lb→kg") result=$(calc "$val * 0.453592") ;;
"kg→lb") result=$(calc "$val / 0.453592") ;;
"oz→g") result=$(calc "$val * 28.3495") ;;
"g→oz") result=$(calc "$val / 28.3495") ;;
"st→kg") result=$(calc "$val * 6.35029") ;;
"kg→st") result=$(calc "$val / 6.35029") ;;
esac
;;
*Data*)
conversion=$(printf '%s\n' \
"B→KB" "KB→MB" "MB→GB" "GB→TB" \
"TB→GB" "GB→MB" "MB→KB" "KB→B" \
"B→bits" "bits→B" \
| $DMENU " Data")
[ -z "$conversion" ] && return
local val
val=$($DMENU " Value" < /dev/null)
[ -z "$val" ] && return
local result
case "$conversion" in
"B→KB") result=$(calc "$val / 1024") ;;
"KB→MB") result=$(calc "$val / 1024") ;;
"MB→GB") result=$(calc "$val / 1024") ;;
"GB→TB") result=$(calc "$val / 1024") ;;
"TB→GB") result=$(calc "$val * 1024") ;;
"GB→MB") result=$(calc "$val * 1024") ;;
"MB→KB") result=$(calc "$val * 1024") ;;
"KB→B") result=$(calc "$val * 1024") ;;
"B→bits") result=$(calc "$val * 8") ;;
"bits→B") result=$(calc "$val / 8") ;;
esac
;;
*Time*)
conversion=$(printf '%s\n' \
"hr→min" "min→hr" "min→sec" "sec→min" \
"hr→sec" "sec→hr" "days→hr" "hr→days" \
| $DMENU " Time")
[ -z "$conversion" ] && return
local val
val=$($DMENU " Value" < /dev/null)
[ -z "$val" ] && return
local result
case "$conversion" in
"hr→min") result=$(calc "$val * 60") ;;
"min→hr") result=$(calc "$val / 60") ;;
"min→sec") result=$(calc "$val * 60") ;;
"sec→min") result=$(calc "$val / 60") ;;
"hr→sec") result=$(calc "$val * 3600") ;;
"sec→hr") result=$(calc "$val / 3600") ;;
"days→hr") result=$(calc "$val * 24") ;;
"hr→days") result=$(calc "$val / 24") ;;
esac
;;
*Speed*)
conversion=$(printf '%s\n' \
"mph→kph" "kph→mph" "mph→m/s" "m/s→mph" "kph→m/s" "m/s→kph" "knots→kph" "kph→knots" \
| $DMENU " Speed")
[ -z "$conversion" ] && return
local val
val=$($DMENU " Value" < /dev/null)
[ -z "$val" ] && return
local result
case "$conversion" in
"mph→kph") result=$(calc "$val * 1.60934") ;;
"kph→mph") result=$(calc "$val / 1.60934") ;;
"mph→m/s") result=$(calc "$val * 0.44704") ;;
"m/s→mph") result=$(calc "$val / 0.44704") ;;
"kph→m/s") result=$(calc "$val / 3.6") ;;
"m/s→kph") result=$(calc "$val * 3.6") ;;
"knots→kph") result=$(calc "$val * 1.852") ;;
"kph→knots") result=$(calc "$val / 1.852") ;;
esac
;;
esac
[ -z "$result" ] && return
# Trim trailing zeros but keep reasonable precision
result=$(echo "$result" | sed 's/\.\{0,1\}0*$//')
echo "$conversion: $val → $result" >> "$HIST_FILE"
printf '%s' "$result" | xclip -selection clipboard
notify-send " dcalc" "$conversion: $val → $result"
}
###############################################################################
# main loop
###############################################################################
while true; do
# Build prompt from history (most recent at top) + empty for new input
INPUT=$(tac "$HIST_FILE" 2>/dev/null | $DMENU " Calc" < /dev/null)
# Exit on empty / escape
[ -z "$INPUT" ] && exit 0
# Handle special commands
case "$INPUT" in
hist|history)
if [ -s "$HIST_FILE" ]; then
sel=$(tac "$HIST_FILE" | $DMENU " History")
if [ -n "$sel" ]; then
# Extract just the result (after = sign)
result="${sel##*= }"
printf '%s' "$result" | xclip -selection clipboard
notify-send " dcalc" "Copied: $result"
fi
else
notify-send " dcalc" "No history yet"
fi
continue
;;
const|constants)
show_constants
continue
;;
conv|convert)
unit_convert
continue
;;
clear)
: > "$HIST_FILE"
notify-send " dcalc" "History cleared"
continue
;;
help|"?")
show_help
continue
;;
quit|exit|q)
exit 0
;;
esac
# If user selected a history entry, extract the expression
if [[ "$INPUT" == *" = "* ]]; then
INPUT="${INPUT%% = *}"
fi
# Evaluate the expression
RESULT=$(calc "$INPUT" 2>&1)
# Check for errors
if [[ "$RESULT" == *"error"* ]] || [[ "$RESULT" == *"illegal"* ]] || [ -z "$RESULT" ]; then
notify-send " dcalc" "Error: $INPUT"
continue
fi
# Trim trailing zeros for cleaner display
RESULT=$(echo "$RESULT" | sed '/\..*[^0]/{s/0*$//}; s/\.$//')
# Save to history
echo "$INPUT = $RESULT" >> "$HIST_FILE"
# Copy to clipboard
printf '%s' "$RESULT" | xclip -selection clipboard
# Show result — selecting it continues the loop with result as new input
notify-send " dcalc" "$INPUT = $RESULT"
done