Last active
March 19, 2026 19:06
-
-
Save fersilva16/5269b5b76b74fd892ccbc7f2060f365c to your computer and use it in GitHub Desktop.
nix-darwin + OpenCode + tmux + Git Worktrees: complete macOS dev environment from scratch
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
| #!/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} " |
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
| #!/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 |
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
| # 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 |
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
| { 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="; | |
| }; | |
| } |
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
| #!/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 |
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
| #!/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" |
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
| #!/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" |
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
| #!/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 |
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
| #!/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 |
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
| #!/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 |
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
| { | |
| 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 | |
| ''; | |
| } | |
| ]; | |
| }; | |
| }; | |
| } |
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
| #!/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} " |
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
| #!/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 |
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
| #!/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 |
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
| #!/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 |
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
| #!/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" |
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
| { 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"; | |
| }; | |
| } |
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
| #!/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 |
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
| { 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="; | |
| }; | |
| } |
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
| #!/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