;; init.el --- initialize emacs configuration -*- lexical-binding: t; -*- ;;; Author: Benj Bellon ;;; Maintainer: Benj Bellon ;;; Homepage: https://code.rse8.com/emacs.git ;;; Keywords: emacs, emacs.d, config ;;; Commentary: ;;; A few things of interest: ;;; 1. C-x C-c is overriden in the GUI so it redirects to the default ;;; *shell* buffer. If you wish for this override while using ;;; Emacs in terminal mode, provide the following ENV variable: ;;; EMACS_OVERRIDE_C_X_C_C ;;; ;;; 2. init.el calls keychain-refresh-environment. If for some reason, ;;; you do not wish for the following to be available to Emacs, add ;;; a comment or remove the call: ;;; SSH_AUTH_SOCK ;;; SSH_AGENT_PID ;;; GPG_AGENT_INFO ;;; ;;; Code: (setq inhibit-startup-message t) (setq initial-scratch-message nil) (require 'package) (package-initialize) (add-to-list 'package-archives '("gnu" . "https://elpa.gnu.org/packages/") t) (add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t) (add-to-list 'package-archives '("melpa-stable" . "http://stable.melpa.org/packages/") t) (when (not (package-installed-p 'use-package)) (package-install 'use-package)) (use-package solarized-theme :ensure t :config (load-theme 'solarized-dark t)) (use-package emacs :after solarized-theme :init (setq is-mac (equal system-type 'darwin)) (setq enable-recursive-minibuffers t) (set-face-attribute 'default nil :font "Source Code Pro Medium" :height 90) ;; 2. Set the fallback font for general symbols (set-fontset-font t 'symbol (font-spec :family "JetBrainsMono Nerd Font") nil 'append) ;; 3. Explicitly map the Unicode Private Use Area (where Nerd Font icons live) ;; Range #xE000 to #xF8FF covers Font Awesome, Material, etc. (set-fontset-font t '(#xE000 . #xF8FF) (font-spec :family "JetBrainsMono Nerd Font") nil 'append) (setq create-lockfiles nil) (setq backup-directory-alist `(("." . ,(expand-file-name (concat user-emacs-directory "backups"))))) (setq auto-save-file-name-transforms `((".*" ,temporary-file-directory t))) ;; Custom load paths, files, and directories (setq custom-file (expand-file-name "custom.el" user-emacs-directory)) (setq templates-dir (expand-file-name "templates" user-emacs-directory)) (setq org-dir (expand-file-name "~/.org")) (setq org-dir-journal (expand-file-name "journal" org-dir)) ;; get rid of the useles default connection string (setq sql-postgres-login-params nil) (add-to-list 'load-path templates-dir) (if (file-exists-p custom-file) (load custom-file) (message "custom.el does not yet exist, cannot load")) ;; Can't fix everything for everyone... (setq byte-compile-warnings '(not obsolete)) (setq warning-suppress-log-types '((comp) (bytecomp))) (setq native-comp-async-report-warnings-errors 'silent) (setq x-select-enable-clipboard t) (setq dired-listing-switches "-lisah") (setq hl-line-mode 0) (setq indent-tabs-mode nil) (when (fboundp 'menu-bar-mode) (menu-bar-mode -1)) (when (fboundp 'tool-bar-mode) (tool-bar-mode -1)) (when (fboundp 'scroll-bar-mode) (scroll-bar-mode -1)) (defalias 'yes-or-no-p 'y-or-n-p) :bind (("C-x C-b" . 'ibuffer) ("C--" . 'undo) ("C-x C-d" . 'find-file) ("C-x r m" . 'bookmark-set) ("C-x r d" . 'bookmark-delete) ("C-l" . 'comint-clear-buffer)) :config ;; Override C-x C-c to open the default shell buffer (if (display-graphic-p) (let ((shell-buffer "*shell*") (quit-command "C-x C-c")) (if (not (get-buffer shell-buffer)) (shell)) (define-key global-map (kbd quit-command) (lambda () (interactive) (delete-other-windows) (switch-to-buffer "*shell*")))))) (use-package ace-window :ensure t :init (setq aw-keys '(?a ?s ?d ?e ?f ?g ?h ?j ?k ?l)) :bind ("C-x o" . 'ace-window)) (use-package agent-shell :ensure t :bind ("C-c a a" . 'agent-shell)) (use-package apheleia :ensure t :config (setf (alist-get 'prettier apheleia-formatters) '("apheleia-npx" "prettier" "--stdin-filepath" filepath))) (use-package avy :ensure t :init :bind (("M-g a a" . 'avy-goto-char-timer) ("M-g a l" . 'avy-goto-line))) (use-package cal-invite :after (:all mu4e org-msg) :load-path "~/workspace/cal-invite" :config (require 'cal-invite)) (use-package cargo-mode :ensure t) (use-package cmake-mode :ensure t :mode "\\CMakeLists\\.txt\\'") (use-package company :ensure t :init (setq company-idle-delay 0.1) (setq company-minimum-prefix-length 1) :config (add-to-list 'company-backends 'company-capf) (global-company-mode)) (use-package consult :ensure t :bind ((:prefix-map consult-commands :prefix-docstring "Consult mode quick commands" :prefix "C-c c" ("b" . 'consult-bookmark) ("f" . 'consult-ripgrep) ("l" . 'consult-focus-lines) ("o" . 'consult-outline) ("r l" . 'consult-register-load) ("r s" . 'consult-register-store) ("r w" . 'consult-register))) :bind (("C-x b" . 'consult-buffer) ("M-g M-g" . 'consult-goto-line)) :hook (completion-list-mode . consult-preview-at-point-mode)) (use-package dap-mode :bind (("C-c d" . dap-commands)) :pin melpa :ensure t :config (require 'dap-lldb) (setq dap-lldb-debug-program '("/usr/bin/lldb-dap")) (transient-define-prefix dap-commands () "DAP mode commands" [["Commands" ("b" "Toggle Breakpoint" dap-breakpoint-toggle) ("c" "Continue" dap-continue) ("d" "Start debugger" dap-debug) ("i" "Step In" dap-step-in) ("n" "Next" dap-next) ("o" "Step Out" dap-step-out) ("s" "Stop" dap-stop-thread) ("u" "DAP UI commands" dap-ui-commands)]]) (transient-define-prefix dap-ui-commands () "DAP UI commands" [["Commands" ("B" "Breakpoints window" dap-ui-breakpoints) ("b" "Breakpoints" dap-ui-breakpoints-list) ("l" "Locals window" dap-ui-locals) ("r" "Repl window" dap-ui-repl) ("s" "Sessions window" dap-ui-sessions) ("W" "Show all windows" dap-ui-show-many-windows) ("w" "Hide all windows" dap-ui-hide-many-windows)]])) (use-package elfeed :ensure t :config (setq elfeed-db-directory "~/.emacs.d/elfeed/db")) (use-package elfeed-org :ensure t :config (setq rmh-elfeed-org-files (list "~/.emacs.d/elfeed/feed.org")) (elfeed-org)) (use-package emacs-lisp-mode :mode "\\.dir-locals\\.el\\'") (use-package embark :ensure t :bind (("C-." . embark-act))) (use-package embark-consult :ensure t :hook (embark-collect-mode . consult-preview-at-point-mode)) (use-package git-timemachine :ensure t) (use-package go-mode :after lsp-mode :ensure t :hook ((go-mode . lsp-mode) (python-mode . lsp) (c-mode . lsp) (before-save . lsp-format-buffer) (before-save . lsp-organize-imports))) (use-package htmlize :ensure t) (use-package just-mode :ensure t) (use-package keychain-environment :ensure t :config (keychain-refresh-environment)) (use-package fountain-mode :ensure t :mode "\\.fountain?\\'" :config :hook (fountain-mode . olivetti-mode)) (use-package flycheck :ensure t) (use-package gptel :ensure t :after (password-store) :bind (:prefix-map ai-commands :prefix-docstring "Emacs AI comamands" :prefix "C-c a g" ("b" . 'gptel) ("m" . 'gptel-menu) ("q" . 'gptel-abort) ("RET" . 'gptel-send) ("ESC" . 'gptel-abort)) :custom (gptel-api-key (auth-source-pass-get 'secret "openai.com/api.openai.com/apikey")) (gptel-default-mode #'org-mode) :config (gptel-make-anthropic "anthropic" :stream t :key (auth-source-pass-get 'secret "anthropic.com/api.anthropic.com/apikey")) (gptel-make-deepseek "deepseek" :stream t :key (auth-source-pass-get 'secret "deepseek.com/api.deepseek.com/apikey")) (gptel-make-openai "DInference" :host "api.dinference.com" :stream t :key (auth-source-pass-get 'secret "dinference.com/api.dinference.com/apikey") :models '(gpt-oss-120b gpt-oss-20b glm-5 glm-4.7)) (gptel-make-gemini "gemini" :stream t :key (auth-source-pass-get 'secret "google.com/aiplatform.googleapis.com/gemini/apikey")) (gptel-make-xai "xAI" :stream t :key (auth-source-pass-get 'secret "x.ai/api.x.ai/apikey"))) (use-package glsl-mode :ensure t) (use-package hl-todo :ensure t :init (global-hl-todo-mode)) (use-package jinx :ensure t :hook ((text-mode . jinx-mode) (org-mode . jinx-mode)) :bind (("" . 'jinx-correct) ("M-$" . 'jinx-correct) ("C-M-$" . 'jinx-languages))) (use-package ledger-mode :mode ("\\.ledger\\'" "\\.hledger\\'") :custom (ledger-binary-path "hledger") (ledger-mode-should-check-version nil) (ledger-report-use-native-highlighting nil) (ledger-report-auto-width nil) (ledger-report-links-in-register nil) (ledger-reports '(("bal" "%(binary) -f %(ledger-file) bal --tree") ("bs" "%(binary) -f %(ledger-file) bs --tree") ("is" "%(binary) -f %(ledger-file) is --tree") ("is-monthly" "%(binary) -f %(ledger-file) is -M --tree") ("cf" "%(binary) -f %(ledger-file) cf") ("reg" "%(binary) -f %(ledger-file) reg") ("expenses" "%(binary) -f %(ledger-file) is --tree --depth 3") ("account" "%(binary) -f %(ledger-file) reg %(account)") ("payee" "%(binary) -f %(ledger-file) reg @%(payee)")))) (use-package lsp-mode :ensure t :commands lsp :bind-keymap ("C-c l" . lsp-command-map) :hook (c-mode . lsp)) (use-package magit :ensure t :bind ("C-x g" . 'magit-status) :init (setq ediff-split-window-function #'split-window-horizontally ediff-window-setup-function #'ediff-setup-windows-plain ediff-scroll-vertically t ediff-scroll-horizontally t)) (use-package marginalia :ensure t :bind (:map minibuffer-local-map ("M-A" . 'marginalia-cycle)) :init (marginalia-mode)) (use-package multiple-cursors :ensure t :init (setq mc/always-run-for-all t) :bind (:prefix-map mc-commands :prefix-docstring "Multiple cursors quick commands" :prefix "C-c m" ("i" . 'mc/insert-numbers) ("l" . 'mc/edit-lines) ("m" . 'mc/mark-all-like-this) ("n" . 'mc/mark-next-like-this) ("p" . 'mc/mark-previous-like-this) ("r r" . 'mc/mark-all-in-region-regexp))) (use-package mu4e :ensure nil :bind (("C-c M m" . mu4e) ("C-c M i c" . cal-invite-create) ("C-c M i u" . cal-invite-update) ("C-c M i x" . cal-invite-cancel) ("C-c M i v" . cal-invite-view) ("C-x m" . mu4e-compose-new)) :hook (mu4e-compose-mode . (lambda () (use-hard-newlines -1))) :init (setq mail-user-agent 'mu4e-user-agent) :config (defun my/mu4e-refile-folder-skip-sent (msg) "Refile to Archive, but keep sent messages in place." (let ((maildir (mu4e-message-field msg :maildir))) (if (string-match-p "/Sent\\'" maildir) maildir (let ((account (replace-regexp-in-string "^/\\([^/]+\\)/.*" "\\1" maildir))) (concat "/" account "/Archive"))))) (defun my/email-to-display-name (email) "Derive a display name from EMAIL, e.g. jane.doe@x.com -> Jane Doe." (mapconcat #'capitalize (split-string (car (split-string email "@")) "[._-]") " ")) ;; (setq mu4e-compose-format-flowed t) ;; this handled by org-msg (setq message-citation-line-function 'message-insert-formatted-citation-line) (setq message-citation-line-format "On %a, %d %b %Y at %R, %f wrote:\n") (setq mu4e-maildir "~/.mail" mu4e-get-mail-command "mbsync -a" mu4e-update-interval 300 mu4e-change-filenames-when-moving t mu4e-attachment-dir "~/Downloads/" mu4e-context-policy 'pick-first mu4e-compose-context-policy 'ask-if-none) (setq message-send-mail-function 'message-send-mail-with-sendmail sendmail-program "/usr/bin/msmtp" message-sendmail-extra-arguments '("--read-envelope-from") message-sendmail-f-is-evil t) (defun my/route-localhost-mail () "Force msmtp to use smtp4dev if emailing a localhost address." (let ((to-field (message-field-value "To"))) (when (and to-field (string-match-p "@localhost" to-field)) (setq-local message-sendmail-extra-arguments '("-a" "smtp4dev"))))) (add-hook 'message-send-hook 'my/route-localhost-mail) (defvar my/mu4e-account-maildirs '((mu4e-drafts-folder . "Drafts") (mu4e-sent-folder . "Sent") (mu4e-trash-folder . "Trash")) "Common subfolder names shared across all accounts.") (setq mu4e-refile-folder #'my/mu4e-refile-folder-skip-sent) (defvar my/mu4e-account-shortcuts '(("Inbox" . ?i) ("Archive" . ?a) ("Sent" . ?s) ("Trash" . ?t)) "Common shortcut folders shared across all accounts.") (defun my/mu4e-discover-accounts () "Scan ~/.mail for directories and generate mu4e contexts." (let ((accounts (directory-files mu4e-maildir nil "^[^.]"))) (seq-map-indexed (lambda (account idx) (make-mu4e-context :name (format "%d-%s" (1+ idx) account) :match-func (lambda (msg) (when msg (string-prefix-p (concat "/" account) (mu4e-message-field msg :maildir)))) :vars (append `((user-mail-address . ,account) (user-full-name . ,(my/email-to-display-name account))) (mapcar (lambda (pair) (cons (car pair) (concat "/" account "/" (cdr pair)))) my/mu4e-account-maildirs) `((mu4e-maildir-shortcuts . ,(mapcar (lambda (pair) (cons (concat "/" account "/" (car pair)) (cdr pair))) my/mu4e-account-shortcuts)))))) accounts))) (setq mu4e-contexts (my/mu4e-discover-accounts))) (use-package nerd-icons :ensure t :after marginalia :hook (marginalia . nerd-icons-completion-marginalia-setup) :custom (nerd-icons-font-family "Symbols Nerd Font Mono")) (use-package nerd-icons-completion :ensure t :config (nerd-icons-completion-mode)) (use-package nerd-icons-dired :ensure t :hook (dired-mode . nerd-icons-dired-mode)) (use-package nginx-mode :ensure t) (use-package ob-duckdb :ensure t :after org :config (org-babel-do-load-languages 'org-babel-load-languages (append org-babel-load-languages '((duckdb . t))))) (use-package ob-restclient :ensure t) (use-package olivetti :ensure t :custom (olivetti-body-width 250) :mode (("\\.txt?\\'" . olivetti-mode) ("\\README.md\\'" . olivetti-mode) ("\\.org?\\'" . olivetti-mode))) (use-package orderless :ensure t :init (setq orderless-matching-styles '(orderless-flex)) :custom (completion-styles '(orderless basic)) (completion-category-overrides'((file (styles basic partial-completion))))) (use-package org-ai :after password-store :ensure t :bind (("C-c o o" . 'org-ai-prompt)) :init (org-ai-global-mode) (setq org-ai-openai-api-token (password-store-get "openai.com/api.openai.com/apikey") org-ai-default-chat-model "gpt-4o")) (use-package org-journal :ensure t :init (setq org-journal-dir org-dir-journal) (setq org-journal-file-type 'monthly) (setq org-journal-file-format "%Y-%m.org") (setq org-journal-enable-agenda-integration nil)) (use-package org-msg :ensure t :after (:all mu4e org) :hook ((message-mode . org-msg-mode) (mu4e-compose-mode . org-msg-mode) (org-msg-edit-mode . my/org-msg-hexo-ish-css)) :config (defun my/org-msg-hexo-ish-css () "Use a Hexo-ish CSS file for org-msg emails only." (setq-local org-msg-enforce-css (expand-file-name "org-msg-style.css" user-emacs-directory))) (setq org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil tex:dvipng \\n:t" org-msg-startup "hidestars indent inlineimages" org-msg-greeting-fmt "\n" org-msg-greeting-name-limit 3 org-msg-default-alternatives '((new . (text html)) (reply-to-html . (text html)) (reply-to-text . (text))) org-msg-convert-citation t org-msg-signature "")) (use-package org-mode :after (:all org-ai org-journal ob-restclient) :mode "\\.org$" :bind (:prefix-map org-commands :prefix-docstring "Org mode quick commands" :prefix "C-c o" ("j c" . org-journal-open-current-journal-file) ("j n" . org-journal-new-entry) ("p n" . org-capture) ;; open plan.org ("p o" . (lambda () (interactive) (find-file (expand-file-name "planner/plan.org" org-dir)))) ;; bulk archive ("p a" . (lambda () (interactive) (org-map-entries #'org-archive-subtree "/DONE|CANCELLED" 'file))) ("a" . org-agenda)) :hook ((org-mode . hl-todo-mode) (org-mode . olivetti-mode) (org-mode . org-ai-mode) (org-mode . org-indent-mode) (after-save . my/org-git-autocommit-after-save)) :preface (defun my/org-agenda-workspace-files () "Return all plan.org files under ~/workspace, skipping common generated dirs." (let ((root (expand-file-name "~/workspace"))) (seq-filter (lambda (f) (not (string-match-p (rx "/" (or ".git" "node_modules" ".direnv" ".venv" "dist" "build" "target" ".cache") "/") f))) (directory-files-recursively root "\\`plan\\.org\\'")))) :init (org-babel-do-load-languages 'org-babel-load-languages '((emacs-lisp . t) (python . t) (restclient . t) (haskell . t) (shell . t) (sql . t))) (setq org-ai-image-directory (expand-file-name "org-ai-images/" org-dir)) (defun my/org-git-autocommit-after-save () "Auto-commit the current file if it lives under ~/.org/." (when (and buffer-file-name (not (file-remote-p buffer-file-name)) (string-prefix-p (file-name-as-directory (expand-file-name "~/.org/")) (expand-file-name buffer-file-name))) (let* ((default-directory (expand-file-name "~/.org/")) (rel (file-relative-name buffer-file-name default-directory)) (msg (format "autocommit on save [%s]" rel))) (call-process "git" nil nil nil "add" "--" rel) (call-process "git" nil nil nil "commit" "-m" msg "--" rel)))) :custom (org-html-htmlize-output-type 'css) ;; Planner Files (org-default-notes-file (expand-file-name "planner/plan.org" org-dir)) (org-agenda-files (cons (expand-file-name "planner/plan.org" org-dir) (my/org-agenda-workspace-files))) ;; Capture templates (org-capture-templates `(("t" "Todo" entry (file+headline ,(expand-file-name "planner/plan.org" org-dir) "Inbox") "* TODO %?\n:PROPERTIES:\n:CREATED: %U\n:END:\n" :empty-lines 1))) ;; Refile targets (org-refile-targets `((,(expand-file-name "planner/plan.org" org-dir) :maxlevel . 2))) (org-refile-use-outline-path t) (org-outline-path-complete-in-steps nil) (org-refile-allow-creating-parent-nodes 'confirm) ;; Archive location (year-based) (org-archive-location (concat (expand-file-name "planner/archive/" org-dir) (format-time-string "%Y") ".org::")) ;; Planner states (org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "WAIT(w@)" "PROJECT(p)" "SOMEDAY(s)" "|" "DONE(d!)" "CANCELLED(c@)"))) (org-agenda-custom-commands '(("g" "GTD" ((agenda "" ((org-agenda-span 7))) (todo "NEXT" ((org-agenda-overriding-header "Next Actions"))) (todo "WAIT" ((org-agenda-overriding-header "Waiting For"))) (todo "PROJECT" ((org-agenda-overriding-header "Projects"))) (todo "SOMEDAY" ((org-agenda-overriding-header "Someday / Maybe"))))))) ;; Babel (org-confirm-babel-evaluate nil) ;; Display settings (org-auto-align-tags t) (org-hidden-keywords '(title author date startup)) (org-startup-folded nil) (org-hide-leading-stars t) (org-hide-emphasis-markers t) (org-num-face nil) (org-level-color-stars-only nil) :custom-face (org-level-1 ((t (:family "Roboto" :height 120)))) (org-level-2 ((t (:family "Roboto" :height 110)))) (org-level-3 ((t (:family "Roboto" :height 100)))) (org-document-title ((t (:family "Roboto Slab" :height 220 :weight medium)))) (org-document-info ((t (:family "Roboto Slab" :height 110 :weight medium))))) (use-package org-superstar :ensure t :after org :hook (org-mode . org-superstar-mode) :init (setq org-superstar-headline-bullets-list '("◉" "○" "◉" "○" "◉")) (setq org-superstar-special-todo-items t) (setq org-superstar-prettify-item-bullets t) (setq org-superstar-item-bullet-alist '((?* . ?*) (?+ . ?◦) (?- . ?•)))) (use-package ox-hugo :ensure t :after ox) (use-package pass :ensure t) (use-package paredit :ensure t :hook (lisp-mode . paredit-mode) (emacs-lisp-mode . paredit-mode) (scheme-mode . paredit-mode)) (use-package password-store :ensure t :config ;; don't append, since we /only/ want password-store based credentials (setq auth-sources '(password-store))) (use-package pest-mode :ensure t) (use-package projectile :ensure t :init (projectile-mode +1) :bind (("C-c p" . 'projectile-command-map) ("C-c p s g" . 'projectile-ripgrep) ("C-c p s r" . 'projectile-replace-regexp))) (use-package projectile-ripgrep :ensure t) (use-package protobuf-mode :ensure t) (use-package pyvenv :ensure t) (use-package restclient :ensure t) (use-package rmsbolt :ensure t) (use-package rustic :ensure t) (use-package swift-mode :ensure t) (use-package treemacs :ensure t :after transient :bind (("C-c T" . treemacs-dispatch)) :config (transient-define-prefix treemacs-dispatch () "Treemacs command list" [["Commands" ("t" "Toggle treemacs" treemacs) ("s" "Select treemacs window" treemacs-select-window) ("f" "Find file in treemacs" treemacs-find-file)]])) (use-package treemacs-nerd-icons :ensure t :config (treemacs-load-theme "nerd-icons")) (use-package undo-tree :ensure t) (use-package vertico :ensure t :init (defun my/go-home () (interactive) (cond ((looking-back "/") (insert "~/")) (:else (insert "~")))) (vertico-mode) :custom (vertico-count 25) (vertico-resize t) :bind (:map vertico-map ("~" . 'my/go-home))) (use-package vertico-directory :after vertico :ensure nil :bind (:map vertico-map ("C-d" . 'vertico-exit) ("DEL" . 'vertico-directory-delete-char)) :hook (rfn-eshadow-update-overlay . vertico-directory-tidy)) (use-package uuidgen :ensure t) (use-package visual-regexp :ensure t :bind (("C-s" . 'vr/isearch-forward) ("C-r" . 'vr/isearch-backward) ("C-M-r" . 'vr/replace))) (use-package visual-regexp-steroids :after visual-regexp :ensure t) (use-package web-mode :ensure t :mode (("\\.svelte?\\'" . web-mode) ("\\.html?\\'" . web-mode) ("\\.js?\\'" . web-mode) ("\\.ts?\\'" . web-mode)) :config (setq web-mode-engines-alist '(("svelte" . ".svelte$") ("django" . "\\.html"))) :custom (web-mode-code-indent-offset 2) (web-mode-css-indent-offset 2) (web-mode-markup-indent-offset 2) (web-mode-script-padding 2) (web-mode-style-padding 2) (web-mode-block-padding 2) (apheleia-mode t) :hook ((web-mode . lsp) (web-mode . apheleia-mode) (web-mode . (lambda () (setq-local lsp-format-on-save nil))))) (use-package which-key :ensure t :config (which-key-mode 1)) (use-package ws-butler :ensure t :after lsp-mode :hook (lsp-mode . ws-butler-mode)) (use-package yaml-mode :ensure t) (use-package yasnippet :ensure t ;; :bind (("" . 'yas-expand)) :config (yas-global-mode t))