Skip to content

Instantly share code, notes, and snippets.

@fersilva16
Last active March 19, 2026 19:06
Show Gist options
  • Select an option

  • Save fersilva16/5269b5b76b74fd892ccbc7f2060f365c to your computer and use it in GitHub Desktop.

Select an option

Save fersilva16/5269b5b76b74fd892ccbc7f2060f365c to your computer and use it in GitHub Desktop.
nix-darwin + OpenCode + tmux + Git Worktrees: complete macOS dev environment from scratch
#!/usr/bin/env bash
# tmux-battery-widget: Status bar widget showing battery percentage and state.
# Only shown when remote access mode is active (to keep status bar clean normally).
# Uses pmset to read battery info on macOS.
STATE_FILE="/tmp/tmux-remote-state"
# Flexoki light theme colors
BG="#f2f0e5"
FG="#100f0f"
GREEN="#879a39"
YELLOW="#d0a215"
RED="#d14d41"
ORANGE="#da702c"
RESET="#[fg=${FG},bg=${BG},nobold,noitalics,nounderscore,nodim]"
# Only show battery when remote mode is active
if [[ ! -f "$STATE_FILE" ]]; then
exit 0
fi
# Read battery info from pmset
BATTERY_INFO=$(pmset -g batt 2>/dev/null)
if [[ -z "$BATTERY_INFO" ]]; then
exit 0
fi
# Extract percentage (e.g., "85%")
PERCENT=$(echo "$BATTERY_INFO" | grep -oE '[0-9]+%' | head -1 | tr -d '%')
if [[ -z "$PERCENT" ]]; then
exit 0
fi
# Determine charging state
CHARGING=""
if echo "$BATTERY_INFO" | grep -q "AC Power"; then
CHARGING="󰂄"
elif echo "$BATTERY_INFO" | grep -q "charged"; then
CHARGING="󰂄"
fi
# Pick icon and color based on percentage
if [[ "$PERCENT" -ge 80 ]]; then
ICON="󰁹"
COLOR="$GREEN"
elif [[ "$PERCENT" -ge 60 ]]; then
ICON="󰂀"
COLOR="$GREEN"
elif [[ "$PERCENT" -ge 40 ]]; then
ICON="󰁾"
COLOR="$YELLOW"
elif [[ "$PERCENT" -ge 20 ]]; then
ICON="󰁻"
COLOR="$ORANGE"
else
ICON="󰁺"
COLOR="$RED"
fi
# Use charging icon if plugged in
if [[ -n "$CHARGING" ]]; then
ICON="$CHARGING"
fi
echo "#[fg=${COLOR},bg=${BG},bold] ${ICON} ${PERCENT}%${RESET} "
#!/usr/bin/env bash
# Flexoki light theme colors via ANSI escapes
# Using tput/ANSI for terminal coloring inside the popup
BOLD=$(tput bold)
DIM=$(tput dim)
RESET=$(tput sgr0)
CYAN=$(tput setaf 6)
YELLOW=$(tput setaf 3)
MAGENTA=$(tput setaf 5)
header() {
echo "${BOLD}${CYAN}$1${RESET}"
}
key() {
printf " ${YELLOW}%-20s${RESET} %s\n" "$1" "$2"
}
sep() {
echo ""
}
SEPARATOR="${DIM}$(printf '%0.s─' $(seq 1 58))${RESET}"
# Build content into a temp file (avoids mapfile/bash-ism issues)
CONTENT=$(mktemp)
trap 'rm -f "$CONTENT"' EXIT
{
echo "${BOLD}${MAGENTA} tmux cheatsheet${RESET} ${DIM}prefix = Ctrl+Space${RESET}"
echo "$SEPARATOR"
sep
header "Sessions"
key "prefix s" "list sessions"
key "prefix d" "detach"
key "prefix \$" "rename session"
key ":new -s name" "new session"
key ":kill-session -t X" "delete session X"
key "x (in prefix s)" "delete hovered session"
sep
header "Windows"
key "prefix c" "new window"
key "prefix ," "rename window"
key "prefix n / p" "next / previous window"
key "prefix 1-9" "go to window #"
key "prefix &" "close window"
key "prefix w" "window preview"
sep
header "Panes"
key "prefix %" "split vertical"
key "prefix \"" "split horizontal"
key "prefix o" "open opencode pane"
key "prefix arrow" "move between panes"
key "prefix z" "toggle zoom"
key "prefix x" "close pane"
key "prefix !" "pane → window"
key "prefix { / }" "swap pane left / right"
key "prefix space" "cycle layouts"
sep
header "Copy Mode (vi)"
key "prefix [" "enter copy mode"
key "v" "begin selection"
key "y" "yank selection"
key "prefix ]" "paste"
key "/" "search forward"
key "?" "search backward"
sep
header "Multi-Monitor"
key "prefix g" "group session (new view)"
key "prefix G" "leave grouped session"
sep
header "Remote Access"
key "prefix Ctrl+R" "toggle remote mode"
sep
header "Misc"
key "prefix ?" "this cheatsheet"
key "prefix n" "notification panel"
key "prefix N" "jump to last notification"
key "prefix :" "command prompt"
key "prefix t" "show clock"
key "prefix ~" "show messages"
} > "$CONTENT"
TOTAL=$(wc -l < "$CONTENT")
# Scrollable viewer
TOP=1
draw() {
local height visible
height=$(tput lines)
visible=$((height - 1))
clear
sed -n "${TOP},$((TOP + visible - 1))p" "$CONTENT"
# Footer
tput cup $((height - 1)) 0
printf '%s j/k scroll esc/q close%s' "${DIM}" "${RESET}"
}
draw
while true; do
read -rsn1 key
# Handle escape sequences (esc key sends \e, arrow keys send \e[A etc.)
if [[ "$key" == $'\e' ]]; then
# Read any remaining bytes of escape sequence (non-blocking)
read -rsn2 -t 0.01 extra || true
# If no extra bytes, it was a bare Esc press
if [[ -z "$extra" ]]; then
exit 0
fi
# Otherwise ignore the escape sequence (arrow keys etc.)
continue
fi
case "$key" in
j)
height=$(tput lines)
visible=$((height - 1))
if ((TOP + visible <= TOTAL)); then
TOP=$((TOP + 1))
draw
fi
;;
k)
if ((TOP > 1)); then
TOP=$((TOP - 1))
draw
fi
;;
q) exit 0 ;;
esac
done
# status
set -g status "on"
set -g status-bg $color_bg_2
set -g status-justify "left"
set -g status-left-length "100"
set -g status-right-length "100"
# messages
set -g message-style fg=$color_cyan,bg=$color_tx_1,align="centre"
set -g message-command-style fg=$color_cyan,bg=$color_ui_3,align="centre"
# panes
set -g pane-border-style fg=$color_ui_3
set -g pane-active-border-style fg=$color_blue
# windows
setw -g window-status-activity-style fg=$color_tx_1,bg=$color_bg_1,none
setw -g window-status-separator ""
setw -g window-status-style fg=$color_tx_1,bg=$color_bg_1,none
# statusline
set -g status-left "#{?client_prefix,#[fg=#$color_bg_2#,bg=#$color_orange],#[fg=#$color_orange#,bg=#$color_bg_2]} λ #S "
# status-right is set in tmux.nix extraConfig (after all plugins load)
# window-status
setw -g window-status-format "#[bg=#$color_bg_2,fg=#$color_tx_3] #I #W "
setw -g window-status-current-format "#[bg=#$color_bg_1,fg=#$color_tx_1] #I #W "
# Modes
setw -g clock-mode-colour $color_blue
setw -g mode-style fg=$color_magenta,bg=$color_tx_1,bold
{ pkgs }:
pkgs.tmuxPlugins.mkTmuxPlugin {
pluginName = "flexoki-tmux";
version = "2.0.0";
rtpFilePath = "flexoki.tmux";
preInstall = ''
cd tmux
cp ${./flexoki-bar.conf} flexoki-bar.conf
cp ${./flexoki.tmux} flexoki.tmux
chmod +x flexoki.tmux
'';
src = pkgs.fetchFromGitHub {
owner = "kepano";
repo = "flexoki";
rev = "8d723bac4a9ac46adfdf99d42155286977aac72a";
sha256 = "sha256-IxnvoZ9hGEvwq/PBbHTL5L2a2kxMSXSINIfd5Dg9ttA=";
};
}
#!/usr/bin/env bash
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
get_tmux_option() {
local option="$1"
local default_value="$2"
local option_value
option_value="$(tmux show-option -gqv "$option")"
if [[ -z "$option_value" ]]; then
echo "$default_value"
else
echo "$option_value"
fi
}
main() {
local theme
theme="$(get_tmux_option "@flexoki-theme" "")"
if [[ -z "$theme" ]]; then
theme="light"
fi
tmux source-file "$CURRENT_DIR/flexoki-${theme}.tmuxtheme"
tmux source-file "$CURRENT_DIR/flexoki-bar.conf"
}
main
#!/usr/bin/env bash
# Print the nearest git repo root for a given directory.
# Falls back to the directory itself if not inside a git repo.
dir="${1:-.}"
cd "$dir" && git rev-parse --show-toplevel 2>/dev/null || echo "$dir"
#!/usr/bin/env bash
# Flexoki light theme colors
BG="#f2f0e5"
FG="#100f0f"
FG_MUTED="#6f6e69"
RED="#d14d41"
GREEN="#879a39"
YELLOW="#d0a215"
MAGENTA="#8b7ec8"
RESET="#[fg=${FG},bg=${BG},nobold,noitalics,nounderscore,nodim]"
cd "$1" || exit 1
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
[[ -z "$BRANCH" ]] && exit 0
STATUS=$(git status --porcelain 2>/dev/null | grep -cE "^(M| M)")
SYNC_MODE=0
if [[ ${#BRANCH} -gt 25 ]]; then
BRANCH="${BRANCH:0:25}…"
fi
STATUS_CHANGED=""
STATUS_INSERTIONS=""
STATUS_DELETIONS=""
STATUS_UNTRACKED=""
if [[ $STATUS -ne 0 ]]; then
read -r CHANGED_COUNT INSERTIONS_COUNT DELETIONS_COUNT < <(
git diff --numstat 2>/dev/null | awk 'NF==3 {changed+=1; ins+=$1; del+=$2} END {printf("%d %d %d", changed, ins, del)}'
)
SYNC_MODE=1
fi
UNTRACKED_COUNT="$(git ls-files --other --directory --exclude-standard 2>/dev/null | wc -l | tr -d ' ')"
if [[ ${CHANGED_COUNT:-0} -gt 0 ]]; then
STATUS_CHANGED="${RESET}#[fg=${YELLOW},bg=${BG},bold] ${CHANGED_COUNT} "
fi
if [[ ${INSERTIONS_COUNT:-0} -gt 0 ]]; then
STATUS_INSERTIONS="${RESET}#[fg=${GREEN},bg=${BG},bold] ${INSERTIONS_COUNT} "
fi
if [[ ${DELETIONS_COUNT:-0} -gt 0 ]]; then
STATUS_DELETIONS="${RESET}#[fg=${RED},bg=${BG},bold] ${DELETIONS_COUNT} "
fi
if [[ ${UNTRACKED_COUNT:-0} -gt 0 ]]; then
STATUS_UNTRACKED="${RESET}#[fg=${FG_MUTED},bg=${BG},bold] ${UNTRACKED_COUNT} "
fi
# Determine repository sync status
if [[ $SYNC_MODE -eq 0 ]]; then
# shellcheck disable=SC1083
NEED_PUSH=$(git log @{push}.. 2>/dev/null | wc -l | tr -d ' ')
if [[ ${NEED_PUSH:-0} -gt 0 ]]; then
SYNC_MODE=2
elif [[ -f .git/FETCH_HEAD ]]; then
LAST_FETCH=$(stat -c %Y .git/FETCH_HEAD 2>/dev/null || stat -f %m .git/FETCH_HEAD 2>/dev/null || echo 0)
NOW=$(date +%s)
if [[ $((NOW - LAST_FETCH)) -gt 300 ]]; then
git fetch --atomic origin --negotiation-tip=HEAD 2>/dev/null
fi
REMOTE_DIFF="$(git diff --numstat "${BRANCH}" "origin/${BRANCH}" 2>/dev/null)"
if [[ -n $REMOTE_DIFF ]]; then
SYNC_MODE=3
fi
fi
fi
# Set the status indicator based on the sync mode
case "$SYNC_MODE" in
1)
REMOTE_STATUS="$RESET#[bg=${BG},fg=${RED},bold] 󱓎"
;;
2)
REMOTE_STATUS="$RESET#[bg=${BG},fg=${RED},bold] 󰛃"
;;
3)
REMOTE_STATUS="$RESET#[bg=${BG},fg=${MAGENTA},bold] 󰛀"
;;
*)
REMOTE_STATUS="$RESET#[bg=${BG},fg=${GREEN},bold] "
;;
esac
echo "$REMOTE_STATUS $RESET$BRANCH $STATUS_CHANGED$STATUS_INSERTIONS$STATUS_DELETIONS$STATUS_UNTRACKED"
#!/usr/bin/env bash
# tmux-notify-panel: Floating popup that displays notifications
# Renders a scrollable list with keybindings to dismiss or jump to source.
#
# Keybindings:
# j/k or arrows Navigate up/down
# Enter Jump to the tmux window that triggered the notification
# d Dismiss selected notification
# D Dismiss all notifications
# q/Escape Close panel
set -eu
NOTIFY_FILE="${TMUX_NOTIFY_FILE:-/tmp/tmux-notifications.json}"
# Terminal styling
BOLD=$(tput bold)
DIM=$(tput dim)
RESET=$(tput sgr0)
CYAN=$(tput setaf 6)
YELLOW=$(tput setaf 3)
MAGENTA=$(tput setaf 5)
REVERSE=$(tput rev)
# State
SELECTED=0
SCROLL_OFFSET=0
get_notifications() {
if [[ ! -f "$NOTIFY_FILE" ]]; then
echo '[]'
return
fi
# Return newest first
jq 'sort_by(.timestamp) | reverse' "$NOTIFY_FILE" 2>/dev/null || echo '[]'
}
render() {
local notifications="$1"
local count
count=$(echo "$notifications" | jq 'length')
local term_height
term_height=$(tput lines)
local max_items=$((term_height - 5)) # Reserve lines for header/footer
clear
# Header
echo "${BOLD}${MAGENTA} Notifications${RESET} ${DIM}(${count})${RESET}"
echo "${DIM}$(printf '%.0s─' $(seq 1 56))${RESET}"
if [[ "$count" -eq 0 ]]; then
echo ""
echo " ${DIM}No notifications${RESET}"
echo ""
echo "${DIM}$(printf '%.0s─' $(seq 1 56))${RESET}"
echo " ${DIM}press q to close${RESET}"
return
fi
# Ensure selected is within bounds
if [[ $SELECTED -ge $count ]]; then
SELECTED=$((count - 1))
fi
if [[ $SELECTED -lt 0 ]]; then
SELECTED=0
fi
# Adjust scroll offset to keep selected visible
if [[ $SELECTED -lt $SCROLL_OFFSET ]]; then
SCROLL_OFFSET=$SELECTED
fi
if [[ $SELECTED -ge $((SCROLL_OFFSET + max_items)) ]]; then
SCROLL_OFFSET=$((SELECTED - max_items + 1))
fi
local i
for ((i = SCROLL_OFFSET; i < count && i < SCROLL_OFFSET + max_items; i++)); do
local entry
entry=$(echo "$notifications" | jq -r ".[$i]")
local session
session=$(echo "$entry" | jq -r '.session')
local message
message=$(echo "$entry" | jq -r '.message')
local timestamp
timestamp=$(echo "$entry" | jq -r '.timestamp')
# Format timestamp to just time
local time_display
time_display=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$timestamp" "+%H:%M" 2>/dev/null || echo "${timestamp:11:5}")
# Truncate message to fit
local max_msg_len=32
if [[ ${#message} -gt $max_msg_len ]]; then
message="${message:0:$max_msg_len}…"
fi
local prefix=" "
local style=""
if [[ $i -eq $SELECTED ]]; then
style="${REVERSE}"
prefix="▸"
fi
printf " %s ${CYAN}●${RESET} ${style}${YELLOW}%-8s${RESET}${style} %s ${DIM}%s${RESET}\n" \
"$prefix" "$session" "$message" "$time_display"
done
# Scroll indicator
if [[ $count -gt $max_items ]]; then
local visible_end=$((SCROLL_OFFSET + max_items))
if [[ $visible_end -gt $count ]]; then
visible_end=$count
fi
echo "${DIM} [$((SCROLL_OFFSET + 1))-${visible_end} of ${count}]${RESET}"
fi
echo "${DIM}$(printf '%.0s─' $(seq 1 56))${RESET}"
echo " ${DIM}j/k${RESET} navigate ${DIM}Enter${RESET} goto ${DIM}d${RESET} dismiss ${DIM}D${RESET} all ${DIM}q${RESET} close"
}
dismiss_selected() {
local notifications="$1"
local count
count=$(echo "$notifications" | jq 'length')
if [[ $count -eq 0 ]]; then
return
fi
local id
id=$(echo "$notifications" | jq -r ".[$SELECTED].id")
if [[ -n "$id" && "$id" != "null" ]]; then
tmux-notify dismiss "$id"
fi
}
dismiss_all() {
tmux-notify dismiss all
SELECTED=0
SCROLL_OFFSET=0
}
# Jump to the tmux window that triggered the selected notification
goto_selected() {
local notifications="$1"
local count
count=$(echo "$notifications" | jq 'length')
if [[ $count -eq 0 ]]; then
return
fi
local target
target=$(echo "$notifications" | jq -r ".[$SELECTED].target // empty")
if [[ -n "$target" ]]; then
# Dismiss the notification and switch to the target window
dismiss_selected "$notifications"
# select-window works across sessions (target is "session:window")
tmux select-window -t "$target" 2>/dev/null || true
tmux switch-client -t "$target" 2>/dev/null || true
exit 0
fi
}
# Hide cursor
tput civis 2>/dev/null || true
trap 'tput cnorm 2>/dev/null || true' EXIT
# Main loop
while true; do
notifications=$(get_notifications)
render "$notifications"
# Read a single key
IFS= read -rsn1 key
case "$key" in
j)
SELECTED=$((SELECTED + 1))
;;
k)
SELECTED=$((SELECTED - 1))
;;
'')
goto_selected "$notifications"
;;
d)
dismiss_selected "$notifications"
;;
D)
dismiss_all
;;
q)
exit 0
;;
$'\e')
# Handle escape sequences (arrow keys send \e[A, \e[B, etc.)
read -rsn1 -t 0.1 next_key || true
if [[ "${next_key:-}" == "[" ]]; then
read -rsn1 -t 0.1 arrow_key || true
case "${arrow_key:-}" in
A) SELECTED=$((SELECTED - 1)) ;;
B) SELECTED=$((SELECTED + 1)) ;;
esac
else
# Plain escape key - close panel
exit 0
fi
;;
esac
done
#!/usr/bin/env bash
# tmux-notify-widget: Status bar widget showing notification count
# Displays a bell icon with count when there are pending notifications.
# Shows nothing when empty (clean status bar).
NOTIFY_FILE="${TMUX_NOTIFY_FILE:-/tmp/tmux-notifications.json}"
# Flexoki light theme colors
BG="#f2f0e5"
FG="#100f0f"
ORANGE="#da702c"
RESET="#[fg=${FG},bg=${BG},nobold,noitalics,nounderscore,nodim]"
if [[ ! -f "$NOTIFY_FILE" ]]; then
exit 0
fi
COUNT=$(jq 'length' "$NOTIFY_FILE" 2>/dev/null || echo 0)
if [[ "$COUNT" -gt 0 ]]; then
echo "#[fg=${ORANGE},bg=${BG},bold] 󰂞 ${COUNT} ${RESET}"
fi
#!/usr/bin/env bash
# tmux-notify: Notification state manager for tmux
# Stores notifications in a JSON file and provides subcommands to manage them.
#
# Usage:
# tmux-notify add [--event <type>] <message> Add a notification
# tmux-notify dismiss <id|all> Remove notification(s)
# tmux-notify list Output all notifications as JSON
# tmux-notify count Output notification count
# tmux-notify open Open the notification panel popup
#
# Events:
# Only "complete", "permission", "error", "question" events are recorded. Subagent and user_cancelled events are ignored.
#
# Environment:
# TMUX_NOTIFY_FILE Override notification file path (default: /tmp/tmux-notifications.json)
set -eu
NOTIFY_FILE="${TMUX_NOTIFY_FILE:-/tmp/tmux-notifications.json}"
LOCK_FILE="${NOTIFY_FILE}.lock"
# Ensure the notification file exists
init_file() {
if [[ ! -f "$NOTIFY_FILE" ]]; then
echo '[]' > "$NOTIFY_FILE"
fi
}
# Simple file locking using mkdir (atomic on all platforms)
lock() {
local attempts=0
while ! mkdir "$LOCK_FILE" 2>/dev/null; do
attempts=$((attempts + 1))
if [[ $attempts -gt 50 ]]; then
# Stale lock, remove and retry
rm -rf "$LOCK_FILE"
fi
sleep 0.01
done
}
unlock() {
rm -rf "$LOCK_FILE"
}
# Add a notification
cmd_add() {
local event=""
local message=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--event)
event="${2:-}"
shift 2
;;
*)
message="$1"
shift
;;
esac
done
if [[ -z "$message" ]]; then
echo "Usage: tmux-notify add [--event <type>] <message>" >&2
exit 1
fi
# Filter: only allow task completion events through
# Known events: complete, subagent_complete, error, permission, question, user_cancelled
if [[ -n "$event" ]]; then
case "$event" in
complete) ;; # allow
permission) ;; # allow
error) ;; # allow
question) ;; # allow
*)
# Silently ignore subagent and user_cancelled events
exit 0
;;
esac
fi
local id timestamp session window target
id="$(printf '%s%s' "$(date +%s)" "$$" | shasum | head -c 8)"
timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Try to detect the session and window from tmux
# Use $TMUX_PANE with -t flag for reliable detection from child processes
session="${TMUX_NOTIFY_SESSION:-}"
window=""
target=""
if command -v tmux &>/dev/null; then
local pane_target="${TMUX_PANE:-}"
# Skip notification if the pane is in the currently active window
# of an attached session (i.e., the user is actually looking at it)
if [[ -n "$pane_target" ]]; then
local pane_active session_attached
pane_active="$(tmux display-message -p -t "$pane_target" '#{window_active}' 2>/dev/null || true)"
session_attached="$(tmux display-message -p -t "$pane_target" '#{session_attached}' 2>/dev/null || true)"
if [[ "$pane_active" == "1" && "${session_attached:-0}" -gt 0 ]]; then
exit 0
fi
fi
if [[ -n "$pane_target" ]]; then
if [[ -z "$session" ]]; then
session="$(tmux display-message -p -t "$pane_target" '#S' 2>/dev/null || true)"
fi
window="$(tmux display-message -p -t "$pane_target" '#I' 2>/dev/null || true)"
else
if [[ -z "$session" ]]; then
session="$(tmux display-message -p '#S' 2>/dev/null || true)"
fi
window="$(tmux display-message -p '#I' 2>/dev/null || true)"
fi
if [[ -n "$session" && -n "$window" ]]; then
target="${session}:${window}"
fi
fi
session="${session:-opencode}"
init_file
lock
local entry
entry=$(jq -n \
--arg id "$id" \
--arg ts "$timestamp" \
--arg sess "$session" \
--arg msg "$message" \
--arg tgt "$target" \
'{id: $id, timestamp: $ts, session: $sess, message: $msg, target: $tgt}')
jq --argjson entry "$entry" '. += [$entry]' "$NOTIFY_FILE" > "${NOTIFY_FILE}.tmp" \
&& mv "${NOTIFY_FILE}.tmp" "$NOTIFY_FILE"
unlock
# Flash a styled message in the tmux status bar and refresh the widget
if command -v tmux &>/dev/null; then
tmux refresh-client -S 2>/dev/null || true
# Temporarily set a clean message style matching the flexoki light theme
tmux set -g message-style "fg=#da702c,bg=#f2f0e5" 2>/dev/null || true
tmux display-message -d 5000 "󰂞 ${session}: ${message}" 2>/dev/null || true
fi
# Send terminal bell when remote mode is active.
# Termius (and most iOS SSH clients) surface BEL as an iOS notification
# when the app is backgrounded, providing a vibration/alert on the phone.
if [[ -f "/tmp/tmux-remote-state" ]]; then
# Send BEL to all tmux clients so the SSH session receives it
local pane_target="${TMUX_PANE:-}"
if [[ -n "$pane_target" ]]; then
tmux send-keys -t "$pane_target" "" 2>/dev/null || true
else
printf '\a'
fi
fi
}
# Dismiss (remove) one or all notifications
cmd_dismiss() {
local target="${1:-}"
if [[ -z "$target" ]]; then
echo "Usage: tmux-notify dismiss <id|all>" >&2
exit 1
fi
init_file
lock
if [[ "$target" == "all" ]]; then
echo '[]' > "$NOTIFY_FILE"
else
jq --arg id "$target" '[.[] | select(.id != $id)]' "$NOTIFY_FILE" > "${NOTIFY_FILE}.tmp" \
&& mv "${NOTIFY_FILE}.tmp" "$NOTIFY_FILE"
fi
unlock
}
# List all notifications as JSON
cmd_list() {
init_file
cat "$NOTIFY_FILE"
}
# Count notifications
cmd_count() {
init_file
jq 'length' "$NOTIFY_FILE"
}
# Open the notification panel popup
cmd_open() {
if ! command -v tmux &>/dev/null; then
echo "tmux is not available" >&2
exit 1
fi
local panel_script
panel_script="$(command -v tmux-notify-panel)"
tmux display-popup -w 60 -h 20 -E "$panel_script"
}
# Jump to the most recent notification's window and dismiss it
cmd_goto() {
init_file
local count
count=$(jq 'length' "$NOTIFY_FILE")
if [[ "$count" -eq 0 ]]; then
tmux display-message "No notifications" 2>/dev/null || true
return
fi
# Get the newest notification (last in the array)
local target id
target=$(jq -r '.[-1].target // empty' "$NOTIFY_FILE")
id=$(jq -r '.[-1].id' "$NOTIFY_FILE")
# Dismiss it
if [[ -n "$id" && "$id" != "null" ]]; then
cmd_dismiss "$id"
fi
# Switch to the target window and refresh status bar
if command -v tmux &>/dev/null; then
tmux refresh-client -S 2>/dev/null || true
if [[ -n "$target" ]]; then
tmux select-window -t "$target" 2>/dev/null || true
tmux switch-client -t "$target" 2>/dev/null || true
fi
fi
}
# Main dispatch
case "${1:-}" in
add) shift; cmd_add "$@" ;;
dismiss) shift; cmd_dismiss "$@" ;;
goto) cmd_goto ;;
list) cmd_list ;;
count) cmd_count ;;
open) cmd_open ;;
*)
echo "Usage: tmux-notify <add|dismiss|goto|list|count|open> [args...]" >&2
exit 1
;;
esac
{
username,
pkgs,
lib,
...
}:
let
flexoki-neovim = pkgs.vimUtils.buildVimPlugin {
pname = "flexoki-neovim";
version = "2025-08-26";
src = pkgs.fetchFromGitHub {
owner = "kepano";
repo = "flexoki-neovim";
rev = "c3e2251e813d29d885a7cbbe9808a7af234d845d";
sha256 = "0j6r1rm9g6mm5b5x2wddwyhh6wjagk0x9babs73ky081sgvlyl2f";
};
};
nvim-treesitter = pkgs.vimPlugins.nvim-treesitter.withPlugins (
treesitter-plugins: with treesitter-plugins; [
bash
css
dart
elixir
go
gomod
gosum
html
javascript
json
lua
markdown
markdown_inline
nix
python
rust
toml
tsx
typescript
yaml
]
);
in
{
home-manager.users.${username} = {
# LSP servers and dev tools installed as Nix packages (no Mason needed)
home.packages = with pkgs; [
# Language servers
lua-language-server
nil # Nix LSP
typescript-language-server
vscode-langservers-extracted # HTML, CSS, JSON, ESLint
pyright
gopls
rust-analyzer
dart
elixir-ls
tailwindcss-language-server
nodePackages.bash-language-server
yaml-language-server
taplo # TOML LSP
marksman # Markdown LSP
# Formatters & linters
stylua
nixfmt-rfc-style
prettierd
black
gofumpt
rustfmt
];
programs.neovim = {
enable = true;
defaultEditor = true;
viAlias = true;
vimAlias = true;
vimdiffAlias = true;
initLua = ''
-- ╔══════════════════════════════════════════╗
-- ║ CORE OPTIONS ║
-- ╚══════════════════════════════════════════╝
vim.g.mapleader = ' '
vim.g.maplocalleader = ' '
vim.o.swapfile = false
vim.o.hlsearch = false
vim.wo.number = true
vim.wo.relativenumber = true
vim.o.mouse = 'a'
vim.o.clipboard = 'unnamedplus'
vim.o.breakindent = true
vim.o.undofile = true
vim.o.ignorecase = true
vim.o.smartcase = true
vim.wo.signcolumn = 'yes'
vim.o.updatetime = 250
vim.o.timeoutlen = 300
vim.o.completeopt = 'menuone,noselect'
vim.o.termguicolors = true
vim.o.scrolloff = 8
vim.o.sidescrolloff = 8
vim.o.cursorline = true
vim.o.splitbelow = true
vim.o.splitright = true
vim.o.wrap = false
vim.o.tabstop = 2
vim.o.shiftwidth = 2
vim.o.expandtab = true
vim.o.showmode = false -- lualine shows the mode
vim.keymap.set({ 'n', 'v' }, '<Space>', '<Nop>', { silent = true })
-- Remap for dealing with word wrap
vim.keymap.set('n', 'k', "v:count == 0 ? 'gk' : 'k'", { expr = true, silent = true })
vim.keymap.set('n', 'j', "v:count == 0 ? 'gj' : 'j'", { expr = true, silent = true })
-- ╔══════════════════════════════════════════╗
-- ║ VS CODE-LIKE KEYBINDINGS ║
-- ╚══════════════════════════════════════════╝
-- Move lines up/down (like Alt+Up/Down in VS Code)
vim.keymap.set('n', '<A-j>', ':m .+1<CR>==', { desc = 'Move line down', silent = true })
vim.keymap.set('n', '<A-k>', ':m .-2<CR>==', { desc = 'Move line up', silent = true })
vim.keymap.set('v', '<A-j>', ":m '>+1<CR>gv=gv", { desc = 'Move selection down', silent = true })
vim.keymap.set('v', '<A-k>', ":m '<-2<CR>gv=gv", { desc = 'Move selection up', silent = true })
-- Indent/dedent in visual mode (stay in visual)
vim.keymap.set('v', '<', '<gv', { desc = 'Dedent and reselect' })
vim.keymap.set('v', '>', '>gv', { desc = 'Indent and reselect' })
-- Window navigation (Ctrl+hjkl like VS Code pane switching)
vim.keymap.set('n', '<C-h>', '<C-w>h', { desc = 'Move to left window' })
vim.keymap.set('n', '<C-j>', '<C-w>j', { desc = 'Move to below window' })
vim.keymap.set('n', '<C-k>', '<C-w>k', { desc = 'Move to above window' })
vim.keymap.set('n', '<C-l>', '<C-w>l', { desc = 'Move to right window' })
-- Resize windows with Ctrl+arrows
vim.keymap.set('n', '<C-Up>', ':resize +2<CR>', { desc = 'Increase height', silent = true })
vim.keymap.set('n', '<C-Down>', ':resize -2<CR>', { desc = 'Decrease height', silent = true })
vim.keymap.set('n', '<C-Left>', ':vertical resize -2<CR>', { desc = 'Decrease width', silent = true })
vim.keymap.set('n', '<C-Right>', ':vertical resize +2<CR>', { desc = 'Increase width', silent = true })
-- Buffer navigation
vim.keymap.set('n', '<S-h>', ':bprevious<CR>', { desc = 'Previous buffer', silent = true })
vim.keymap.set('n', '<S-l>', ':bnext<CR>', { desc = 'Next buffer', silent = true })
-- Better paste (don't replace register content)
vim.keymap.set('v', 'p', '"_dP', { desc = 'Paste without yanking replaced text' })
-- Clear search with Escape
vim.keymap.set('n', '<Esc>', ':noh<CR>', { desc = 'Clear search highlight', silent = true })
-- ╔══════════════════════════════════════════╗
-- ║ WORD / LINE / FILE NAVIGATION ║
-- ╚══════════════════════════════════════════╝
-- Opt+Left/Right → move by word (like Option+arrows in VS Code)
vim.keymap.set({'n', 'v'}, '<A-Left>', 'b', { desc = 'Word back' })
vim.keymap.set({'n', 'v'}, '<A-Right>', 'w', { desc = 'Word forward' })
vim.keymap.set('i', '<A-Left>', '<C-o>b', { desc = 'Word back' })
vim.keymap.set('i', '<A-Right>', '<C-o>w', { desc = 'Word forward' })
-- Cmd+Left/Right → start/end of line (arrives as Home/End in terminal)
vim.keymap.set({'n', 'v'}, '<Home>', '^', { desc = 'Start of line' })
vim.keymap.set({'n', 'v'}, '<End>', '$', { desc = 'End of line' })
vim.keymap.set('i', '<Home>', '<C-o>^', { desc = 'Start of line' })
vim.keymap.set('i', '<End>', '<C-o>$', { desc = 'End of line' })
-- Cmd+Up/Down → start/end of file (arrives as CSI u via extended-keys)
vim.keymap.set({'n', 'v'}, '<C-Home>', 'gg', { desc = 'Start of file' })
vim.keymap.set({'n', 'v'}, '<C-End>', 'G', { desc = 'End of file' })
-- Opt+Backspace → delete word back (like Option+Backspace in VS Code)
vim.keymap.set('i', '<A-BS>', '<C-w>', { desc = 'Delete word back' })
-- ╔══════════════════════════════════════════╗
-- ║ QUICK ACCESS (VS Code muscle memory) ║
-- ╚══════════════════════════════════════════╝
-- Cmd+P → fuzzy find files in project (Ghostty sends CSI 80;6u)
vim.keymap.set('n', '<C-S-p>', '<cmd>Telescope find_files<cr>', { desc = 'Find file in project' })
-- Also bind Alt+p as fallback
vim.keymap.set('n', '<A-p>', '<cmd>Telescope find_files<cr>', { desc = 'Find file in project' })
-- Cmd+Shift+F → search text across project (Ghostty sends CSI 70;6u)
vim.keymap.set('n', '<C-S-f>', function() require('telescope.builtin').live_grep() end, { desc = 'Search in project' })
-- Also bind Alt+Shift+F as fallback
vim.keymap.set('n', '<A-F>', function() require('telescope.builtin').live_grep() end, { desc = 'Search in project' })
-- ╔══════════════════════════════════════════╗
-- ║ CHEATSHEET (floating popup) ║
-- ╚══════════════════════════════════════════╝
local function show_cheatsheet()
local s = ' '
local lines = {
' nvim cheatsheet leader = Space',
string.rep('─', 54),
s,
' Quick Access',
' Cmd+p find file in project',
' Cmd+Shift+f search text in project',
' Space Space switch buffer',
' Space / search in current buffer',
' Space g live grep',
s,
' Navigation (arrows) (native)',
' Opt+Left/Right b / w word',
' Cmd+Left/Right ^ / $ line',
' Cmd+Up/Down gg / G file',
' Opt+Backspace (insert) del word',
' Alt+j / Alt+k move line up/down',
s,
' Files',
' Space ff find file',
' Space fr recent files',
' Space fn new file',
' Space fs save file',
s,
' Code (LSP)',
' Space cd go to definition',
' Space cR go to references',
' Space ci go to implementation',
' Space cr rename symbol',
' Space ca code action',
' Space cf format file',
' Space ct type definition',
' Space csd document symbols',
' Space csw workspace symbols',
s,
' Diagnostics',
' Space xd line diagnostics',
' Space xl all diagnostics',
' Space xn / xp next / prev diagnostic',
s,
' Buffers & Tabs',
' Shift+h / Shift+l prev / next buffer',
' Space bd delete buffer',
' Space ett new tab',
' Space etc close tab',
s,
' Splits & Windows',
' Space sv vertical split',
' Space sh horizontal split',
' Ctrl+h/j/k/l move between windows',
' Ctrl+arrows resize windows',
s,
' Git (Source Control)',
' Space Gg open neogit (stage/commit)',
' Space Gc commit',
' Space Gp push',
' Space Gl pull',
' Space Gd diff view (all changes)',
' Space Gf current file history',
' Space GL repo log',
' Space Gq close diff view',
' Space Gb branches',
' ]h / [h next / prev hunk',
' Space Ghs stage hunk',
' Space Ghr reset hunk',
' Space Ghp preview hunk',
' Space Ghb blame line',
s,
' Editing',
' Alt+j / Alt+k move line down / up',
' < / > (visual) indent and reselect',
' gcc toggle comment',
' sa" / sd" / sr" surround add/del/replace',
s,
' AI',
' Ctrl+y accept supermaven suggestion',
' Ctrl+] clear suggestion',
' Ctrl+j accept word',
' Ctrl+. toggle opencode',
' Ctrl+a ask opencode',
' Ctrl+x opencode actions',
' go / goo send range/line to opencode',
s,
' File Explorer',
' Space oo open Oil',
' - (in Oil) go up directory',
s,
' General',
' Space hk search keymaps',
' Space ? this cheatsheet',
' Space qq force quit all',
s,
' press q to close',
}
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].modifiable = false
vim.bo[buf].bufhidden = 'wipe'
local width = 54
local height = #lines
local win = vim.api.nvim_open_win(buf, true, {
relative = 'editor',
width = width,
height = height,
col = math.floor((vim.o.columns - width) / 2),
row = math.floor((vim.o.lines - height) / 2),
style = 'minimal',
border = 'rounded',
title = ' cheatsheet ',
title_pos = 'center',
})
-- Highlight the header line
vim.api.nvim_buf_add_highlight(buf, -1, 'Title', 0, 0, -1)
-- Highlight section headers and dim separator/footer
for i, line in ipairs(lines) do
if line:match('^ [A-Z]') and not line:match('^ +[A-Z][a-z]+[+-]') then
vim.api.nvim_buf_add_highlight(buf, -1, 'Function', i - 1, 0, -1)
end
if line:match('^─') or line:match('press q') then
vim.api.nvim_buf_add_highlight(buf, -1, 'Comment', i - 1, 0, -1)
end
end
-- Close on q or Esc
local close = function() pcall(vim.api.nvim_win_close, win, true) end
vim.keymap.set('n', 'q', close, { buffer = buf, silent = true })
vim.keymap.set('n', '<Esc>', close, { buffer = buf, silent = true })
end
vim.keymap.set('n', '<leader>?', show_cheatsheet, { desc = 'Cheatsheet' })
-- Highlight on yank
local highlight_group = vim.api.nvim_create_augroup('YankHighlight', { clear = true })
vim.api.nvim_create_autocmd('TextYankPost', {
callback = function()
vim.highlight.on_yank()
end,
group = highlight_group,
pattern = '*',
})
-- ╔══════════════════════════════════════════╗
-- ║ LSP (Neovim 0.11+ native API) ║
-- ╚══════════════════════════════════════════╝
-- Shared capabilities (enhanced by cmp-nvim-lsp, loaded later)
-- We defer the actual capability merging to LspAttach
vim.api.nvim_create_autocmd('LspAttach', {
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
if not client then return end
local bufnr = args.buf
-- Enable inlay hints
if client.server_capabilities.inlayHintProvider then
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
end
-- Format on save
if client.server_capabilities.documentFormattingProvider then
vim.api.nvim_create_autocmd('BufWritePre', {
buffer = bufnr,
callback = function()
vim.lsp.buf.format({ bufnr = bufnr })
end,
})
end
end,
})
-- Default config applied to all servers
vim.lsp.config('*', {
capabilities = vim.lsp.protocol.make_client_capabilities(),
})
-- Per-server configuration
vim.lsp.config('lua_ls', {
settings = {
Lua = {
workspace = { checkThirdParty = false },
telemetry = { enable = false },
completion = { callSnippet = 'Replace' },
},
},
})
vim.lsp.config('nil_ls', {
settings = {
['nil'] = {
formatting = { command = { "nixfmt" } },
},
},
})
vim.lsp.config('gopls', {
settings = {
gopls = {
analyses = { unusedparams = true },
staticcheck = true,
gofumpt = true,
},
},
})
vim.lsp.config('rust_analyzer', {
settings = {
['rust-analyzer'] = {
checkOnSave = { command = 'clippy' },
cargo = { allFeatures = true },
},
},
})
vim.lsp.config('elixirls', {
cmd = { 'elixir-ls' },
})
-- Enable all servers (filetypes auto-detected via nvim-lspconfig definitions)
vim.lsp.enable({
'lua_ls',
'nil_ls',
'ts_ls',
'pyright',
'gopls',
'rust_analyzer',
'dartls',
'elixirls',
'tailwindcss',
'html',
'cssls',
'jsonls',
'bashls',
'yamlls',
'taplo',
'marksman',
})
-- Diagnostic UI
vim.diagnostic.config({
virtual_text = { spacing = 4, prefix = '●' },
signs = {
text = {
[vim.diagnostic.severity.ERROR] = ' ',
[vim.diagnostic.severity.WARN] = ' ',
[vim.diagnostic.severity.HINT] = '󰌵 ',
[vim.diagnostic.severity.INFO] = ' ',
},
},
underline = true,
update_in_insert = false,
severity_sort = true,
float = {
border = 'rounded',
source = true,
},
})
'';
plugins = with pkgs; [
# ── Core dependencies ──────────────────────────────
vimPlugins.plenary-nvim
# ── Colorscheme ────────────────────────────────────
{
plugin = flexoki-neovim;
config = ''
lua << EOF
vim.cmd.colorscheme "flexoki-light"
EOF
'';
}
# ── Completion engine (must be before LSP setup) ───
{
plugin = vimPlugins.cmp-nvim-lsp;
config = ''
lua << EOF
-- Enhance default LSP capabilities with cmp completions
vim.lsp.config('*', {
capabilities = require('cmp_nvim_lsp').default_capabilities(),
})
EOF
'';
}
vimPlugins.cmp-buffer
vimPlugins.cmp-path
vimPlugins.cmp_luasnip
{
plugin = vimPlugins.luasnip;
config = ''
lua << EOF
require('luasnip.loaders.from_vscode').lazy_load()
EOF
'';
}
vimPlugins.friendly-snippets
# ── LSP progress UI ────────────────────────────────
{
plugin = vimPlugins.fidget-nvim;
config = ''
lua << EOF
require('fidget').setup({})
EOF
'';
}
{
plugin = vimPlugins.lazydev-nvim;
config = ''
lua << EOF
require('lazydev').setup({})
EOF
'';
}
# ── Treesitter (grammars managed by Nix) ──────────
{
plugin = nvim-treesitter;
config = ''
lua << EOF
-- Neovim 0.11+: treesitter highlight/indent are built-in
-- Enable for all buffers that have a parser available
vim.api.nvim_create_autocmd('FileType', {
callback = function(args)
pcall(vim.treesitter.start, args.buf)
end,
})
EOF
'';
}
# ── Fuzzy finder ───────────────────────────────────
vimPlugins.telescope-fzf-native-nvim
{
plugin = vimPlugins.telescope-nvim;
config = ''
lua << EOF
local telescope = require('telescope')
local actions = require('telescope.actions')
telescope.setup {
defaults = {
mappings = {
i = {
['<C-u>'] = false,
['<C-d>'] = false,
['<C-j>'] = actions.move_selection_next,
['<C-k>'] = actions.move_selection_previous,
}
},
file_ignore_patterns = { 'node_modules', '.git/', 'target/' },
},
pickers = {
find_files = {
hidden = true,
},
},
}
telescope.load_extension('fzf')
EOF
'';
}
# ── Which-key (v3 API) ─────────────────────────────
{
plugin = vimPlugins.which-key-nvim;
config = ''
lua << EOF
local wk = require('which-key')
wk.setup({
plugins = {
spelling = { enabled = true },
},
})
wk.add({
-- Top-level leader bindings
{ "<leader><space>", "<cmd>Telescope buffers<cr>", desc = "Switch buffer" },
{ "<leader>/", function()
require('telescope.builtin').current_buffer_fuzzy_find(
require('telescope.themes').get_dropdown { winblend = 10, previewer = false }
)
end, desc = "Search in buffer" },
{ "<leader>g", function() require('telescope.builtin').live_grep() end, desc = "Live grep" },
-- File
{ "<leader>f", group = "file" },
{ "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Find file" },
{ "<leader>fr", "<cmd>Telescope oldfiles<cr>", desc = "Recent files" },
{ "<leader>fn", "<cmd>enew<cr>", desc = "New file" },
{ "<leader>fs", "<cmd>w<cr>", desc = "Save file" },
-- Code (LSP)
{ "<leader>c", group = "code" },
{ "<leader>ca", vim.lsp.buf.code_action, desc = "Code action" },
{ "<leader>cr", vim.lsp.buf.rename, desc = "Rename symbol" },
{ "<leader>cf", function() vim.lsp.buf.format({ async = true }) end, desc = "Format file" },
{ "<leader>cd", vim.lsp.buf.definition, desc = "Go to definition" },
{ "<leader>cD", vim.lsp.buf.declaration, desc = "Go to declaration" },
{ "<leader>ci", function() require('telescope.builtin').lsp_implementations() end, desc = "Go to implementation" },
{ "<leader>cR", function() require('telescope.builtin').lsp_references() end, desc = "Go to references" },
{ "<leader>ct", vim.lsp.buf.type_definition, desc = "Type definition" },
{ "<leader>cs", group = "symbols" },
{ "<leader>csd", function() require('telescope.builtin').lsp_document_symbols() end, desc = "Document symbols" },
{ "<leader>csw", function() require('telescope.builtin').lsp_dynamic_workspace_symbols() end, desc = "Workspace symbols" },
-- Editor
{ "<leader>e", group = "editor" },
{ "<leader>et", group = "tabs" },
{ "<leader>ett", "<cmd>tabnew<cr>", desc = "New tab" },
{ "<leader>etc", "<cmd>tabclose<cr>", desc = "Close tab" },
{ "<leader>eto", "<cmd>tabonly<cr>", desc = "Only this tab" },
{ "<leader>etp", "<cmd>tabprevious<cr>", desc = "Previous tab" },
{ "<leader>etn", "<cmd>tabnext<cr>", desc = "Next tab" },
-- Splits (VS Code-like)
{ "<leader>sv", "<cmd>vsplit<cr>", desc = "Vertical split" },
{ "<leader>sh", "<cmd>split<cr>", desc = "Horizontal split" },
{ "<leader>se", "<C-w>=", desc = "Equal size splits" },
{ "<leader>sc", "<cmd>close<cr>", desc = "Close split" },
-- Quit
{ "<leader>q", group = "quit" },
{ "<leader>qq", "<cmd>qa!<CR>", desc = "Force quit all" },
{ "<leader>qa", "<cmd>qa<CR>", desc = "Quit all" },
-- Help
{ "<leader>h", group = "help" },
{ "<leader>hk", "<cmd>Telescope keymaps<CR>", desc = "Keymaps" },
{ "<leader>hh", "<cmd>Telescope help_tags<CR>", desc = "Help tags" },
{ "<leader>hm", "<cmd>Telescope man_pages<CR>", desc = "Man pages" },
-- Open
{ "<leader>o", group = "open" },
{ "<leader>oo", "<cmd>Oil<CR>", desc = "Oil file explorer" },
{ "<leader>ot", "<cmd>Telescope<CR>", desc = "Telescope" },
{ "<leader>oa", function() require("opencode").toggle() end, desc = "Toggle opencode" },
-- Buffers
{ "<leader>b", group = "buffer" },
{ "<leader>bd", "<cmd>bdelete<CR>", desc = "Delete buffer" },
{ "<leader>bn", "<cmd>bnext<CR>", desc = "Next buffer" },
{ "<leader>bp", "<cmd>bprevious<CR>", desc = "Previous buffer" },
-- Diagnostics
{ "<leader>x", group = "diagnostics" },
{ "<leader>xd", function() vim.diagnostic.open_float() end, desc = "Line diagnostics" },
{ "<leader>xl", function() require('telescope.builtin').diagnostics() end, desc = "All diagnostics" },
{ "<leader>xn", function() vim.diagnostic.goto_next() end, desc = "Next diagnostic" },
{ "<leader>xp", function() vim.diagnostic.goto_prev() end, desc = "Previous diagnostic" },
-- Window management
{ "<leader>w", proxy = "<C-w>", group = "window" },
-- Git
{ "<leader>G", group = "git" },
{ "<leader>Gg", "<cmd>Neogit<cr>", desc = "Open Neogit (source control)" },
{ "<leader>Gc", "<cmd>Neogit commit<cr>", desc = "Commit" },
{ "<leader>Gp", "<cmd>Neogit push<cr>", desc = "Push" },
{ "<leader>Gl", "<cmd>Neogit pull<cr>", desc = "Pull" },
{ "<leader>Gd", "<cmd>DiffviewOpen<cr>", desc = "Diff view (all changes)" },
{ "<leader>Gf", "<cmd>DiffviewFileHistory %<cr>", desc = "File history" },
{ "<leader>GL", "<cmd>DiffviewFileHistory<cr>", desc = "Repo log" },
{ "<leader>Gq", "<cmd>DiffviewClose<cr>", desc = "Close diff view" },
{ "<leader>Gb", "<cmd>Telescope git_branches<cr>", desc = "Branches" },
{ "<leader>Gs", "<cmd>Telescope git_status<cr>", desc = "Status (telescope)" },
{ "<leader>Gh", group = "hunks" },
})
EOF
'';
}
# ── LSP setup (Neovim 0.11+ native vim.lsp.config) ──
#
# nvim-lspconfig is still needed for its /lsp/*.lua server
# definition files which register filetypes and default cmd/settings.
# We just don't call require('lspconfig') anymore.
vimPlugins.nvim-lspconfig
# ── Completion (nvim-cmp) ──────────────────────────
{
plugin = vimPlugins.nvim-cmp;
config = ''
lua << EOF
local cmp = require('cmp')
local luasnip = require('luasnip')
cmp.setup {
snippet = {
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
mapping = cmp.mapping.preset.insert {
['<C-n>'] = cmp.mapping.select_next_item(),
['<C-p>'] = cmp.mapping.select_prev_item(),
['<C-d>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete {},
['<CR>'] = cmp.mapping.confirm {
behavior = cmp.ConfirmBehavior.Replace,
select = true,
},
['<Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
elseif luasnip.expand_or_locally_jumpable() then
luasnip.expand_or_jump()
else
fallback()
end
end, { 'i', 's' }),
['<S-Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item()
elseif luasnip.locally_jumpable(-1) then
luasnip.jump(-1)
else
fallback()
end
end, { 'i', 's' }),
},
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'luasnip' },
{ name = 'path' },
}, {
{ name = 'buffer' },
}),
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
}
EOF
'';
}
# ── UI & Navigation ───────────────────────────────
vimPlugins.nvim-web-devicons
{
plugin = vimPlugins.lualine-nvim;
config = ''
lua << EOF
require('lualine').setup({
options = {
theme = 'auto',
component_separators = { left = '│', right = '│' },
section_separators = { left = "", right = "" },
},
sections = {
lualine_a = { 'mode' },
lualine_b = { 'branch', 'diff', 'diagnostics' },
lualine_c = { { 'filename', path = 1 } },
lualine_x = { 'encoding', 'fileformat', 'filetype' },
lualine_y = { 'progress' },
lualine_z = {
'location',
{
function()
local ok, oc = pcall(require, "opencode")
if ok and oc.statusline then return oc.statusline() end
return ""
end,
},
},
},
})
EOF
'';
}
{
plugin = vimPlugins.bufferline-nvim;
config = ''
lua << EOF
require('bufferline').setup({
options = {
diagnostics = 'nvim_lsp',
show_buffer_close_icons = true,
show_close_icon = false,
separator_style = 'thin',
offsets = {
{
filetype = 'oil',
text = 'File Explorer',
text_align = 'center',
},
},
},
})
EOF
'';
}
{
plugin = vimPlugins.indent-blankline-nvim;
config = ''
lua << EOF
require('ibl').setup({
indent = { char = '│' },
scope = { enabled = true },
})
EOF
'';
}
{
plugin = vimPlugins.gitsigns-nvim;
config = ''
lua << EOF
require('gitsigns').setup({
signs = {
add = { text = '│' },
change = { text = '│' },
delete = { text = '_' },
topdelete = { text = '‾' },
changedelete = { text = '~' },
},
on_attach = function(bufnr)
local gs = package.loaded.gitsigns
local function map(mode, l, r, opts)
opts = opts or {}
opts.buffer = bufnr
vim.keymap.set(mode, l, r, opts)
end
-- Navigation
map('n', ']h', gs.next_hunk, { desc = 'Next git hunk' })
map('n', '[h', gs.prev_hunk, { desc = 'Previous git hunk' })
-- Actions
map('n', '<leader>Ghs', gs.stage_hunk, { desc = 'Stage hunk' })
map('n', '<leader>Ghr', gs.reset_hunk, { desc = 'Reset hunk' })
map('n', '<leader>Ghp', gs.preview_hunk, { desc = 'Preview hunk' })
map('n', '<leader>Ghb', function() gs.blame_line({ full = true }) end, { desc = 'Blame line' })
map('n', '<leader>Ghd', gs.diffthis, { desc = 'Diff this' })
end,
})
EOF
'';
}
# ── Git: neogit (source control panel) ─────────────
{
plugin = vimPlugins.diffview-nvim;
config = ''
lua << EOF
require('diffview').setup({
use_icons = true,
})
EOF
'';
}
{
plugin = vimPlugins.neogit;
config = ''
lua << EOF
require('neogit').setup({
integrations = {
telescope = true,
diffview = true,
},
signs = {
hunk = { " ", " " },
item = { "▸", "▾" },
section = { "▸", "▾" },
},
})
EOF
'';
}
# ── File explorer ──────────────────────────────────
{
plugin = vimPlugins.oil-nvim;
config = ''
lua << EOF
require('oil').setup({
default_file_explorer = true,
columns = {
"icon",
"permissions",
"size",
"mtime",
},
delete_to_trash = true,
cleanup_delay_ms = 1000,
use_default_keymaps = true,
view_options = {
show_hidden = true,
},
})
EOF
'';
}
# ── Editing helpers ────────────────────────────────
vimPlugins.nvim-comment
{
plugin = vimPlugins.nvim-autopairs;
config = ''
lua << EOF
require('nvim-autopairs').setup({})
-- Integrate with cmp
local cmp_autopairs = require('nvim-autopairs.completion.cmp')
local cmp = require('cmp')
cmp.event:on('confirm_done', cmp_autopairs.on_confirm_done())
EOF
'';
}
{
plugin = vimPlugins.mini-nvim;
config = ''
lua << EOF
-- Surround: add/change/delete surrounding brackets, quotes, etc.
-- sa" to add quotes, sd" to delete, sr"' to replace " with '
require('mini.surround').setup({})
-- Highlight word under cursor
require('mini.cursorword').setup({})
EOF
'';
}
# ── AI autocomplete (Supermaven) ───────────────────
{
plugin = vimPlugins.supermaven-nvim;
config = ''
lua << EOF
require('supermaven-nvim').setup({
keymaps = {
accept_suggestion = "<C-y>",
clear_suggestion = "<C-]>",
accept_word = "<C-j>",
},
color = {
suggestion_color = "#888888",
},
log_level = "off",
})
EOF
'';
}
# ── opencode.nvim (AI agent integration) ──────────
{
plugin = vimPlugins.opencode-nvim;
config = ''
lua << EOF
vim.o.autoread = true -- Required for opencode edit reloading
---@type opencode.Opts
vim.g.opencode_opts = {}
-- Ask opencode about selection/cursor
vim.keymap.set({ "n", "x" }, "<C-a>", function() require("opencode").ask("@this: ", { submit = true }) end, { desc = "Ask opencode" })
-- Select from opencode actions (prompts, commands)
vim.keymap.set({ "n", "x" }, "<C-x>", function() require("opencode").select() end, { desc = "opencode actions" })
-- Toggle opencode TUI
vim.keymap.set({ "n", "t" }, "<C-.>", function() require("opencode").toggle() end, { desc = "Toggle opencode" })
-- Operator mode: send range to opencode (supports dot-repeat)
vim.keymap.set({ "n", "x" }, "go", function() return require("opencode").operator("@this ") end, { desc = "Send range to opencode", expr = true })
vim.keymap.set("n", "goo", function() return require("opencode").operator("@this ") .. "_" end, { desc = "Send line to opencode", expr = true })
-- Remap increment/decrement since we took <C-a>/<C-x>
vim.keymap.set("n", "+", "<C-a>", { desc = "Increment", noremap = true })
vim.keymap.set("n", "-", "<C-x>", { desc = "Decrement", noremap = true })
EOF
'';
}
# ── Dashboard ──────────────────────────────────────
{
plugin = vimPlugins.alpha-nvim;
config = ''
lua << EOF
local alpha = require('alpha')
local dashboard = require('alpha.themes.dashboard')
dashboard.section.header.val = {
[[ ]],
[[ ▄▄▄ ]],
[[ ███ ▀▀ ▀▀ ▀▀ ]],
[[ ███ ██ ███▄███▄ ██ ██ ██ ██ ███▄███▄ ]],
[[ ███ ██ ██ ██ ██ ██ ██▄██ ██ ██ ██ ██ ]],
[[ ████████ ██▄ ██ ██ ██ ██▄ ▀█▀ ██▄ ██ ██ ██ ]],
[[ ]],
}
dashboard.section.buttons.val = {
dashboard.button("f", " Find file", "<cmd>Telescope find_files<CR>"),
dashboard.button("r", " Recent files", "<cmd>Telescope oldfiles<CR>"),
dashboard.button("g", " Live grep", "<cmd>Telescope live_grep<CR>"),
dashboard.button("e", " New file", "<cmd>enew<CR>"),
dashboard.button("q", " Quit", "<cmd>qa<CR>"),
}
alpha.setup(dashboard.opts)
EOF
'';
}
];
};
};
}
#!/usr/bin/env bash
# Flexoki light theme colors
BG="#f2f0e5"
FG="#100f0f"
BLUE="#4385be"
RESET="#[fg=${FG},bg=${BG},nobold,noitalics,nounderscore,nodim]"
SHOW_PATH=$(tmux show-option -gv @flexoki-tmux_show_path 2>/dev/null)
PATH_FORMAT=$(tmux show-option -gv @flexoki-tmux_path_format 2>/dev/null)
# Enabled by default; set @flexoki-tmux_show_path to "0" to disable
if [ "${SHOW_PATH}" = "0" ]; then
exit 0
fi
current_path="${1}"
PATH_FORMAT="${PATH_FORMAT:-relative}"
if [[ ${PATH_FORMAT} == "relative" ]]; then
home_dir="${HOME:-$(dscl . -read "/Users/$(id -un)" NFSHomeDirectory 2>/dev/null | awk '{print $2}')}"
home_dir="${home_dir:-/Users/$(id -un)}"
current_path="${current_path/#${home_dir}/\~}"
fi
echo "#[fg=${BLUE},bg=${BG}]  ${RESET}${current_path} "
#!/usr/bin/env bash
# tmux-remote-widget: Status bar widget showing remote access state.
# Displays an SSH icon when remote access is active.
# Shows nothing when inactive (clean status bar).
STATE_FILE="/tmp/tmux-remote-state"
# Flexoki light theme colors
BG="#f2f0e5"
FG="#100f0f"
RED="#d14d41"
RESET="#[fg=${FG},bg=${BG},nobold,noitalics,nounderscore,nodim]"
if [[ -f "$STATE_FILE" ]]; then
echo "#[fg=${RED},bg=${BG},bold] 󰣀 SSH ${RESET}"
fi
#!/usr/bin/env bash
# tmux-remote: Toggle "remote access" mode for lid-closed SSH from iPhone.
#
# When enabled:
# - Prevents sleep on lid close (pmset disablesleep)
# - Keeps Wi-Fi alive during sleep (pmset womp + powernap)
# - Enables SSH (Remote Login)
# - Closes resource-heavy GUI apps to save battery
# - Stores list of closed apps for restoration on disable
#
# When disabled:
# - Re-enables normal sleep behavior
# - Disables SSH
# - Reopens apps that were closed
#
# Usage: tmux-remote on|off|toggle|status
STATE_FILE="/tmp/tmux-remote-state"
CLOSED_APPS_FILE="/tmp/tmux-remote-closed-apps"
# Apps to close for battery savings (process names as seen by osascript/pkill)
# These are the resource-heavy GUI apps; system utilities are left running.
APPS_TO_CLOSE=(
# Browsers
"Firefox"
"Google Chrome"
# Chat
"Slack"
"Microsoft Teams"
"Telegram"
"WhatsApp"
"Discord"
# Editors / IDEs
"Visual Studio Code"
"Cursor"
"IntelliJ IDEA CE"
"Android Studio"
# Dev tools
"DBeaver"
"Linear"
"Postman"
"Studio 3T"
"OrbStack"
# Media
"IINA"
"Spotify"
"Stremio"
# Productivity
"Anki"
"Anytype"
"calibre"
"Figma"
"LibreOffice"
"Loom"
"Microsoft Word"
"NetNewsWire"
"Notion Calendar"
"Obsidian"
"Spark"
# Games
"Steam"
# Terminal (if running the GUI terminal — you'll SSH in instead)
"Ghostty"
)
is_active() {
[[ -f "$STATE_FILE" ]]
}
get_running_apps() {
# Returns a list of currently running app names (one per line)
osascript -e 'tell application "System Events" to get name of every process whose background only is false' 2>/dev/null |
sed 's/, /\n/g'
}
close_apps() {
local running closed_count=0
running=$(get_running_apps)
# Clear previous closed apps list
: > "$CLOSED_APPS_FILE"
for app in "${APPS_TO_CLOSE[@]}"; do
if echo "$running" | grep -qxF "$app"; then
echo "$app" >> "$CLOSED_APPS_FILE"
# Graceful quit via osascript
osascript -e "tell application \"$app\" to quit" 2>/dev/null &
closed_count=$((closed_count + 1))
fi
done
# Wait for quit signals to be sent
wait
echo "$closed_count"
}
reopen_apps() {
if [[ ! -f "$CLOSED_APPS_FILE" ]]; then
return
fi
local count=0
while IFS= read -r app; do
[[ -z "$app" ]] && continue
open -gja "$app" 2>/dev/null &
count=$((count + 1))
done < "$CLOSED_APPS_FILE"
wait
rm -f "$CLOSED_APPS_FILE"
echo "$count"
}
enable_remote() {
if is_active; then
echo "Remote access is already enabled"
return 0
fi
# Close GUI apps first (before any sudo prompts)
local closed
closed=$(close_apps)
# Prevent sleep when lid is closed (battery mode)
sudo pmset -b disablesleep 1
# Keep network alive during lid-closed operation
# womp = Wake on Magic Packet (keeps network interface alive)
# powernap = allows periodic network activity during sleep
# tcpkeepalive = maintains TCP connections during sleep
sudo pmset -b womp 1
sudo pmset -b powernap 1
sudo pmset -b tcpkeepalive 1
# Enable SSH daemon via launchctl (avoids Full Disk Access requirement)
sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist 2>/dev/null
# Mark as active
touch "$STATE_FILE"
# Notify via tmux
if [[ -n "$TMUX" ]]; then
tmux display-message "Remote access ON — closed ${closed} apps — safe to close lid"
tmux refresh-client -S
else
echo "Remote access ON — closed ${closed} apps — safe to close lid"
fi
}
disable_remote() {
if ! is_active; then
echo "Remote access is already disabled"
return 0
fi
# Restore normal sleep behavior
sudo pmset -b disablesleep 0
sudo pmset -b womp 0
sudo pmset -b powernap 0
# tcpkeepalive is normally on by default, leave it
sudo pmset -b tcpkeepalive 1
# Disable SSH daemon via launchctl
sudo launchctl unload -w /System/Library/LaunchDaemons/ssh.plist 2>/dev/null
# Remove state file
rm -f "$STATE_FILE"
# Reopen apps that were closed
local reopened
reopened=$(reopen_apps)
# Notify via tmux
if [[ -n "$TMUX" ]]; then
tmux display-message "Remote access OFF — reopened ${reopened} apps"
tmux refresh-client -S
else
echo "Remote access OFF — reopened ${reopened} apps"
fi
}
toggle_remote() {
if is_active; then
disable_remote
else
enable_remote
fi
}
print_status() {
if is_active; then
echo "ACTIVE"
else
echo "INACTIVE"
fi
}
case "${1:-}" in
on) enable_remote ;;
off) disable_remote ;;
toggle) toggle_remote ;;
status) print_status ;;
*)
echo "Usage: tmux-remote on|off|toggle|status"
exit 1
;;
esac
#!/usr/bin/env bash
# tmux-status-right: Unified status bar right side.
# When remote mode is active, shows a minimal bar (SSH + battery).
# When inactive, shows the full bar (notifications + path + git).
STATE_FILE="/tmp/tmux-remote-state"
PANE_PATH="${1:-}"
# Flexoki light theme colors
BG="#f2f0e5"
FG="#100f0f"
RED="#d14d41"
GREEN="#879a39"
YELLOW="#d0a215"
ORANGE="#da702c"
RESET="#[fg=${FG},bg=${BG},nobold,noitalics,nounderscore,nodim]"
# --- Battery widget (inline, only in remote mode) ---
battery_widget() {
local info percent icon color charging=""
info=$(pmset -g batt 2>/dev/null)
[[ -z "$info" ]] && return
percent=$(echo "$info" | grep -oE '[0-9]+%' | head -1 | tr -d '%')
[[ -z "$percent" ]] && return
if echo "$info" | grep -q "AC Power"; then
charging="󰂄"
elif echo "$info" | grep -q "charged"; then
charging="󰂄"
fi
if [[ "$percent" -ge 80 ]]; then
icon="󰁹"; color="$GREEN"
elif [[ "$percent" -ge 60 ]]; then
icon="󰂀"; color="$GREEN"
elif [[ "$percent" -ge 40 ]]; then
icon="󰁾"; color="$YELLOW"
elif [[ "$percent" -ge 20 ]]; then
icon="󰁻"; color="$ORANGE"
else
icon="󰁺"; color="$RED"
fi
[[ -n "$charging" ]] && icon="$charging"
echo "#[fg=${color},bg=${BG},bold] ${icon} ${percent}%${RESET}"
}
if [[ -f "$STATE_FILE" ]]; then
# ---- Remote mode: minimal bar ----
# SSH indicator + battery + time
SSH="#[fg=${RED},bg=${BG},bold] 󰣀 SSH${RESET}"
BATT=$(battery_widget)
echo "${SSH}${BATT}"
else
# ---- Normal mode: full bar ----
# Notifications + path + git + date + time
# These call the existing widget scripts
SELF_DIR="$(dirname "$0")"
NOTIFY=$("${SELF_DIR}/tmux-notify-widget")
PATH_W=$("${SELF_DIR}/tmux-path-widget" "$PANE_PATH")
GIT=$("${SELF_DIR}/tmux-git-status" "$PANE_PATH")
echo "${NOTIFY}${PATH_W}${GIT}"
fi
#!/usr/bin/env bash
# tmux-attach: Attach to the most recent tmux session, or create one.
SESSION=$(tmux list-sessions -F "#{session_name}" 2>/dev/null | head -1)
if [ -z "$SESSION" ]; then
exec tmux new-session -s main
fi
exec tmux attach-session -t "$SESSION"
{ pkgs }:
pkgs.stdenvNoCC.mkDerivation {
pname = "tmux-extras";
version = "1.0.0";
src = ./.;
nativeBuildInputs = [ pkgs.makeWrapper ];
dontBuild = true;
installPhase = ''
mkdir -p $out/bin
cp git-status.sh $out/bin/tmux-git-status
cp path-widget.sh $out/bin/tmux-path-widget
cp cheatsheet.sh $out/bin/tmux-cheatsheet
cp notify.sh $out/bin/tmux-notify
cp notify-panel.sh $out/bin/tmux-notify-panel
cp notify-widget.sh $out/bin/tmux-notify-widget
cp tmux-attach.sh $out/bin/tmux-attach
cp tmux-group.sh $out/bin/tmux-group
cp tmux-ungroup.sh $out/bin/tmux-ungroup
cp remote.sh $out/bin/tmux-remote
cp remote-widget.sh $out/bin/tmux-remote-widget
cp battery-widget.sh $out/bin/tmux-battery-widget
cp status-right.sh $out/bin/tmux-status-right
cp git-root-path.sh $out/bin/tmux-git-root-path
chmod +x $out/bin/*
# Wrap notification scripts to ensure dependencies are on PATH
wrapProgram $out/bin/tmux-notify \
--prefix PATH : ${
pkgs.lib.makeBinPath [
pkgs.jq
pkgs.coreutils
pkgs.tmux
]
} \
--prefix PATH : $out/bin
wrapProgram $out/bin/tmux-notify-panel \
--prefix PATH : ${
pkgs.lib.makeBinPath [
pkgs.jq
pkgs.coreutils
pkgs.ncurses
]
} \
--prefix PATH : $out/bin
wrapProgram $out/bin/tmux-notify-widget \
--prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.jq ]}
wrapProgram $out/bin/tmux-attach \
--prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.tmux ]}
wrapProgram $out/bin/tmux-group \
--prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.tmux ]}
wrapProgram $out/bin/tmux-ungroup \
--prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.tmux ]}
wrapProgram $out/bin/tmux-remote \
--prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.tmux ]}
wrapProgram $out/bin/tmux-status-right \
--prefix PATH : ${
pkgs.lib.makeBinPath [
pkgs.jq
pkgs.coreutils
]
} \
--prefix PATH : $out/bin
'';
meta = {
description = "Custom tmux status bar widgets, cheatsheet, and notification system";
};
}
#!/usr/bin/env bash
# tmux-group: Create a grouped session from the current session.
# Opens in a new Ghostty window with independent window selection.
SESSION=$(tmux display-message -p "#{session_name}")
# Don't group an already grouped session — use its parent instead
if [[ "$SESSION" =~ _g[0-9]+$ ]]; then
SESSION="${SESSION%_g[0-9]*}"
fi
# Find next available group ID
id=1
while tmux has-session -t "${SESSION}_g${id}" 2>/dev/null; do
id=$((id + 1))
done
GROUP_SESSION="${SESSION}_g${id}"
# Open a new Ghostty window that creates the grouped session.
# destroy-unattached ensures cleanup when the window closes.
open -na Ghostty --args -e tmux new-session -t "$SESSION" -s "$GROUP_SESSION" \; \
set-option destroy-unattached on
{ pkgs }:
pkgs.tmuxPlugins.mkTmuxPlugin {
pluginName = "tmux-nerd-font-window-name";
version = "2.3.0-unstable-2026-02-17";
rtpFilePath = "tmux-nerd-font-window-name.tmux";
src = pkgs.fetchFromGitHub {
owner = "joshmedeski";
repo = "tmux-nerd-font-window-name";
rev = "7c08b6be2a1d0502d5c5cc7171f8507502ca3e25";
sha256 = "sha256-i3DT+r7WUvutRhob+tHZOe8TBUxpe4JflS9e1dgkg6s=";
};
}
#!/usr/bin/env bash
# tmux-ungroup: Leave and destroy the current grouped session.
# If not in a grouped session, does nothing.
SESSION=$(tmux display-message -p "#{session_name}")
if [[ ! "$SESSION" =~ _g[0-9]+$ ]]; then
tmux display-message "Not in a grouped session"
exit 0
fi
# Kill this grouped session — tmux will close the client (and Ghostty window)
tmux kill-session -t "$SESSION"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment