#!/usr/bin/env bash ############################################################################### # dcalc — Advanced Scientific Calculator via dmenu # # Maintainer: Kris Yotam # 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&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