Last active
April 7, 2026 17:10
-
-
Save johnfmorton/9961b6f97954067396a2c338269f3d76 to your computer and use it in GitHub Desktop.
A zsh function for chatting with local Gemma 4 via Ollama — streaming output, Glow markdown rendering, conversation history, and @filename / PDF support.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # the following function goes in your .zshrc file to add "gemma" to your command line | |
| # see https://supergeekery.ddev.site/blog/running-googles-gemma-4-locally-on-macos-with-ollama | |
| # and https://supergeekery.com/blog/upgrading-your-local-gemma-setup | |
| # for more details | |
| gemma() { | |
| setopt local_options no_monitor no_notify | |
| if ! curl -sf http://localhost:11434/ > /dev/null 2>&1; then | |
| echo "Starting Ollama service..." | |
| brew services start ollama | |
| while ! curl -sf http://localhost:11434/ > /dev/null 2>&1; do | |
| sleep 1 | |
| done | |
| fi | |
| local has_glow=false | |
| command -v glow > /dev/null 2>&1 && has_glow=true | |
| local _gemma_system='Respond in plain Markdown only. Do not use LaTeX or math delimiters ($...$, \(...\), \[...\]). Write units and symbols as Unicode directly (e.g. 0°C, 32°F, π, ², ³, ½) instead of \circ, \text{}, \frac, etc.' | |
| # One-shot mode: pass prompt directly | |
| if [[ $# -gt 0 ]]; then | |
| local _prompt="$*" | |
| local _w _fp _fc | |
| for _w in ${(z)_prompt}; do | |
| if [[ "$_w" == @* ]]; then | |
| _fp="${_w#@}" | |
| if [[ -f "$_fp" ]]; then | |
| if [[ "$_fp" == *.pdf ]]; then | |
| if command -v pdftotext > /dev/null 2>&1; then | |
| echo "Reading PDF: ${_fp##*/}... (this may take a moment)" | |
| _fc=$(pdftotext "$_fp" -) | |
| else | |
| echo "Warning: install poppler for PDF support (brew install poppler)" | |
| return 1 | |
| fi | |
| else | |
| _fc=$(cat "$_fp") | |
| fi | |
| _prompt="${_prompt//$_w/$'\n\n--- Contents of '"$_fp"$' ---\n'"$_fc"$'\n--- End of '"$_fp"$' ---\n'}" | |
| else | |
| echo "File not found: $_fp" | |
| echo "Use the full path, e.g. @\$HOME/Downloads/$_fp" | |
| return 1 | |
| fi | |
| fi | |
| done | |
| # Use the HTTP API directly so we get clean text (no TUI escape codes | |
| # that `ollama run` emits even when redirected). Run curl in the | |
| # background and show a spinner until it finishes. | |
| local _out_file _payload | |
| _out_file=$(mktemp) | |
| _payload=$(jq -n --arg s "$_gemma_system" --arg p "$_prompt" \ | |
| '{"model": "gemma4:26b", "messages": [{"role":"system","content":$s},{"role":"user","content":$p}], "stream": false}') | |
| curl -sS http://localhost:11434/api/chat -d "$_payload" > "$_out_file" 2>&1 & | |
| local _curl_pid=$! | |
| local _spinner='|/-\' | |
| local _i=0 | |
| printf '\033[?25l' | |
| while kill -0 "$_curl_pid" 2>/dev/null; do | |
| printf '\r\033[2;90mThinking... %s\033[0m' "${_spinner:_i++%${#_spinner}:1}" | |
| sleep 0.1 | |
| done | |
| wait "$_curl_pid" | |
| local _status=$? | |
| printf '\r\033[K\033[?25h' | |
| if [[ $_status -ne 0 ]]; then | |
| cat "$_out_file" | |
| rm -f "$_out_file" | |
| return $_status | |
| fi | |
| local _content | |
| _content=$(jq -r '.message.content // empty' "$_out_file" 2>/dev/null) | |
| if [[ -z "$_content" ]]; then | |
| echo "Error: no response from model" | |
| cat "$_out_file" | |
| rm -f "$_out_file" | |
| return 1 | |
| fi | |
| if $has_glow; then | |
| printf '%s\n' "$_content" | glow | |
| else | |
| printf '%s\n' "$_content" | |
| fi | |
| rm -f "$_out_file" | |
| return | |
| fi | |
| # Interactive mode with spinner + glow render | |
| local tmpfile=$(mktemp) | |
| local response_file="" | |
| local prompt expanded content _w _fp _fc | |
| _gemma_cleanup() { | |
| printf '\033[0m' | |
| rm -f "$tmpfile" "$tmpfile.tmp" "$response_file" | |
| trap - INT TERM | |
| } | |
| trap '_gemma_cleanup; return' INT TERM | |
| jq -n --arg s "$_gemma_system" '[{"role":"system","content":$s}]' > "$tmpfile" | |
| echo "Chat with Gemma (type 'exit' or Ctrl-C to quit)" | |
| while true; do | |
| printf "\n>>> " | |
| read -r prompt || break | |
| [[ -z "$prompt" || "$prompt" == "exit" ]] && break | |
| # Expand @filename references to file contents | |
| expanded="$prompt" | |
| local _file_error=false | |
| for _w in ${(z)prompt}; do | |
| if [[ "$_w" == @* ]]; then | |
| _fp="${_w#@}" | |
| if [[ -f "$_fp" ]]; then | |
| if [[ "$_fp" == *.pdf ]]; then | |
| if command -v pdftotext > /dev/null 2>&1; then | |
| echo "Reading PDF: ${_fp##*/}... (this may take a moment)" | |
| _fc=$(pdftotext "$_fp" -) | |
| else | |
| echo "Warning: install poppler for PDF support (brew install poppler)" | |
| _file_error=true | |
| break | |
| fi | |
| else | |
| _fc=$(cat "$_fp") | |
| fi | |
| expanded="${expanded//$_w/$'\n\n--- Contents of '"$_fp"$' ---\n'"$_fc"$'\n--- End of '"$_fp"$' ---\n'}" | |
| else | |
| echo "File not found: $_fp (use the full path, e.g. @/Users/john/Downloads/$_fp)" | |
| _file_error=true | |
| break | |
| fi | |
| fi | |
| done | |
| if $_file_error; then | |
| continue | |
| fi | |
| jq --arg p "$expanded" '. + [{"role": "user", "content": $p}]' "$tmpfile" > "$tmpfile.tmp" \ | |
| && mv "$tmpfile.tmp" "$tmpfile" | |
| # Non-streaming request with spinner (matches one-shot mode) | |
| response_file=$(mktemp) | |
| local _payload=$(jq -n --argjson msgs "$(cat "$tmpfile")" \ | |
| '{"model": "gemma4:26b", "messages": $msgs, "stream": false}') | |
| curl -sS http://localhost:11434/api/chat -d "$_payload" > "$response_file" 2>&1 & | |
| local _curl_pid=$! | |
| local _spinner='|/-\' _i=0 | |
| printf '\033[?25l' | |
| while kill -0 "$_curl_pid" 2>/dev/null; do | |
| printf '\r\033[2;90mThinking... %s\033[0m' "${_spinner:_i++%${#_spinner}:1}" | |
| sleep 0.1 | |
| done | |
| wait "$_curl_pid" | |
| printf '\r\033[K\033[?25h' | |
| content=$(jq -r '.message.content // empty' "$response_file" 2>/dev/null) | |
| if [[ -z "$content" ]]; then | |
| echo "Error: no response from model" | |
| rm -f "$response_file" | |
| continue | |
| fi | |
| jq --arg c "$content" '. + [{"role": "assistant", "content": $c}]' "$tmpfile" > "$tmpfile.tmp" \ | |
| && mv "$tmpfile.tmp" "$tmpfile" | |
| if $has_glow; then | |
| printf '%s' "$content" | glow | |
| else | |
| printf '%s\n' "$content" | |
| fi | |
| rm -f "$response_file" | |
| done | |
| _gemma_cleanup | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment