nixfleet/hmModules/emacs/init.el
Andrea Ciceri 40fd5e3035
Some checks failed
/ test (push) Failing after 31m28s
Disasble gptel tools
2025-04-28 13:10:01 +02:00

1214 lines
40 KiB
EmacsLisp

;; package --- My Emacs config -*- lexical-binding:t -*-
;; Author: Andrea Ciceri <andrea.ciceri@autistici.org>
;;; Commentary:
;; TODO org goodies
;; TODO understand how to configure cape
;; TODO org-roam-protocol to capture notes from the browser
;; TODO attach documents (like PDFs) to my Zettelkasten notes
;;; Code:
(use-package flymake
:config
;; TODO write "E", "W" or "N" in margins overriding the margin created by diff-hl
;; (push `(before-string . ,(propertize " " 'display '((margin left-margin) "E"))) (get :error 'flymake-overlay-control))
;; (push `(before-string . ,(propertize " " 'display '((margin left-margin) "W"))) (get :warning 'flymake-overlay-control))
;; (push `(before-string . ,(propertize " " 'display '((margin left-margin) "N"))) (get :note 'flymake-overlay-control))
;; TODO set following only when on terminal (where wavy underlines are not shown)
;; (set-face-attribute 'flymake-error nil :inverse-video t)
;; (set-face-attribute 'flymake-warning nil :inverse-video t)
;; (set-face-attribute 'flymake-note nil :inverse-video t)
:custom
(flymake-mode-line-lighter "Fly")
:hook prog-mode)
(use-package eglot
:custom
;; Tricks that should make Emacs faster
(eglot-events-buffer-size 0) ; disable events logging, it should be enabled only when debuggigng LSP servers
(eglot-sync-connect-nil 0) ; disable UI freeze when opening big files
(eglot-connect-timeout nil) ; never timeout
)
(use-package consult-eglot
:after (consult eglot embark)
:config
(require 'consult-eglot-embark)
(consult-eglot-embark-mode)
)
(use-package eglot-booster
:after eglot
:config (eglot-booster-mode))
(use-package emacs
:bind (("<mouse-4>" . scroll-down-line)
("<mouse-5>" . scroll-up-line)
(("C-x F" . recentf-open)))
:hook (server-after-make-frame . (lambda () (xterm-mouse-mode +1))) ;; FIXME why is this needed?
:custom
(use-dialog-box nil)
(use-short-answers t)
(native-comp-async-report-warnings-errors nil)
(inhibit-startup-message t)
(visible-bell t)
(temporary-file-directory "~/.emacs-saves/")
(backup-directory-alist `(("." . ,temporary-file-directory)))
(auto-save-files-name-transforms `((".*" ,temporary-file-directory t)))
(backup-by-copying t)
(focus-follows-mouse t)
(mouse-autoselect-window t)
(treesit-font-lock-level 4)
(custom-file "~/.config/emacs/custom.el")
(frame-title-format "%b - Emacs")
:config
(set-face-background 'vertical-border (face-background 'default))
(set-display-table-slot standard-display-table 'vertical-border (make-glyph-code ?┃))
(menu-bar-mode -1)
(scroll-bar-mode -1)
(xterm-mouse-mode +1)
(tool-bar-mode -1)
(global-hl-line-mode -1)
(global-auto-revert-mode t)
(show-paren-mode +1)
(column-number-mode +1)
(add-to-list 'default-frame-alist '(font . "Iosevka Comfy-13"))
(recentf-mode +1)
(fset #'jsonrpc--log-event #'ignore) ; this should be enabled only when needed, otherwise makes Emacs slower
(setq save-interprogram-paste-before-kill 't) ; system clipboard will be saved in the kill ring
(defun ccr/reload-emacs ()
(interactive)
(load-file "~/.config/emacs/init.el"))
(load-theme 'catppuccin 't)
(defun ccr/nixos-rebuild ()
(interactive)
(let* ((operation (completing-read "nixos-rebuild " '("switch" "boot" "test" "dry-activate")))
(buffer-name (format "nixos-rebuild-%s" operation)))
(async-shell-command (format "sudo nixos-rebuild --flake fleet %s -L" operation) buffer-name)))
)
(use-package ultra-scroll
:init
(setq scroll-conservatively 101 ; important!
scroll-margin 0)
:config
(ultra-scroll-mode 1))
(use-package doc-view
:custom
(doc-view-scale-internally nil)
(doc-view-imenu-enabled 't)
(doc-view-continuous t))
(use-package tramp
:config
;; TODO ugly `ccr' hardcoded, moreover this makes sense only when connecting to NixOS machines
(add-to-list 'tramp-remote-path "/home/ccr/.nix-profile/bin" 't)
(add-to-list 'tramp-remote-path "/etc/profiles/per-user/ccr/bin" 't)
(add-to-list 'tramp-remote-path "/run/current-system/sw/bin" 't)
(add-to-list 'tramp-remote-path 'tramp-own-remote-path)
:custom
(tramp-use-ssh-controlmaster-options nil) ;; makes tramp use ~/.ssh/config
)
(use-package ligature
:config
(ligature-set-ligatures 't '("www"))
(ligature-set-ligatures 'eww-mode '("ff" "fi" "ffi"))
(ligature-set-ligatures 'prog-mode '("|||>" "<|||" "<==>" "<!--" "####" "~~>" "***" "||=" "||>"
":::" "::=" "=:=" "===" "==>" "=!=" "=>>" "=<<" "=/=" "!=="
"!!." ">=>" ">>=" ">>>" ">>-" ">->" "->>" "-->" "---" "-<<"
"<~~" "<~>" "<*>" "<||" "<|>" "<$>" "<==" "<=>" "<=<" "<->"
"<--" "<-<" "<<=" "<<-" "<<<" "<+>" "</>" "###" "#_(" "..<"
"..." "+++" "/==" "///" "_|_" "www" "&&" "^=" "~~" "~@" "~="
"~>" "~-" "**" "*>" "*/" "||" "|}" "|]" "|=" "|>" "|-" "{|"
"[|" "]#" "::" ":=" ":>" ":<" "$>" "==" "=>" "!=" "!!" ">:"
">=" ">>" ">-" "-~" "-|" "->" "--" "-<" "<~" "<*" "<|" "<:"
"<$" "<=" "<>" "<-" "<<" "<+" "</" "#{" "#[" "#:" "#=" "#!"
"##" "#(" "#?" "#_" "%%" ".=" ".-" ".." ".?" "+>" "++" "?:"
"?=" "?." "??" ";;" "/*" "/=" "/>" "//" "__" "~~" "(*" "*)"
"\\\\" "://"))
(global-ligature-mode t))
(use-package rainbow-delimiters
:hook (prog-mode . rainbow-delimiters-mode))
(use-package pulsar
:after (consult imenu)
:config
(pulsar-global-mode)
;; TODO use :hook (I've tried but it didn't work, why?)
(add-hook 'consult-after-jump-hook #'pulsar-recenter-top)
(add-hook 'consult-after-jump-hook #'pulsar-reveal-entry)
(add-hook 'imenu-after-jump-hook #'pulsar-recenter-top)
(add-hook 'imenu-after-jump-hook #'pulsar-reveal-entry)
(add-hook 'next-error-hook #'pulsar-pulse-line)
)
(use-package visual-replace
:defer t
:bind (("C-c r" . visual-replace)
:map isearch-mode-map
("C-c r" . visual-replace-from-isearch)))
(use-package clipetty
:delight
:hook (after-init . global-clipetty-mode))
(use-package nerd-icons)
(use-package nerd-icons-completion
:after marginalia
:config (nerd-icons-completion-mode +1)
:hook (
(marginalia-mode . nerd-icons-completion-marginalia-setup)
(ibuffer-mode . nerd-icons-completion-marginalia-setup)))
(use-package nerd-icons-ibuffer
:hook (ibuffer-mode . nerd-icons-ibuffer-mode))
(use-package treemacs-nerd-icons
:config
(treemacs-load-theme "nerd-icons"))
(use-package nerd-icons-dired
:hook
(dired-mode . nerd-icons-dired-mode))
(use-package indent-bars
:config
(require 'indent-bars-ts)
:custom
(indent-bars-treesit-support t)
(indent-bars-spacing-override 2)
;; (indent-bars-treesit-wrap '())
(indent-bars-color-by-depth '(:regexp "outline-\\([0-9]+\\)" :blend 0.4))
(indent-bars-no-stipple-char (string-to-char ""))
(indent-bars-prefer-character 't) ;; so it works also in terminal
)
(use-package copilot
:custom
(copilot-max-char -1)
(copilot-indent-offset-warning-disable 't)
:hook (prog-mode org-mode)
:bind (("C-<tab>" . copilot-accept-completion)))
(use-package diredfl
:config (diredfl-global-mode))
(use-package treemacs
:after solaire-mode
:custom
(treemacs-show-cursor nil)
(treemacs-display-current-project-exclusively t)
(treemacs-project-followlinto-home nil)
(treemacs-display-current-project-exclusively t)
(treemacs-git-mode 'deferred)
:bind (("C-c w t" . treemacs-select-window)
("C-c o T" . treemacs))
)
(use-package meow
:hook (server-after-make-frame . (lambda () (meow--prepare-face)))
:custom
(meow-use-clipboard 't)
:config
(require 'meow-tree-sitter)
(meow-tree-sitter-register-defaults)
(add-hook 'after-make-frame-functions (defun ccr/meow--prepare-face (_)
(meow--prepare-face)
(remove-hook 'after-make-frame-functions 'ccr/meow--prepare-face)))
(add-to-list 'meow-mode-state-list '(eshell-mode . insert))
(add-to-list 'meow-mode-state-list '(eat-mode . insert))
(add-to-list 'meow-mode-state-list '(notmuch-hello-mode . insert))
(add-to-list 'meow-mode-state-list '(notmuch-search-mode . insert))
:init
(meow-global-mode 1)
(meow-motion-overwrite-define-key
'("j" . meow-next)
'("k" . meow-prev)
'("<escape>" . ignore))
(meow-leader-define-key
;; SPC j/k will run the original command in MOTION state.
'("j" . "H-j")
'("k" . "H-k")
;; Use SPC (0-9) for digit arguments.
'("1" . meow-digit-argument)
'("2" . meow-digit-argument)
'("3" . meow-digit-argument)
'("4" . meow-digit-argument)
'("5" . meow-digit-argument)
'("6" . meow-digit-argument)
'("7" . meow-digit-argument)
'("8" . meow-digit-argument)
'("9" . meow-digit-argument)
'("0" . meow-digit-argument)
'("/" . meow-keypad-describe-key)
'("?" . meow-cheatsheet))
(meow-normal-define-key
'("0" . meow-expand-0)
'("9" . meow-expand-9)
'("8" . meow-expand-8)
'("7" . meow-expand-7)
'("6" . meow-expand-6)
'("5" . meow-expand-5)
'("4" . meow-expand-4)
'("3" . meow-expand-3)
'("2" . meow-expand-2)
'("1" . meow-expand-1)
'("-" . negative-argument)
'(";" . meow-reverse)
'("," . meow-inner-of-thing)
'("." . meow-bounds-of-thing)
'("[" . meow-beginning-of-thing)
'("]" . meow-end-of-thing)
'(">" . indent-rigidly-right)
'("<" . indent-rigidly-left)
'("a" . meow-append)
'("A" . meow-open-below)
'("b" . meow-back-word)
'("B" . meow-back-symbol)
'("c" . meow-change)
'("d" . meow-delete)
'("D" . meow-backward-delete)
'("e" . meow-next-word)
'("E" . meow-next-symbol)
'("f" . meow-find)
'("g" . meow-cancel-selection)
'("G" . meow-grab)
'("h" . meow-left)
'("H" . meow-left-expand)
'("i" . meow-insert)
'("I" . meow-open-above)
'("j" . meow-next)
'("J" . meow-next-expand)
'("k" . meow-prev)
'("K" . meow-prev-expand)
'("l" . meow-right)
'("L" . meow-right-expand)
'("m" . meow-join)
'("n" . meow-search)
'("o" . meow-block)
'("O" . meow-to-block)
'("p" . meow-yank)
'("q" . meow-quit)
'("Q" . meow-goto-line)
'("r" . meow-replace)
'("R" . meow-swap-grab)
'("s" . meow-kill)
'("t" . meow-till)
'("u" . meow-undo)
'("U" . vundo)
'("/" . meow-visit)
'("v" . meow-visit)
'("w" . meow-mark-word)
'("W" . meow-mark-symbol)
'("x" . meow-line)
'("X" . meow-goto-line)
'("y" . meow-save)
'("Y" . meow-sync-grab)
'("z" . meow-pop-selection)
'("'" . repeat)
'("<escape>" . ignore)))
(use-package windmove
:config
(windmove-mode +1)
(defcustom ccr/v-resize-amount 4
"How smany rows move when calling `ccr/v-resize`"
:type 'integer
:group 'ccr)
(defcustom ccr/h-resize-amount 4
"How many columns move when calling `ccr/h-resize`"
:type 'integer
:group 'ccr
)
(defun ccr/v-resize (key)
"Interactively vertically resize the window"
(interactive "cHit >/< to enlarge/shrink")
(cond ((eq key (string-to-char ">"))
(enlarge-window-horizontally ccr/v-resize-amount)
(call-interactively 'ccr/v-resize))
((eq key (string-to-char "<"))
(enlarge-window-horizontally (- ccr/v-resize-amount))
(call-interactively 'ccr/v-resize))
(t (push key unread-command-events))))
(defun ccr/h-resize (key)
"Interactively horizontally resize the window"
(interactive "cHit >/< to enlarge/shrink")
(cond ((eq key (string-to-char ">"))
(enlarge-window ccr/h-resize-amount)
(call-interactively 'ccr/h-resize))
((eq key (string-to-char "<"))
(enlarge-window (- ccr/h-resize-amount))
(call-interactively 'ccr/h-resize))
(t (push key unread-command-events))))
:bind (("C-c w k" . windmove-up)
("C-c w l" . windmove-right)
("C-c w j" . windmove-down)
("C-c w h" . windmove-left)
("M-k" . windmove-up)
("M-l" . windmove-right)
("M-j" . windmove-down)
("M-h" . windmove-left)
("C-c w <up>" . windmove-up)
("C-c w <right>" . windmove-right)
("C-c w <down>" . windmove-down)
("C-c w <left>" . windmove-left)
("C-c w q" . delete-window)
("C-c w K" . windmove-delete-up)
("C-c w L" . windmove-delete-right)
("C-c w J" . windmove-delete-down)
("C-c w H" . windmove-delete-left)
("C-c w x" . kill-buffer-and-window)
("C-c w v" . split-window-right)
("C-c w s" . split-window-below)
("C-c w V" . ccr/v-resize)
("C-c w S" . ccr/h-resize)))
(use-package vertico
:custom
(vertico-mouse-mode t)
;; (vertico-reverse-mode t) ;; FIXME breaks vertico-posframe
(vertico-count 16)
(vertico-resize t)
(vertico-cycle t)
(vertico-mode t)
:bind (:map vertico-map
(("DEL" . vertico-directory-delete-char)
("C-DEL" . vertico-directory-delete-word)
("M-q" . vertico-quick-insert)
("C-q" . vertico-quick-exit))))
;; (use-package vertico-posframe
;; :after vertico
;; :config
;; (vertico-posframe-mode +1)
;; :custom
;; (vertico-multiform-commands
;; '((t
;; posframe
;; (vertico-posframe-poshandler . posframe-poshandler-frame-center)
;; (vertico-posframe-fallback-mode . vertico-buffer-mode))))
;; (vertico-posframe-min-height 1)
;; ;; (vertico-posframe-min-width 80)
;; (vertico-posframe-parameters '((alpha-background . 80)))
;; )
(use-package marginalia
:init
(marginalia-mode +1)
:custom
(marginalia-aligh 'right))
(use-package consult
:bind (([remap switch-to-buffer] . consult-buffer)
([remap goto-line] . consult-goto-line)
([remazp imenu] . consult-imenu)
([remap project-switch-to-buffer] . consult-project-buffer)
("C-c b b" . consult-project-buffer)
("C-c b B" . consult-buffer)
("C-c g l" . consult-goto-line)
("C-c b i" . consult-imenu)
("C-c f f" . consult-find)
("C-c F" . consult-ripgrep)
("C-c f" . consult-find)
("C-c l" . consult-line)
("C-c L" . consult-focus-lines)
("C-c m" . consult-mark)
("C-c o o" . consult-outline)
("C-c e" . consult-flymake))
:custom
(xref-show-xrefs-function #'consult-xref)
(xref-show-definitions-function #'consult-xref))
(use-package orderless
:custom
(completion-styles '(orderless basic))
(completion-category-overrides '((file (styles basic partial-completion)))))
(use-package embark
:bind (("C-'" . embark-act)
("C-=" . embark-dwim)))
(use-package corfu
:config
(corfu-terminal-mode)
(corfu-popupinfo-mode)
(add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter)
:custom
(completion-cycle-threshold nil)
(tab-always-indent 'complete)
(kind-icon-default-face 'corfu-default)
:bind (:map corfu-map
(("M-d" . corfu-doc-toggle)
("M-l" . corfu-show-location)
("SPC" . corfu-insert-separator)))
:init
(global-corfu-mode))
(use-package cape
:config
(add-to-list 'completion-at-point-functions #'cape-dabbrev)
(add-to-list 'completion-at-point-functions #'cape-file))
(use-package prog-mode
:hook ((prog-mode . hl-line-mode)
(prog-mode . display-line-numbers-mode)))
(use-package which-key :delight :config
(which-key-mode)
(which-key-setup-side-window-right))
(use-package p-search)
(use-package magit
:bind (("C-c o g" . magit)))
(use-package magit-delta
:hook (magit-mode . magit-delta-mode))
(use-package magit-todos
:after magit
:custom (magit-todos-keyword-suffix "\\(?:([^)]+)\\)?:?")
:config (magit-todos-mode 1))
(use-package difftastic
:demand t
:bind (:map magit-blame-read-only-mode-map
("D" . difftastic-magit-show)
("S" . difftastic-magit-show))
:config
(eval-after-load 'magit-diff
'(transient-append-suffix 'magit-diff '(-1 -1)
[("D" "Difftastic diff (dwim)" difftastic-magit-diff)
("S" "Difftastic show" difftastic-magit-show)])))
;; FIXME there is something deeply wrong about how nix is configured here
;; (use-package nix-mode
;; :delight nix-prettify-mode
;; :config
;; (global-nix-prettify-mode))
(use-package agenix
:after envrc
:hook (agenix-pre-mode . envrc-mode))
(use-package nix-ts-mode
:hook (
(nix-ts-mode . (lambda ()
(require 'eglot)
(add-to-list 'eglot-server-programs
'(nix-ts-mode . ("nixd")))
(eglot-ensure)))
(nix-ts-mode . electric-pair-mode)
(nix-ts-mode . (lambda () (setq-local indent-bars-spacing-override 2) (indent-bars-mode)))
(nix-ts-mode . (lambda ()
(setq-local
treesit-font-lock-settings
(append treesit-font-lock-settings
(treesit-font-lock-rules
:language 'nix
:feature 'function
:override t
`((formal) @font-lock-type-face)
:language 'nix
:feature 'function
`((attrpath) @font-lock-function-name-face)
)))))
)
:mode "\\.nix\\'"
)
(use-package dockerfile-ts-mode
:mode "Dockerfile\\'")
(use-package lean4-mode
:mode "\\.lean\\'")
(use-package python-ts-mode
:hook ((python-ts-mode . (lambda ()
(require 'eglot)
(add-to-list 'eglot-server-programs
'(python-ts-mode . ("jedi-language-server")))
(eglot-ensure))))
:mode "\\.py\\'")
(use-package solidity-mode
:hook ((solidity-mode . (lambda ()
(require 'eglot)
(add-to-list 'eglot-server-programs
'(solidity-mode . ("nomicfoundation-solidity-language-server" "--stdio")))
(eglot-ensure))))
:mode "\\.sol\\'")
(use-package typescript-ts-mode
:hook ((typescript-ts-mode . (lambda ()
(require 'eglot)
(eglot-ensure))))
:mode "\\.ts\\'")
(use-package rust-mode
:init
(setq rust-mode-treesitter-derive t)
:hook ((rust-mode . (lambda ()
(require 'eglot)
(eglot-ensure)))))
(use-package haskell-ts-mode
:hook ((haskell--ts-mode . eglot-ensure))
:mode "\\.hs\\'"
:config
(add-to-list 'eglot-server-programs
'(haskell-ts-mode . ("haskell-language-server-wrapper" "--lsp"))))
(use-package purescript-mode
:custom ((project-vc-extra-root-markers '("spago.dhall")))
:hook ((purescript-mode . eglot-ensure)
(purescript-mode . turn-on-purescript-indentation)
(purescript-mode . (lambda () (setq project-vc-extra-root-markers '("spago.dhall"))))))
(use-package terraform-mode
:after eglot
:config
(add-to-list 'eglot-server-programs
'(terraform-mode . ("terraform-lsp")))
:hook ((terraform-mode . eglot-ensure)
;; (terraform-mode . tree-sitter-hl-mode)
(terraform-mode . (lambda () (setq indent-bars-spacing-override 2) (indent-bars-mode)))
))
(use-package yaml-mode
:hook (yaml-mode . yaml-ts-mode))
(use-package sh-mode
:hook (sh-mode . bash-ts-mode))
(use-package kdl-ts-mode
:mode "\\.kdl\\'")
(use-package gptscript-mode
:mode "\\.gpt\\'")
;; FIXME
;; (use-package mmm-mode
;; :config
;; (mmm-add-group 'nix-sh
;; '((sh-command
;; :submode sh-mode
;; :face mmm-output-submode-face
;; :front "[^'a-zA-Z]''[^']"
;; :back "''[^$\\']"
;; :include-front t
;; :front-offset 4
;; :end-not-begin t)))
;; (mmm-add-mode-ext-class 'nix-mode "\\.nix\\'" 'nix-sh))
(use-package paredit
:delight
:hook ((lisp-mode . enable-paredit-mode)
(emacs-lisp-mode . enable-paredit-mode)))
(use-package aggressive-indent
:hook ((lisp-mode . aggressive-indent-mode)
(emacs-lisp-mode . aggressive-indent-mode)))
(use-package eldoc
:delight)
(use-package eldoc-box
:after eglot
:custom
(eldoc-box-only-multiline nil)
(eldoc-box-lighter "ElBox")
:bind (("C-c h" . eldoc-box-help-at-point)))
(use-package diff-hl
:init
(global-diff-hl-mode 1)
(diff-hl-margin-mode 1))
(use-package envrc
:config
(envrc-global-mode +1))
(use-package hl-todo
:init
(global-hl-todo-mode))
(use-package eat
:init
;; FIXME if we not load eat on startup then adding more non bound keys in :config
;; will a cause "nesting exceeds `max-lisp-eval-depth'" on (eat-reload)
(eat)
:custom
(eat-kill-buffer-on-exit t)
:config
(add-to-list 'eat-semi-char-non-bound-keys '[?\e 104]) ; M-h
(add-to-list 'eat-semi-char-non-bound-keys '[?\e 106]) ; M-j
(add-to-list 'eat-semi-char-non-bound-keys '[?\e 107]) ; M-k
(add-to-list 'eat-semi-char-non-bound-keys '[?\e 108]) ; M-l
(eat-update-semi-char-mode-map)
(eat-reload)
:bind (("C-c o t" . eat-project))
;; FIXME doesn't work well
;; ((eat-mode . compilation-shell-minor-mode))
)
(use-package eshell
:init (require 'eshell) ;; this slows down Emacs startup but it's needed when starting eshell with
;; emacsclient --eval before opening another eshell buffer directly from inside Emacs
(eat-eshell-mode)
(eat-eshell-visual-command-mode)
:custom ((eshell-prefer-lisp-functions t)
(eshell-history-size 10000)
(eshell-banner-message ""))
:config
(defun ccr/start-eshell () ;; Used from outside Emacs by emacsclient --eval
(eshell 'N)
(add-hook 'kill-buffer-hook 'delete-frame nil 't)) ;; destroy frame on exit
;; Wrapping this in order to merge histories from different shells
;; (advice-add 'eshell-write-history
;; :around #'ccr/wrap-eshell-write-history)
(add-to-list 'eshell-modules-list 'eshell-tramp) ;; to use sudo in eshell
;; (add-to-list 'eshell-modules-list 'eshell-smart) ;; plan 9 style
(setq ccr/eshell-aliases
'((g . magit)
(gl . magit-log)
(d . dired)
(o . find-file)
(oo . find-file-other-window)
(l . (lambda () (eshell/ls '-la)))
(eshell/clear . eshell/clear-scrollback)))
(mapc (lambda (alias)
(defalias (car alias) (cdr alias)))
ccr/eshell-aliases)
(defun ccr/eshell-get-current-input ()
"Restituisce l'input corrente dell'utente nella riga Eshell."
(when (eq major-mode 'eshell-mode)
(let ((start (save-excursion (eshell-bol) (point)))
(end (point-at-eol)))
(buffer-substring-no-properties start end))))
(defun ccr/eshell-replace-current-input (new-input)
(when (eq major-mode 'eshell-mode)
(let ((inhibit-read-only t))
(eshell-bol)
(delete-region (point) (point-at-eol))
(insert new-input)
(end-of-line))))
(defun ccr/eshell-history ()
(interactive)
(when (eq major-mode 'eshell-mode)
(let* ((current-input (ccr/eshell-get-current-input))
(eshell-history (when (and eshell-history-file-name
(file-readable-p eshell-history-file-name))
(f-read-text eshell-history-file-name)))
(bash-history (when (file-readable-p "~/.bash_history")
(f-read-text "~/.bash_history")))
(history (split-string (concat (or eshell-history "") "\n"
(or bash-history "")) "\n" t))
(selection (completing-read "History: " history nil t current-input)))
(ccr/eshell-replace-current-input selection))))
:bind (("C-c o e" . project-eshell)
(:map eshell-mode-map
("C-r" . ccr/eshell-history)
("C-<return>" . corfu-send)
))) ;; i.e. just C-r in semi-char-mode
(use-package eshell-command-not-found
:custom ((eshell-command-not-found-command "command-not-found"))
:hook ((eshell-mode . eshell-command-not-found-mode)))
;; (use-package eshell-atuin
;; :hook ((eshell-mode . eshell-atuin-mode)))
(use-package eshell-syntax-highlighting
:custom
((eshell-syntax-highlighting-highlight-in-remote-dirs nil))
:config
(eshell-syntax-highlighting-global-mode +1))
(use-package eshell-prompt-extras
:custom ((eshell-highlight-prompt nil)
(eshell-prompt-function 'epe-theme-lambda)))
(use-package popper
:custom
(popper-reference-buffers
'("\*Messages\*"
"Output\*$"
"\\*Async Shell Command\\*"
(completion-list-mode . hide)
help-mode
compilation-mode
"^\\*Nix-REPL*\\*$" nix-repl-mode
"^\\*.+-eshell.*\\*$" eshell-mode
"^\\*shell.*\\*$" shell-mode
"^\\*term.*\\*$" term-mode
"^\\*eat.*\\*$" eat-mode
))
(popper-window-height 0.33)
(popper-echo-lines 1)
(popper-mode-line nil)
:init
(popper-mode 1)
(popper-echo-mode 1)
:bind (("C-c t t" . popper-toggle-latest)
("C-c t c" . popper-cycle)
("C-c t p" . popper-toggle-type)))
(use-package org
:hook ((org-mode . variable-pitch-mode)
(org-mode . visual-line-mode)
(org-mode . visual-fill-column-mode))
:custom ((org-log-done nil)
(org-return-follows-link t)
(org-hide-emphasis-markers t)
(visual-fill-column-center-text t)
(visual-fill-column-width 100)
(fill-column 100)
(org-capture-templates '(
("j" "Work Log Entry"
entry (file+datetree "~/org/work-log.org")
"* %?"
:empty-lines 0)
("n" "Note"
entry (file+headline "~/org/notes.org" "Random Notes")
"** %?"
:empty-lines 0)
))
(org-auto-align-tags nil)
(org-tags-column 0)
(org-catch-invisible-edits 'show-and-error)
(org-special-ctrl-a/e t)
(org-insert-heading-respect-content t)
(org-pretty-entities t)
(org-ellipsis "")
)
:bind (("C-c o l" . org-store-link)
("C-c o c" . org-capture)
("C-c b o" . org-switchb))
:config
(defun ccr/org-capture (key)
"Capture a note using the template KEY and close the frame when done.
This is meant to be an helper to be called from the window manager."
(interactive)
(org-capture nil key)
(add-hook 'kill-buffer-hook 'delete-frame nil 't) ;; destroy frame on exit
(delete-other-windows))
(dolist (face '(org-block-begin-line
org-block-end-line
org-verbatim
org-code
))
(set-face-attribute face nil :inherit 'fixed-pitch))
(org-babel-do-load-languages
'org-babel-load-languages '((haskell . t)))
(defun ccr/org-attach-save-file-list-to-property (dir)
"Save list of attachments to ORG_ATTACH_FILES property."
(when-let* ((files (org-attach-file-list dir)))
(org-set-property "ORG_ATTACH_FILES" (mapconcat #'identity files ", "))))
(add-hook 'org-attach-after-change-hook #'ccr/org-attach-save-file-list-to-property))
(use-package org-agenda
:after org-super-agenda
:custom
(org-agenda-files '("~/org"))
(org-agenda-tags-column 0)
(org-agenda-block-separator ?─)
(org-agenda-time-grid
'((daily today require-timed)
(800 1000 1200 1400 1600 1800 2000)
" ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄"))
(org-agenda-current-time-string
"◀── now ─────────────────────────────────────────────────")
(org-super-agenda-groups
'(;; Each group has an implicit boolean OR operator between its selectors.
(:name "Today" ; Optionally specify section name
:time-grid t ; Items that appear on the time grid
:todo "TODAY") ; Items that have this TODO keyword
(:name "Important"
;; Single arguments given alone
:tag "bills"
:priority "A")
;; Set order of multiple groups at once
(:order-multi (2 (:name "Shopping in town"
;; Boolean AND group matches items that match all subgroups
:and (:tag "shopping" :tag "@town"))
(:name "Food-related"
;; Multiple args given in list with implicit OR
:tag ("food" "dinner"))
(:name "Personal"
:habit t
:tag "personal")
(:name "Space-related (non-moon-or-planet-related)"
;; Regexps match case-insensitively on the entire entry
:and (:regexp ("space" "NASA")
;; Boolean NOT also has implicit OR between selectors
:not (:regexp "moon" :tag "planet")))))
;; Groups supply their own section names when none are given
(:todo "WAITING" :order 8) ; Set order of this section
(:todo ("SOMEDAY" "TO-READ" "CHECK" "TO-WATCH" "WATCHING")
;; Show this group at the end of the agenda (since it has the
;; highest number). If you specified this group last, items
;; with these todo keywords that e.g. have priority A would be
;; displayed in that group instead, because items are grouped
;; out in the order the groups are listed.
:order 9)
(:priority<= "B"
;; Show this section after "Today" and "Important", because
;; their order is unspecified, defaulting to 0. Sections
;; are displayed lowest-number-first.
:order 1)
;; After the last group, the agenda will display items that didn't
;; match any of these groups, with the default order position of 99
))
:bind (("C-c o a" . org-agenda))
:config
(org-super-agenda-mode)
)
(use-package org-modern
:after org
:init
(global-org-modern-mode)
;; FIXME the following doesn't work when using the daemon, it should be executed only
;; one time after the first frame is created
:hook (server-after-make-frame . (lambda ()
(set-face-font 'variable-pitch "Dejavu Serif-14")
(set-face-font 'fixed-pitch "Iosevka Comfy-14")
(set-face-font 'org-table "Iosevka Comfy-14")
(set-face-font 'org-meta-line "Iosevka Comfy-14")
(set-face-font 'org-drawer "Iosevka Comfy-14")
(set-face-font 'org-special-keyword "Iosevka Comfy-14")
(set-face-font 'org-property-value "Iosevka Comfy-14")
(set-face-font 'org-block "Iosevka Comfy-14")
(set-face-font 'org-modern-tag "Iosevka Comfy-14")
(set-face-font 'org-modern-date-active "Iosevka Comfy-14")
(set-face-font 'org-modern-date-inactive "Iosevka Comfy-14")))
)
(use-package org-roam
:init
(require 'org)
(require 'org-roam)
(require 'org-roam-dailies)
(require 'org-protocol)
(require 'org-roam-protocol)
:custom
(org-roam-v2-ack t)
(org-roam-directory (file-truename "~/org"))
(org-roam-complete-everywhere 't)
(org-roam-dailies-capture-templates
'(
("d" "Generic entry" entry
"* %?"
:target (file+head "%<%Y-%m-%d>.org" "#+TITLE: %<%Y-%m-%d>"))
("b" "Billable entry" entry
"* TODO ${Entry} :billable:${Client}:\n:PROPERTIES:\n:SPENT: ${Spent}\n:END:\n%?"
:target (file+head "%<%Y-%m-%d>.org" "#+TITLE: %<%Y-%m-%d>")
:create-id t)
)
)
(org-roam-capture-ref-templates
'(
("r" "Web entry" entry
"* ${Title} \n:PROPERTIES:\n:URL: ${ref}\n:END:\n %i"
:target (file+head "daily/%<%Y-%m-%d>.org" "#+TITLE: %<%Y-%m-%d>")
:create-id t)
)
)
:config
(org-roam-db-autosync-mode)
;; In order to automatically add an org id for some capture templates (using the :create-id keyword)
(defun ccr/org-capture-maybe-create-id ()
(when (org-capture-get :create-id)
(org-id-get-create)))
(add-hook 'org-capture-mode-hook #'ccr/org-capture-maybe-create-id)
;; The following functions name are relevant because org-roam-ql columns in queries use their suffix
(defun org-roam-node-spent (node)
"Return the hours spent as number"
(string-to-number (cdr (assoc "SPENT" (org-roam-node-properties node)))))
(defun org-roam-node-date (node)
"Return the org datestring when a node was created (obtained from the filename)"
(format "<%s>" (file-name-sans-extension (file-name-nondirectory (org-roam-node-file node)))))
(org-roam-ql-defpred
'date-range
"Check if node was created in given time range"
#'org-roam-node-date
#'(lambda (node-date start-date end-date)
(let ((node-date (condition-case nil
;; if the entry is not from the journal (i.e. the filename is not something like "2024-10-10.org")
;; then it's always discarded (the epoch time is given to it)
(encode-time (org-parse-time-string node-date))
(error (encode-time (org-parse-time-string "<1970-01-01>")))))
(start-date (encode-time (org-parse-time-string start-date)))
(end-date (encode-time (org-parse-time-string end-date))))
(and (time-less-p start-date node-date)
(time-less-p node-date end-date)))
))
(defun ccr/org-roam-spent-hours (client &optional date-start date-end)
"Return the total spent hours on something (usually a client)"
(let* ((query-tags `(tags ,client "billable"))
(query (if (and date-start date-end)
`(and ,query-tags (date-range ,date-start ,date-end))
query-tags)))
(apply #'+(mapcar #'org-roam-node-spent (org-roam-ql-nodes query))))))
(use-package org-roam-ql
:after org-roam
:bind ((:map org-roam-mode-map
("v" . org-roam-ql-buffer-dispatch)
:map minibuffer-mode-map
("C-c n i" . org-roam-ql-insert-node-title))))
(use-package consult-org-roam
:delight
:after org-roam
:init
(require 'consult-org-roam)
;; Activate the minor mode
(consult-org-roam-mode 1)
:custom
(consutl-org-roam-grep-func #'consult-ripgrep)
(consult-org-roam-buffer-narrow-key ?r)
(consult-org-roam-buffer-after-buffers t)
(setq org-roam-database-connector 'sqlite-builtin)
:config
(consult-customize
consult-org-roam-forward-links
:preview-key (kbd "M-."))
:bind
("C-c n f" . consult-org-roam-file-find)
("C-c n b" . consult-org-roam-backlinks)
("C-c n l" . consult-org-roam-forward-links)
("C-c n s" . consult-org-roam-search))
(use-package org-roam-ui
:after org-roam
:hook (after-init . org-roam-ui-mode) ;; don't care about startup time since I'm using Emacs daemonized
:config
(setq org-roam-ui-sync-theme t
org-roam-ui-follow t
org-roam-ui-update-on-save t
org-roam-ui-open-on-start nil))
(use-package gptel
:custom
(gptel-model 'google/gemini-2.5-flash-preview)
(gptel-backend (gptel-make-openai "OpenRouter"
:host "openrouter.ai"
:endpoint "/api/v1/chat/completions"
:key (lambda () (require 'f) (f-read-text (getenv "OPENAI_API_KEY_PATH")))
:stream t
:models '(google/gemini-2.5-flash-preview))
)
(gptel-default-mode 'org-mode)
(gptel-org-branching-context nil) ;; this is cool but I don't feel comfortable with it
(gptel-use-tools nil)
:bind
("C-c a a" . gptel-add)
("C-c a f" . gptel-add-file)
("C-c a r" . gptel-context-remove-all)
("C-c a " . gptel-menu)
:config
(require 'gptel-curl)
;; (add-hook 'gptel-post-response-functions 'gptel-end-of-response)
;; (add-hook 'gptel-post-stream-hook 'gptel-auto-scroll)
(setq gptel-tools (mapcar (lambda (tool) (apply #'gptel-make-tool tool))
'((
:name "create_file"
:function (lambda (path filename content)
(let ((full-path (expand-file-name filename path)))
(with-temp-buffer
(insert content)
(write-file full-path))
(format-read "Created file %s in %s" filename path)))
:description "Create a new file with the specified content"
:args (list '(:name "path"
:type string
:description "The directory where to create the file")
'(:name "filename"
:type string
:description "The name of the file to create")
'(:name "content"
:type string
:description "The content to write to the file"))
:category "filesystem"
)
;; (
;; :name "run_command"
;; :confirm 't
;; :function (lambda (command)
;; (shell-command-to-string command))
;; :description "Run arbitrary commands"
;; :args (list '(:name "command"
;; :type string
;; :description "The content to run e.g. 'ls *' or 'fd <pattern> <path>'"))
;; )
(
:name "get_weather"
:function (lambda (location)
(url-retrieve-synchronously "api.weather.com/..."
location unit))
:description "Get the current weather in a given location"
:args (list '(:name "location"
:type string
:description "The city and state, e.g. San Francisco, CA")
'(:name "unit"
:type string
:enum ["celsius" "farenheit"]
:description
"The unit of temperature, either 'celsius' or 'fahrenheit"
:optional t
))
)
))
)
(defun ccr/suggest-eshell-command ()
(interactive)
(save-excursion
(eshell-bol)
(let ((start-pos (point))
(end-pos (line-end-position)))
(gptel-request
(buffer-substring-no-properties start-pos end-pos) ;the prompt
:system "You are proficient with emacs shell (eshell), translate the following to something I could directly prompt to the shell. Your responses should only be code, without explanation or formatting or quoting."
:buffer (current-buffer)
:context (cons (set-marker (make-marker) start-pos)
(set-marker (make-marker) end-pos))
:callback
(lambda (response info)
(if (not response)
(message "ChatGPT response failed with: %s" (plist-get info :status))
(kill-region start-pos end-pos)
(insert response)))))))
(add-to-list 'display-buffer-alist
'("^\\*ChatGPT\\*"
(display-buffer-full-frame)
(name . "floating")))
(defun ccr/start-chatgpt () ;; Used from outside Emacs by emacsclient --eval
(display-buffer (gptel "*ChatGPT*"))
(set-frame-name "floating")
)
)
(use-package mixed-pitch
:hook (text-mode . mixed-pitch-mode))
(use-package pass
:config
(require 'password-store-otp) ;; FIXME use `use-pacakge' idiomatic way
:bind (("C-c p p" . password-store-copy)
("C-c p o" . password-store-otp-token-copy)
("C-c p e" . password-store-edit)
("C-c p i" . password-store-insert)))
(use-package with-editor
:init (shell-command-with-editor-mode +1))
(use-package go-translate
:custom
(gts-translate-list '(("it" "en") ("en" "it")))
(gts-default-translator
(gts-translator
:picker (gts-prompt-picker)
:engines `(,(gts-bing-engine)
,(gts-google-engine :parser (gts-google-summary-parser)))
:render (gts-buffer-render)))
(gts-buffer-follow-p 't)
:bind (("C-c T t" . gts-do-translate)))
(use-package message
:custom
(message-send-mail-function 'smtpmail-send-it)
(send-mail-function 'smtpmail-send-it)
(user-mail-address "andrea.ciceri@autistici.org")
(smtpmail-smtp-server "mail.autistici.org")
(smtpmail-stream-type 'ssl)
(smtpmail-smtp-service 465)
;; also the following line is needeed in ~/.authinfo.gpg
;; machine mail.autistici.org login andrea.ciceri@autistici.org password <password>
)
(use-package notmuch
:custom
(notmuch-show-logo nil)
(send-mail-function 'sendmail-send-it)
(notmuch-archive-tags '("-new"))
(notmuch-saved-searches
'((:name "Inbox" :query "tag:new" :key "i")
(:name "Sent" :query "tag:sent" :key "s")
(:name "Draft" :query "tag:draft" :key "s")
(:name "GitHub" :query "tag:github" :key "g")
(:name "Trash" :query "tag:trash" :key "t"))))
;;; Experiments, remove from here
(defun ccr/test (niri-socket)
"Select a window and focus it based on `niri msg` output."
(interactive)
(let* ((niri-output (ccr/niri-get-windows niri-socket))
(display-list (mapcar (lambda (entry)
(let ((title (cdr (assoc 'title entry)))
(app-id (cdr (assoc 'app_id entry)))
(id (cdr (assoc 'id entry))))
(cons (format "%s - %s" title app-id) id)))
niri-output)))
(with-selected-frame
(make-frame '((name . "Emacs Selector")
(minibuffer . only)
(fullscreen . 0)
(undecorated . t)
(internal-border-width . 10)
(width . 120)
(height . 20)))
(unwind-protect
(let* ((entry (completing-read "Select window: " (mapcar #'car display-list) nil t ""))
(entry-id (cdr (assoc entry display-list))) ;; Get the ID associated with the selected entry
(command (format "NIRI_SOCKET=\"%s\" niri msg action focus-window --id %s" niri-socket entry-id)))
(message command)
(shell-command command))
(delete-frame)))))
(defun ccr/niri-get-windows (niri-socket)
"Esegue `niri msg --json windows` e parse l'output JSON in una alist."
(let* ((command (format "NIRI_SOCKET=\"%s\" niri msg --json windows" niri-socket))
(output (shell-command-to-string command))
(json-object-type 'alist) ; Usa alist per rappresentare gli oggetti JSON
(parsed-json (json-read-from-string output)))
parsed-json))
(provide 'init)
;;; init.el ends here