nixos-dotfiles
nixos-dotfiles
https://git.tonybtw.com/nixos-dotfiles.git
git://git.tonybtw.com/nixos-dotfiles.git
Initial commit.
Diff
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6ea7464
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+elpa/
+eln-cache/
+auto-save-list/
+tramp
+transient/
+recentf
+custom.el
+*.elc
+*~
+\#*\#
diff --git a/config.el b/config.el
new file mode 100644
index 0000000..9ba0ced
--- /dev/null
+++ b/config.el
@@ -0,0 +1,187 @@
+;;; config.el --- Configuration -*- lexical-binding: t -*-
+
+(when (file-directory-p "/run/current-system/sw/lib")
+ (add-to-list 'treesit-extra-load-path "/run/current-system/sw/lib"))
+
+(setq epg-pinentry-mode 'loopback)
+
+(add-hook 'org-mode-hook 'visual-line-mode)
+
+(defun rc/get-default-font ()
+ (cond
+ ((eq system-type 'windows-nt) "Consolas-13")
+ ((eq system-type 'gnu/linux) "Iosevka Nerd Font-24")))
+
+(add-to-list 'default-frame-alist `(font . ,(rc/get-default-font)))
+
+(tool-bar-mode 0)
+(menu-bar-mode 0)
+(scroll-bar-mode 0)
+(column-number-mode 1)
+(show-paren-mode 1)
+(global-display-line-numbers-mode 1)
+
+(setq-default c-basic-offset 4
+ c-default-style '((java-mode . "java")
+ (awk-mode . "awk")
+ (other . "bsd")))
+
+(add-hook 'c-mode-hook (lambda ()
+ (interactive)
+ (c-toggle-comment-style -1)))
+
+(add-hook 'rust-mode-hook
+ (lambda ()
+ (setq-local eglot-workspace-configuration
+ '(:rust-analyzer
+ (:cargo (:allFeatures t)
+ :rustfmt (:extraArgs ["--edition" "2021"]))))
+ (add-hook 'before-save-hook 'eglot-format-buffer nil t)))
+
+(add-hook 'before-save-hook 'delete-trailing-whitespace)
+
+(require 'dired-x)
+(require 'dired-aux)
+(setq dired-omit-files
+ (concat dired-omit-files "\\|^\\..+$"))
+(setq-default dired-dwim-target t)
+(setq dired-listing-switches "-alh")
+
+(setq-default indent-tabs-mode nil)
+(setq-default tab-width 4)
+(setq make-backup-files nil)
+(setq auto-save-default nil)
+
+(defvar my/base-dir "/home/tony")
+
+(defun my/set-base-dir ()
+ "Set the root directory for searches."
+ (interactive)
+ (setq my/base-dir (read-directory-name "Set base directory: " my/base-dir))
+ (message "Search root set to: %s" my/base-dir))
+
+(defun my/consult-fd ()
+ "Find files from base dir."
+ (interactive)
+ (let ((default-directory my/base-dir))
+ (consult-fd)))
+
+(defun my/fzf-find-file ()
+ "Find files from base dir using fzf."
+ (interactive)
+ (let ((default-directory my/base-dir))
+ (fzf-find-file)))
+
+(defun my/affe-find ()
+ "Find files from base dir using affe (async fuzzy)."
+ (interactive)
+ (affe-find my/base-dir))
+
+(load (expand-file-name "telescope.el" user-emacs-directory))
+
+(defun my/telescope-find-files ()
+ "Find files from base dir using telescope."
+ (interactive)
+ (telescope-find-files my/base-dir))
+
+(defun my/consult-ripgrep ()
+ "Ripgrep from base dir."
+ (interactive)
+ (consult-ripgrep my/base-dir))
+
+(defun my/consult-ripgrep-symbol ()
+ "Ripgrep symbol at point from base dir."
+ (interactive)
+ (consult-ripgrep my/base-dir (thing-at-point 'symbol t)))
+
+(defun my/find-emacs-config ()
+ "Find files in emacs config dir."
+ (interactive)
+ (let ((default-directory "/home/tony/.emacs.d/"))
+ (consult-fd)))
+
+(defun my/switch-project ()
+ "Pick a project from ~/repos, set base-dir, open dired."
+ (interactive)
+ (let* ((repos-dir "~/repos/")
+ (dirs (seq-filter
+ (lambda (f) (file-directory-p (expand-file-name f repos-dir)))
+ (directory-files repos-dir nil "^[^.]")))
+ (chosen (completing-read "Project: " dirs nil t)))
+ (when chosen
+ (let ((project-dir (expand-file-name chosen repos-dir)))
+ (setq my/base-dir project-dir)
+ (dired project-dir)
+ (message "Base dir: %s" project-dir)))))
+
+(defun my/vterm-here ()
+ "Open vterm in current window."
+ (interactive)
+ (let ((default-directory (or (and buffer-file-name (file-name-directory buffer-file-name))
+ default-directory))
+ (display-buffer-alist nil)) ; bypass all display rules
+ (pop-to-buffer-same-window (vterm "*vterm*"))))
+
+(setq display-buffer-alist
+ '(("\\*xref\\*\\|\\*compilation\\*\\|\\*grep\\*"
+ (display-buffer-reuse-window display-buffer-below-selected)
+ (window-height . 0.35))))
+
+(defun my/close-popup-window ()
+ "Close windows showing xref, compilation, grep, or help buffers."
+ (interactive)
+ (dolist (win (window-list))
+ (when (string-match-p "\\*xref\\*\\|\\*compilation\\*\\|\\*grep\\*\\|\\*Help\\*"
+ (buffer-name (window-buffer win)))
+ (delete-window win))))
+
+(defun my/reformat-parenthesized-content ()
+ "Reformat comma-separated content inside parentheses to multiple lines."
+ (interactive)
+ (let* ((line (thing-at-point 'line t))
+ (inside (and (string-match "(\\([^)]+\\))" line)
+ (match-string 1 line))))
+ (if (not inside)
+ (message "No content found inside parentheses")
+ (let* ((prefix (and (string-match "^\\(.*?\\)(" line)
+ (match-string 1 line)))
+ (suffix (and (string-match ")\\(.*\\)$" line)
+ (match-string 1 line)))
+ (parts (split-string inside "," t "[ \t]*"))
+ (new-lines (list (concat prefix "("))))
+ (dotimes (i (length parts))
+ (let ((part (nth i parts)))
+ (if (< i (1- (length parts)))
+ (push (concat " " part ",") new-lines)
+ (push (concat " " part) new-lines))))
+ (push (concat " )" (string-trim-right suffix)) new-lines)
+ (setq new-lines (nreverse new-lines))
+ (beginning-of-line)
+ (kill-line 1)
+ (insert (mapconcat 'identity new-lines "\n") "\n")))))
+
+(add-hook 'prog-mode-hook
+ (lambda ()
+ (modify-syntax-entry ?- "w")
+ (modify-syntax-entry ?_ "w")))
+
+(require 'erc)
+(require 'erc-services)
+(erc-services-mode 1)
+
+(setq erc-nick "tonybtw"
+ erc-user-full-name "tony"
+ erc-prompt-for-nickserv-password nil
+ erc-nickserv-identify-mode 'autodetect
+ erc-use-auth-source-for-nickserv-password t)
+
+(setq erc-autojoin-channels-alist
+ '(("libera.chat" "#technicalrenaissance")))
+
+(defun my/erc-connect-libera ()
+ "Connect to Libera.Chat via ERC."
+ (interactive)
+ (erc-tls :server "irc.libera.chat"
+ :port 6697
+ :nick "tonybtw"
+ :user "tony"))
diff --git a/init.el b/init.el
new file mode 100644
index 0000000..928b114
--- /dev/null
+++ b/init.el
@@ -0,0 +1,40 @@
+;;; init.el --- Main entry point -*- lexical-binding: t -*-
+
+(setq byte-compile-warnings '(not obsolete))
+(setq warning-suppress-log-types '((comp) (bytecomp)))
+(setq native-comp-async-report-warnings-errors 'silent)
+
+(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
+(package-initialize)
+
+(add-to-list 'package-archives
+ '("melpa" . "https://melpa.org/packages/") t)
+
+(defvar rc/package-contents-refreshed nil)
+
+(defun rc/package-refresh-contents-once ()
+ (when (not rc/package-contents-refreshed)
+ (setq rc/package-contents-refreshed t)
+ (package-refresh-contents)))
+
+(defun rc/require-one-package (package)
+ (when (not (package-installed-p package))
+ (rc/package-refresh-contents-once)
+ (package-install package)))
+
+(defun rc/require (&rest packages)
+ (dolist (package packages)
+ (rc/require-one-package package)))
+
+(defun rc/require-theme (theme)
+ (let ((theme-package (intern (concat (symbol-name theme) "-theme"))))
+ (rc/require theme-package)
+ (load-theme theme t)))
+
+; Load config files
+(load (expand-file-name "packages.el" user-emacs-directory))
+(load (expand-file-name "config.el" user-emacs-directory))
+(load (expand-file-name "keybinds.el" user-emacs-directory))
+
+(when (file-exists-p custom-file)
+ (load-file custom-file))
diff --git a/keybinds.el b/keybinds.el
new file mode 100644
index 0000000..d4f8d69
--- /dev/null
+++ b/keybinds.el
@@ -0,0 +1,144 @@
+;;; keybinds.el --- All keybindings -*- lexical-binding: t -*-
+
+;; C-c to escape insert mode
+(define-key evil-insert-state-map (kbd "C-c") 'evil-normal-state)
+
+;;; Global keys
+(global-set-key (kbd "M-x") 'execute-extended-command)
+(global-set-key (kbd "C-x b") 'consult-buffer)
+(global-set-key (kbd "C-c m s") 'magit-status)
+(global-set-key (kbd "C-c m l") 'magit-log)
+
+;;; Multiple cursors
+(global-set-key (kbd "C-S-c C-S-c") 'mc/edit-lines)
+(global-set-key (kbd "C->") 'mc/mark-next-like-this)
+(global-set-key (kbd "C-<") 'mc/mark-previous-like-this)
+(global-set-key (kbd "C-c C-<") 'mc/mark-all-like-this)
+(global-set-key (kbd "C-\"") 'mc/skip-to-next-like-this)
+(global-set-key (kbd "C-:") 'mc/skip-to-previous-like-this)
+
+;;; Move text
+(global-set-key (kbd "M-p") 'move-text-up)
+(global-set-key (kbd "M-n") 'move-text-down)
+
+;;; Dired evil bindings
+(with-eval-after-load 'dired
+ (evil-define-key 'normal dired-mode-map
+ "." 'dired-create-empty-file
+ "h" 'dired-up-directory
+ "l" 'dired-find-file
+ "n" 'evil-search-next
+ "N" 'evil-search-previous))
+
+;;; Org mode evil bindings
+(defun my/org-heading-has-checkbox-p ()
+ "Check if current heading has [ ] or [X] checkbox."
+ (save-excursion
+ (beginning-of-line)
+ (looking-at "^\\*+\\s-+\\[[ X-]\\]")))
+
+(defun my/org-update-parent-cookie ()
+ "Update parent heading's [/] or [n/m] cookie by counting child heading checkboxes."
+ (save-excursion
+ (when (org-up-heading-safe)
+ (let ((start (point))
+ (end (save-excursion (org-end-of-subtree t) (point)))
+ (level (org-current-level))
+ (checked 0)
+ (total 0))
+ ;; Count direct child headings with checkboxes
+ (save-excursion
+ (while (re-search-forward (format "^\\*\\{%d\\}\\s-+\\[\\([ X-]\\)\\]" (1+ level)) end t)
+ (setq total (1+ total))
+ (when (string= (match-string 1) "X")
+ (setq checked (1+ checked)))))
+ ;; Update the cookie in parent heading
+ (beginning-of-line)
+ (when (re-search-forward "\\[\\([0-9]*/[0-9]*\\|/\\)\\]" (line-end-position) t)
+ (replace-match (format "[%d/%d]" checked total) t t))))))
+
+(defun my/org-toggle-heading-checkbox ()
+ "Toggle [ ] <-> [X] in current heading and update parent cookie."
+ (save-excursion
+ (beginning-of-line)
+ (when (re-search-forward "\\[\\([ X-]\\)\\]" (line-end-position) t)
+ (replace-match (if (string= (match-string 1) " ") "[X]" "[ ]") t t)))
+ (my/org-update-parent-cookie))
+
+(defun my/org-dwim-at-point ()
+ "Do-what-I-mean at point: toggle checkbox, follow link, or cycle TODO."
+ (interactive)
+ (cond
+ ;; Heading with [ ] or [X]
+ ((my/org-heading-has-checkbox-p)
+ (my/org-toggle-heading-checkbox))
+ ;; List checkbox
+ ((org-at-item-checkbox-p)
+ (org-toggle-checkbox))
+ ;; Link
+ ((org-in-regexp org-link-any-re)
+ (org-open-at-point))
+ ;; Regular TODO heading
+ ((org-at-heading-p)
+ (org-todo))
+ (t (org-return))))
+
+(with-eval-after-load 'org
+ (evil-define-key 'normal org-mode-map
+ (kbd "RET") 'my/org-dwim-at-point
+ (kbd "<return>") 'my/org-dwim-at-point
+ "t" 'org-todo))
+
+;;; LSP keybindings
+(with-eval-after-load 'eglot
+ (evil-define-key 'normal eglot-mode-map
+ "K" 'eldoc-box-help-at-point
+ "gd" 'xref-find-definitions
+ "gr" 'xref-find-references))
+
+;;; Leader keybindings (SPC)
+(evil-leader/set-key
+ ;; files
+ "ff" 'my/telescope-find-files
+ "fF" 'my/fzf-find-file
+ "fg" 'my/consult-ripgrep
+ "fs" 'my/consult-ripgrep-symbol
+ "fo" 'consult-recent-file
+ "fl" 'consult-line
+ "fi" 'my/find-emacs-config
+ "fp" 'my/switch-project
+ ;; buffers
+ "bb" 'consult-buffer
+ "fb" 'consult-buffer
+ "bp" 'previous-buffer
+ "bn" 'next-buffer
+ "bd" 'kill-current-buffer
+ "bm" 'ibuffer
+ ;; custom
+ "cr" 'my/set-base-dir
+ "cd" (lambda () (interactive) (dired (file-name-directory (or buffer-file-name default-directory))))
+ "cl" 'my/close-popup-window
+ "cn" 'next-error
+ "cp" 'previous-error
+ "cc" 'compile
+ "cm" 'recompile
+ "cb" (lambda () (interactive) (compile "bear -- make"))
+ ;; magit
+ "ms" 'magit-status
+ "ml" 'magit-log
+ ;; ssh/servers
+ ; "st" 'my/connect-tonydev
+ ;; terminal
+ "to" 'my/vterm-here
+ ;; window
+ "wv" 'split-window-right
+ "ws" 'split-window-below
+ "wd" 'delete-window
+ "wh" 'evil-window-left
+ "wj" 'evil-window-down
+ "wk" 'evil-window-up
+ "wl" 'evil-window-right
+ ;; reformat
+ "qq" 'my/reformat-parenthesized-content
+ ;; ERC
+ "ec" 'my/erc-connect-libera)
diff --git a/packages.el b/packages.el
new file mode 100644
index 0000000..cc182c1
--- /dev/null
+++ b/packages.el
@@ -0,0 +1,221 @@
+;;; packages.el --- Package configuration -*- lexical-binding: t -*-
+
+;;; evil mode (vim bindings)
+(setq evil-want-keybinding nil) ; required before loading evil-collection
+(setq evil-search-module 'evil-search) ; required for cgn
+(setq evil-undo-system 'undo-redo) ; use emacs 28+ native undo-redo
+(rc/require 'evil 'evil-leader 'evil-collection 'evil-commentary)
+(global-evil-leader-mode)
+(evil-leader/set-leader "<SPC>")
+(evil-mode 1)
+(evil-collection-init)
+(evil-commentary-mode 1)
+
+;;; Vertico + Consult + Orderless (telescope-like fuzzy finding)
+(rc/require 'vertico 'consult 'orderless 'marginalia 'vertico-posframe 'fzf 'affe)
+(vertico-mode 1)
+(vertico-posframe-mode 1)
+(marginalia-mode 1)
+(recentf-mode 1)
+
+(setq vertico-posframe-parameters
+ '((left-fringe . 8)
+ (right-fringe . 8)))
+(setq vertico-posframe-poshandler #'posframe-poshandler-frame-center)
+
+(setq completion-styles '(orderless basic)
+ completion-category-defaults nil
+ completion-category-overrides '((file (styles . (partial-completion)))))
+
+;; Flex matching (fzf-style: characters in sequence)
+(setq orderless-matching-styles '(orderless-literal orderless-flex))
+
+;; Affe (async fuzzy finder using orderless)
+(setq affe-find-command "fd --color=never -t f")
+
+(setq consult-fd-args '("fd" "--color=never" "--type" "f" "--hidden" "--follow" "--exclude" ".git"))
+
+;; Live preview as you navigate
+(setq consult-preview-key 'any)
+
+;;; magit
+(rc/require 'magit)
+(setq magit-auto-revert-mode nil)
+
+;;; multiple cursors
+(rc/require 'multiple-cursors)
+
+;;; Move Text
+(rc/require 'move-text)
+
+;;; Company (autocompletion)
+(rc/require 'company)
+(global-company-mode)
+
+;;; Language modes
+(rc/require 'nix-mode 'zig-mode 'rust-mode 'php-mode 'web-mode 'go-mode 'typescript-mode)
+
+
+;;; Tree-sitter text objects (vif, vaf, vic, vac, etc.)
+(rc/require 'tree-sitter 'tree-sitter-langs 'evil-textobj-tree-sitter)
+(global-tree-sitter-mode)
+(add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode)
+(define-key evil-outer-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.outer"))
+(define-key evil-inner-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.inner"))
+(define-key evil-outer-text-objects-map "c" (evil-textobj-tree-sitter-get-textobj "class.outer"))
+(define-key evil-inner-text-objects-map "c" (evil-textobj-tree-sitter-get-textobj "class.inner"))
+
+;;; CSS color preview
+(rc/require 'rainbow-mode)
+(add-hook 'css-mode-hook 'rainbow-mode)
+(add-hook 'php-mode-hook 'rainbow-mode)
+(add-hook 'html-mode-hook 'rainbow-mode)
+(add-hook 'js-mode-hook 'rainbow-mode)
+(add-hook 'web-mode-hook 'rainbow-mode)
+(add-hook 'scss-mode-hook 'rainbow-mode)
+(add-hook 'conf-mode-hook 'rainbow-mode)
+(add-hook 'toml-mode-hook 'rainbow-mode)
+(add-hook 'yaml-mode-hook 'rainbow-mode)
+(add-hook 'conf-toml-mode-hook 'rainbow-mode)
+
+;;; Treesitter context (sticky function header)
+(rc/require 'topsy)
+(add-hook 'prog-mode-hook 'topsy-mode)
+
+;;; Org mode
+(rc/require 'org-superstar 'org-fancy-priorities)
+
+(setq org-directory "~/org/")
+(setq org-agenda-files '("~/repos/agendas/private.org"))
+
+;; Pretty bullets
+(add-hook 'org-mode-hook #'org-superstar-mode)
+(setq org-superstar-headline-bullets-list '("◉" "●" "○" "◆" "●" "○" "◆"))
+
+;; Priority icons
+(add-hook 'org-mode-hook #'org-fancy-priorities-mode)
+(setq org-fancy-priorities-list '("⚑" "▲" "»"))
+
+;; Syntax highlighting in code blocks
+(setq org-src-fontify-natively t
+ org-src-tab-acts-natively t
+ org-hide-block-startup nil
+ org-src-preserve-indentation nil
+ org-edit-src-content-indentation 0)
+
+;; Hide emphasis markers (*bold*, /italic/, etc.)
+(setq org-hide-emphasis-markers t)
+
+;; Visual tweaks
+(setq org-ellipsis " ▾") ; nicer fold indicator
+(setq org-startup-folded 'content) ; show headings on open
+(add-hook 'org-mode-hook #'org-indent-mode) ; clean indentation
+
+;; Make RET follow links and toggle checkboxes
+(setq org-return-follows-link t)
+
+;;; Org Present (presentation mode)
+(rc/require 'org-present 'visual-fill-column)
+
+(defun my/org-present-start ()
+ ;; Smaller, more readable font scaling
+ (setq-local face-remapping-alist
+ '((default (:height 1.3) default)
+ (header-line (:height 2.0) variable-pitch)
+ (org-document-title (:height 1.5) org-document-title)
+ (org-level-1 (:height 1.3) org-level-1)
+ (org-level-2 (:height 1.2) org-level-2)
+ (org-level-3 (:height 1.1) org-level-3)
+ (org-code (:height 1.0) org-code)
+ (org-block (:height 1.0) org-block)))
+ ;; Center content
+ (setq visual-fill-column-width 80)
+ (setq visual-fill-column-center-text t)
+ (visual-fill-column-mode 1)
+ ;; Word wrap
+ (visual-line-mode 1)
+ ;; Hide UI
+ (setq header-line-format " ")
+ (display-line-numbers-mode 0)
+ (org-display-inline-images))
+
+(defun my/org-present-end ()
+ (setq-local face-remapping-alist nil)
+ (setq header-line-format nil)
+ (visual-fill-column-mode 0)
+ (visual-line-mode 0)
+ (display-line-numbers-mode 1)
+ (org-remove-inline-images))
+
+(add-hook 'org-present-mode-hook #'my/org-present-start)
+(add-hook 'org-present-mode-quit-hook #'my/org-present-end)
+
+;;; LSP (eglot is built-in to Emacs 29+)
+(require 'eglot)
+(rc/require 'eldoc-box)
+
+;; Auto-start LSP for these modes
+(add-hook 'zig-mode-hook 'eglot-ensure)
+(add-hook 'nix-mode-hook 'eglot-ensure)
+(add-hook 'rust-mode-hook 'eglot-ensure)
+(add-hook 'c-mode-hook 'eglot-ensure)
+(add-hook 'php-mode-hook 'eglot-ensure)
+(add-hook 'go-mode-hook 'eglot-ensure)
+(add-hook 'typescript-mode-hook 'eglot-ensure)
+(add-hook 'tsx-ts-mode-hook 'eglot-ensure)
+
+;; LSP server configurations
+(with-eval-after-load 'eglot
+ ;; PHP
+ (add-to-list 'eglot-server-programs
+ '(php-mode . ("intelephense" "--stdio")))
+ (add-to-list 'eglot-server-programs
+ '(web-mode . ("intelephense" "--stdio")))
+ ;; TypeScript/TSX (typescript-language-server)
+ (add-to-list 'eglot-server-programs
+ '(typescript-mode . ("typescript-language-server" "--stdio")))
+ (add-to-list 'eglot-server-programs
+ '(tsx-ts-mode . ("typescript-language-server" "--stdio"))))
+
+;; File associations for TypeScript React
+(add-to-list 'auto-mode-alist '("\\.tsx\\'" . tsx-ts-mode))
+(add-to-list 'auto-mode-alist '("\\.ts\\'" . typescript-mode))
+
+;; Go format on save
+(add-hook 'go-mode-hook
+ (lambda ()
+ (add-hook 'before-save-hook 'eglot-format-buffer nil t)))
+
+(add-hook 'web-mode-hook 'eglot-ensure)
+
+
+;;; Direnv integration (loads devshell environment)
+(rc/require 'envrc)
+(envrc-global-mode)
+
+;;; vterm (terminal emulator)
+(rc/require 'vterm)
+(defun rc/find-shell ()
+ "Find a suitable shell, checking common locations."
+ (or (getenv "SHELL")
+ (seq-find #'file-executable-p
+ '("/bin/bash" ; FHS standard
+ "/usr/bin/bash" ; Some distros
+ "/run/current-system/sw/bin/bash" ; NixOS
+ "/bin/sh")) ; Ultimate fallback
+ "/bin/sh"))
+(setq vterm-shell (rc/find-shell))
+(setq vterm-kill-buffer-on-exit t)
+
+;;; Theme
+;; (rc/require-theme 'gruber-darker)
+(rc/require 'doom-themes)
+(load-theme 'doom-palenight t)
+
+;;; Clean up modeline (hide minor modes)
+(setq eldoc-minor-mode-string nil)
+(setq company-lighter nil)
+(setq-default abbrev-mode nil)
+(with-eval-after-load 'flymake (setq flymake-mode-line-format nil))
+(with-eval-after-load 'envrc (setq envrc-lighter nil))
+(with-eval-after-load 'evil-commentary (setq evil-commentary-mode-lighter nil))
diff --git a/telescope.el b/telescope.el
new file mode 100644
index 0000000..a91ea57
--- /dev/null
+++ b/telescope.el
@@ -0,0 +1,320 @@
+;;; telescope.el --- Telescope-like fuzzy finder -*- lexical-binding: t -*-
+
+(require 'posframe)
+
+(defgroup telescope nil
+ "Telescope-like fuzzy finder."
+ :group 'convenience)
+
+(defcustom telescope-fd-command "fd -t f --color=never --hidden --exclude .git"
+ "Command to list files."
+ :type 'string
+ :group 'telescope)
+
+(defcustom telescope-results-width-pct 0.35
+ "Width of results window as percentage of frame."
+ :type 'float
+ :group 'telescope)
+
+(defcustom telescope-preview-width-pct 0.50
+ "Width of preview window as percentage of frame."
+ :type 'float
+ :group 'telescope)
+
+(defcustom telescope-height-pct 0.70
+ "Height of telescope as percentage of frame."
+ :type 'float
+ :group 'telescope)
+
+(defcustom telescope-gap 2
+ "Gap between results and preview panes in characters."
+ :type 'integer
+ :group 'telescope)
+
+(defcustom telescope-padding 2
+ "Internal padding within panes in characters."
+ :type 'integer
+ :group 'telescope)
+
+;; Internal state
+(defvar telescope--input "")
+(defvar telescope--files nil)
+(defvar telescope--filtered nil)
+(defvar telescope--selected 0)
+(defvar telescope--base-dir nil)
+(defvar telescope--results-buffer " *telescope-results*")
+(defvar telescope--preview-buffer " *telescope-preview*")
+
+(defun telescope--get-files (dir)
+ "Get all files in DIR using fd."
+ (let ((default-directory dir))
+ (split-string
+ (shell-command-to-string (concat telescope-fd-command " ."))
+ "\n" t)))
+
+(defun telescope--fzf-filter (files query)
+ "Filter FILES with QUERY using fzf --filter."
+ (if (or (null files) (string-empty-p query))
+ (seq-take files 100)
+ (with-temp-buffer
+ (insert (string-join files "\n"))
+ (call-process-region (point-min) (point-max) "fzf" t t nil "--filter" query)
+ (split-string (buffer-string) "\n" t))))
+
+(defun telescope--render-results ()
+ "Render the results buffer."
+ (with-current-buffer (get-buffer-create telescope--results-buffer)
+ (let* ((inhibit-read-only t)
+ (pad (make-string telescope-padding ?\s))
+ (max-results (- telescope--height 5)) ; leave room for prompt/footer/padding
+ (content-width (- telescope--results-width (* telescope-padding 2) 2))
+ (separator (make-string (min content-width 80) ?─)))
+ (erase-buffer)
+ ;; Top padding
+ (insert "\n")
+ ;; Prompt
+ (insert pad)
+ (insert (propertize "> " 'face '(:foreground "#87afff" :weight bold)))
+ (insert telescope--input)
+ (insert (propertize "█" 'face 'cursor))
+ (insert "\n")
+ (insert pad)
+ (insert (propertize separator 'face '(:foreground "#444444")))
+ (insert "\n")
+ ;; Results
+ (if (null telescope--filtered)
+ (progn
+ (insert pad)
+ (insert (propertize " No matches\n" 'face '(:foreground "#666666" :slant italic))))
+ (let ((idx 0))
+ (dolist (file (seq-take telescope--filtered max-results))
+ (let* ((max-len (- content-width 4))
+ (display (if (> (length file) max-len)
+ (concat "..." (substring file (- 3 max-len)))
+ file))
+ (selected (= idx telescope--selected))
+ (face (if selected
+ '(:background "#3a3a3a" :weight bold)
+ nil))
+ (prefix (if selected "→ " " ")))
+ (insert pad)
+ (insert (propertize (concat prefix display "\n") 'face face)))
+ (cl-incf idx))))
+ ;; Footer
+ (insert pad)
+ (insert (propertize separator 'face '(:foreground "#444444")))
+ (insert "\n")
+ (insert pad)
+ (insert (propertize (format "%d/%d"
+ (min (1+ telescope--selected) (length telescope--filtered))
+ (length telescope--filtered))
+ 'face '(:foreground "#666666"))))))
+
+(defun telescope--update-preview ()
+ "Update preview buffer with selected file content."
+ (with-current-buffer (get-buffer-create telescope--preview-buffer)
+ (let ((inhibit-read-only t)
+ (pad (make-string telescope-padding ?\s)))
+ (erase-buffer)
+ (if-let* ((selected (nth telescope--selected telescope--filtered))
+ (path (expand-file-name selected telescope--base-dir))
+ (_ (and (file-exists-p path)
+ (file-regular-p path)
+ (not (file-directory-p path)))))
+ (condition-case nil
+ (progn
+ ;; Top padding
+ (insert "\n")
+ ;; File name header
+ (insert pad)
+ (insert (propertize (file-name-nondirectory path) 'face '(:foreground "#87afff" :weight bold)))
+ (insert "\n")
+ (insert pad)
+ (insert (propertize (make-string (min 60 (- telescope--preview-width 4)) ?─) 'face '(:foreground "#444444")))
+ (insert "\n")
+ ;; Content with left padding
+ (let ((start (point)))
+ (insert-file-contents path nil 0 10000)
+ (goto-char start)
+ ;; Add padding to each line
+ (while (not (eobp))
+ (insert pad)
+ (forward-line 1)))
+ (goto-char (point-min))
+ ;; Apply syntax highlighting
+ (when-let ((mode (assoc-default path auto-mode-alist 'string-match)))
+ (delay-mode-hooks (funcall mode))
+ (font-lock-ensure)))
+ (error
+ (erase-buffer)
+ (insert "\n" pad)
+ (insert (propertize "Cannot preview file" 'face '(:foreground "#666666")))))
+ (insert "\n" pad)
+ (insert (propertize "No preview available" 'face '(:foreground "#666666")))))))
+
+(defvar telescope--results-x 0)
+(defvar telescope--results-y 0)
+(defvar telescope--preview-x 0)
+(defvar telescope--results-width 55)
+(defvar telescope--preview-width 75)
+(defvar telescope--height 25)
+
+(defun telescope--calc-dimensions ()
+ "Calculate frame dimensions based on percentages."
+ (let ((frame-cols (frame-width))
+ (frame-rows (frame-height)))
+ (setq telescope--results-width (max 30 (floor (* frame-cols telescope-results-width-pct))))
+ (setq telescope--preview-width (max 40 (floor (* frame-cols telescope-preview-width-pct))))
+ (setq telescope--height (max 15 (floor (* frame-rows telescope-height-pct))))))
+
+(defun telescope--calc-positions ()
+ "Calculate frame positions."
+ (telescope--calc-dimensions)
+ (let* ((char-width (frame-char-width))
+ (char-height (frame-char-height))
+ (border-px 4)
+ (gap-px (* telescope-gap char-width))
+ (total-pixel-width (+ (* telescope--results-width char-width)
+ (* telescope--preview-width char-width)
+ (* 2 border-px)
+ gap-px))
+ (total-pixel-height (* telescope--height char-height))
+ (start-x (max 0 (/ (- (frame-pixel-width) total-pixel-width) 2)))
+ (start-y (max 0 (/ (- (frame-pixel-height) total-pixel-height) 2))))
+ (setq telescope--results-x start-x)
+ (setq telescope--results-y start-y)
+ (setq telescope--preview-x (+ start-x (* telescope--results-width char-width) border-px gap-px))))
+
+(defun telescope--results-poshandler (_info)
+ "Position handler for results frame."
+ (cons telescope--results-x telescope--results-y))
+
+(defun telescope--preview-poshandler (_info)
+ "Position handler for preview frame."
+ (cons telescope--preview-x telescope--results-y))
+
+(defun telescope--show-frames ()
+ "Display the telescope posframes."
+ (telescope--calc-positions)
+ ;; Results frame (left)
+ (posframe-show telescope--results-buffer
+ :position (point-min)
+ :poshandler #'telescope--results-poshandler
+ :width telescope--results-width
+ :height telescope--height
+ :border-width 2
+ :border-color "#5f5fff"
+ :background-color "#1c1c1c"
+ :foreground-color "#d0d0d0"
+ :override-parameters '((cursor-type . nil)))
+ ;; Preview frame (right)
+ (posframe-show telescope--preview-buffer
+ :position (point-min)
+ :poshandler #'telescope--preview-poshandler
+ :width telescope--preview-width
+ :height telescope--height
+ :border-width 2
+ :border-color "#5f87af"
+ :background-color "#1c1c1c"
+ :foreground-color "#d0d0d0"
+ :override-parameters '((cursor-type . nil))))
+
+(defun telescope--hide-frames ()
+ "Hide telescope posframes."
+ (posframe-hide telescope--results-buffer)
+ (posframe-hide telescope--preview-buffer))
+
+(defun telescope--refresh ()
+ "Refresh filtered results and display."
+ (setq telescope--filtered (telescope--fzf-filter telescope--files telescope--input))
+ (setq telescope--selected (min telescope--selected
+ (max 0 (1- (length telescope--filtered)))))
+ (telescope--render-results)
+ (telescope--update-preview)
+ (telescope--show-frames))
+
+(defun telescope--select-next ()
+ "Select next item."
+ (when telescope--filtered
+ (let ((max-visible (- telescope--height 5)))
+ (setq telescope--selected
+ (min (1+ telescope--selected)
+ (1- (min (length telescope--filtered) max-visible)))))
+ (telescope--render-results)
+ (telescope--update-preview)
+ (telescope--show-frames)))
+
+(defun telescope--select-prev ()
+ "Select previous item."
+ (setq telescope--selected (max 0 (1- telescope--selected)))
+ (telescope--render-results)
+ (telescope--update-preview)
+ (telescope--show-frames))
+
+(defun telescope--backspace ()
+ "Delete last character from input."
+ (when (> (length telescope--input) 0)
+ (setq telescope--input (substring telescope--input 0 -1))
+ (telescope--refresh)))
+
+(defun telescope--insert-char (char)
+ "Insert CHAR into input."
+ (setq telescope--input (concat telescope--input (char-to-string char)))
+ (telescope--refresh))
+
+;;;###autoload
+(defun telescope-find-files (&optional dir)
+ "Find files in DIR using telescope interface."
+ (interactive)
+ (let ((dir (expand-file-name (or dir default-directory))))
+ (setq telescope--base-dir dir)
+ (setq telescope--input "")
+ (setq telescope--selected 0)
+ (setq telescope--files (telescope--get-files dir))
+ (setq telescope--filtered (seq-take telescope--files 100))
+ (telescope--render-results)
+ (telescope--update-preview)
+ (telescope--show-frames)
+ (let ((result nil))
+ (unwind-protect
+ (while (null result)
+ (let ((key (read-key (propertize " " 'face '(:height 0.1)))))
+ (cond
+ ;; Quit
+ ((memq key '(?\e ?\C-g ?\C-c))
+ (setq result 'cancel))
+ ;; Confirm selection
+ ((memq key '(?\r ?\C-m))
+ (if telescope--filtered
+ (setq result (expand-file-name
+ (nth telescope--selected telescope--filtered)
+ telescope--base-dir))
+ (setq result 'cancel)))
+ ;; Navigation
+ ((or (memq key '(?\C-n ?\C-j)) (equal key 'down))
+ (telescope--select-next))
+ ((or (memq key '(?\C-p ?\C-k)) (equal key 'up))
+ (telescope--select-prev))
+ ;; Scroll
+ ((memq key '(?\C-d))
+ (dotimes (_ 5) (telescope--select-next)))
+ ((memq key '(?\C-u))
+ (dotimes (_ 5) (telescope--select-prev)))
+ ;; Delete
+ ((memq key '(?\C-h ?\C-? 127 backspace))
+ (telescope--backspace))
+ ;; Clear input
+ ((memq key '(?\C-w))
+ (setq telescope--input "")
+ (telescope--refresh))
+ ;; Regular character input
+ ((and (characterp key) (>= key 32) (<= key 126))
+ (telescope--insert-char key)))))
+ ;; Cleanup
+ (telescope--hide-frames))
+ ;; Handle result
+ (unless (eq result 'cancel)
+ (find-file result)))))
+
+(provide 'telescope)
+;;; telescope.el ends here