summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-12 00:30:11 +0200
committercashmere <cashmere@autistici.org>2026-09-12 00:30:11 +0200
commit32329d9a384642dd231ed1f5a16aa6c85b8d0ef0 (patch)
tree4b2f793909a1f97e52213467cc9f60e810a886df
parenta2151bcd260a50f697220abf75f522623b0960c6 (diff)
downloademacs-32329d9a384642dd231ed1f5a16aa6c85b8d0ef0.tar.gz
relic: 2026-09-12T00:30:15+02:00
-rwxr-xr-xbin/project-ctags69
-rw-r--r--config.org152
-rw-r--r--lisp/fossil-ui.el96
-rw-r--r--test/fossil-ui-test.el56
-rw-r--r--themes/tinty-theme.el13
5 files changed, 340 insertions, 46 deletions
diff --git a/bin/project-ctags b/bin/project-ctags
index 46c8561..47b1fe5 100755
--- a/bin/project-ctags
+++ b/bin/project-ctags
@@ -6,6 +6,7 @@ import functools
import hashlib
import json
import os
+import re
import shlex
import shutil
import subprocess
@@ -35,6 +36,11 @@ IGNORED_DIRECTORIES = {
}
PYTHON_SUFFIXES = {".py", ".pyi"}
RUST_SUFFIXES = {".rs"}
+GO_SUFFIXES = {".go", ".templ"}
+TEMPL_CTAGS_OPTIONS = [
+ "--map-Go=+.templ",
+ r"--regex-Go=/^[[:space:]]*(templ|css|script)[[:space:]]+([[:alpha:]_][[:alnum:]_]*)[[:space:]]*\(/\2/f/",
+]
C_SOURCE_SUFFIXES = {".c", ".cc", ".cpp", ".cxx", ".m", ".mm"}
C_HEADER_SUFFIXES = {".h", ".hh", ".hpp", ".hxx"}
C3_SUFFIXES = {".c3", ".c3i", ".c3t"}
@@ -108,11 +114,13 @@ def parse_arguments():
parser.add_argument("--python")
parser.add_argument("--python-command")
parser.add_argument("--cargo")
+ parser.add_argument("--go")
parser.add_argument("--c3c")
parser.add_argument("--c3-stdlib")
parser.add_argument("--source", action="append", default=[])
parser.add_argument("--no-python-deps", action="store_true")
parser.add_argument("--no-rust-deps", action="store_true")
+ parser.add_argument("--no-go-deps", action="store_true")
parser.add_argument("--no-c-deps", action="store_true")
parser.add_argument("--no-c3-deps", action="store_true")
parser.add_argument("--cache-dir", help="Maintain project and dependency indexes here")
@@ -342,6 +350,51 @@ def rust_dependency_files(args, root):
return sorted(files), cargo
+def go_dependency_files(args, root, files):
+ """Resolve imported Go packages, including the standard library, from local sources."""
+ if args.no_go_deps or not any(path.suffix.lower() in GO_SUFFIXES for path in files):
+ return [], None
+ value = args.go or os.environ.get("PROJECT_CTAGS_GO") or shutil.which("go")
+ if not value:
+ print("Go dependencies skipped: go is unavailable in the project environment", file=sys.stderr)
+ return [], None
+ go = shutil.which(value) or str(resolved_path(value, root))
+ environment = dict(os.environ, GOPROXY="off", GOSUMDB="off", GOTOOLCHAIN="local")
+ result = subprocess.run(
+ [go, "list", "-e", "-deps", "-json", "-mod=readonly", "./..."],
+ cwd=root,
+ env=environment,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=False,
+ )
+ if result.stderr.strip():
+ print(result.stderr.rstrip(), file=sys.stderr)
+ dependencies = set()
+ decoder = json.JSONDecoder()
+ remaining = result.stdout.lstrip()
+ while remaining:
+ try:
+ package, end = decoder.raw_decode(remaining)
+ except json.JSONDecodeError:
+ print("Could not read Go package metadata", file=sys.stderr)
+ break
+ remaining = remaining[end:].lstrip()
+ if error := package.get("Error"):
+ print(f"go: {error.get('Err', error)}", file=sys.stderr)
+ if not package.get("Dir"):
+ continue
+ directory = Path(package["Dir"]).resolve()
+ if directory.is_relative_to(root):
+ continue
+ for name in package.get("GoFiles", []) + package.get("CgoFiles", []):
+ path = directory / name
+ if path.is_file():
+ dependencies.add(path.resolve())
+ return sorted(dependencies), go
+
+
def compilation_database(root):
path = root / "compile_commands.json"
if not path.is_file():
@@ -494,6 +547,7 @@ def generate_tags(ctags, output, files, mappings=None):
"--options=NONE",
"--output-format=e-ctags",
*NIX_CTAGS_OPTIONS,
+ *TEMPL_CTAGS_OPTIONS,
*c3_ctags_options(ctags),
"--languages=all",
"--kinds-all=*",
@@ -580,8 +634,15 @@ def dependency_signature(args, root, files):
"Cargo.toml", "Cargo.lock", "project.json", "pyproject.toml", "uv.lock",
"poetry.lock", "pixi.lock", "pixi.toml", "requirements.txt",
"compile_commands.json", "flake.lock", "flake.nix", ".envrc",
+ "go.mod", "go.sum", "go.work", "go.work.sum",
}:
declarations.append((str(path), file_stamp(path)))
+ elif path.suffix.lower() in GO_SUFFIXES:
+ try:
+ imports = re.findall(r'(?m)^\s*import\s*(?:\([^)]*\)|[^\n]+)', path.read_text(errors="replace"))
+ declarations.append((str(path), imports))
+ except OSError:
+ pass
elif path.suffix.lower() in C_SOURCE_SUFFIXES | C_HEADER_SUFFIXES:
try:
includes = [line.strip() for line in path.read_text(errors="replace").splitlines() if line.lstrip().startswith(("#include", "# include"))]
@@ -596,6 +657,8 @@ def dependency_signature(args, root, files):
"PATH", "VIRTUAL_ENV", "CC", "CFLAGS", "CPPFLAGS",
"PROJECT_CTAGS_PYTHON_COMMAND", "PROJECT_CTAGS_CARGO",
"PROJECT_CTAGS_C3C", "C3_STDLIB", "PROJECT_CTAGS_SOURCES",
+ "PROJECT_CTAGS_GO", "GOROOT", "GOPATH", "GOMODCACHE",
+ "GOWORK", "GOOS", "GOARCH", "CGO_ENABLED", "GOFLAGS",
)
},
[(str(root / name), file_stamp(root / name)) for name in (".venv", ".pixi/envs/default", ".devenv/state/venv")]]
@@ -634,6 +697,7 @@ def refresh_cache(args, root, ctags):
dependency_files.extend(python_dependency_files(args, root, files)[0])
if not args.no_rust_deps:
dependency_files.extend(rust_dependency_files(args, root)[0])
+ dependency_files.extend(go_dependency_files(args, root, files)[0])
if not args.no_c_deps:
dependency_files.extend(c_dependency_files(root, files))
dependency_files.extend(extra_source_files(args, root))
@@ -692,8 +756,9 @@ def main():
c_headers = c_dependency_files(root, local_files)
extra_files = extra_source_files(args, root)
c3_files = c3_dependency_files(args, root, local_files)
+ go_files, go = go_dependency_files(args, root, local_files)
all_files = sorted(
- set(local_files + python_files + rust_files + c_headers + c3_files + extra_files)
+ set(local_files + python_files + rust_files + go_files + c_headers + c3_files + extra_files)
)
if not all_files:
raise SystemExit("No source files found")
@@ -708,6 +773,8 @@ def main():
print(f"rust: {cargo}; {len(rust_files)} dependency files")
if c_headers:
print(f"c: {len(c_headers)} dependency headers")
+ if go:
+ print(f"go: {go}; {len(go_files)} dependency files")
if c3_files:
print(f"c3: {len(c3_files)} standard-library files")
if extra_files:
diff --git a/config.org b/config.org
index fd4e919..2e46114 100644
--- a/config.org
+++ b/config.org
@@ -7,7 +7,7 @@
#+begin_src emacs-lisp
;;; config.el --- Emacs-Kick --- A feature rich Emacs config for (neo)vi(m)mers -*- lexical-binding: t; -*-
-(pixel-scroll-precision-mode 1)
+;; (pixel-scroll-precision-mode 1)
(setq pgtk-wait-for-event-timeout 0.001)
(setq package-enable-at-startup nil
;; Local modules are edited independently of their optional byte-code files, so never let an older .elc silently shadow newer source.
@@ -1248,7 +1248,7 @@ Use =org-modern= only for quiet structural markers in Org files. GTD labels, tim
#+begin_src emacs-lisp
#+end_src
-** COMMENT kitty-graphics
+** kitty-graphics
Display inline images in terminal Emacs via the Kitty graphics protocol.
Only activates when running in a Kitty terminal (not in GUI Emacs).
@@ -1569,6 +1569,29 @@ Skips capture tasks and projects."
"TODO")))))
#+end_src
+*** org-clock-reminder
+
+#+begin_src emacs-lisp
+(use-package org-clock-reminder
+ :ensure (:host github :repo "inickey/org-clock-reminder")
+ :after org
+ :demand t
+ :custom
+ (org-clock-reminder-interval 15)
+ (org-clock-reminder-inactive-notifications-p nil)
+ (org-clock-reminder-active-title "Org: current task")
+ (org-clock-reminder-active-text "%h\nClocked time: %c")
+ (org-clock-reminder-inactive-title "Org: no running clock")
+ (org-clock-reminder-inactive-text "No task is currently clocked in.")
+ (org-clock-reminder-icons nil)
+ (org-clock-reminder-notifiers '(org-clock-reminder-notify))
+ :config
+ (org-clock-reminder-mode 1)
+ ;; Synchronize when enabling reminders while a clock is already running.
+ (when (org-clocking-p)
+ (org-clock-reminder-on-clock-in)))
+#+end_src
+
*** effort and complexity management
#+begin_src emacs-lisp
@@ -2673,15 +2696,14 @@ Auto switch silos (different workspaces/folders) based on preconfigured director
* helm-mode
+
#+begin_src emacs-lisp
(use-package helm
:ensure (:wait t)
:after evil-collection
:hook
(after-init . helm-mode)
- (helm-minibuffer-set-up . (lambda ()
- (evil-insert-state)
- (add-hook 'minibuffer-exit-hook #'turn-off-evil-mode nil t)))
+ (helm-minibuffer-set-up . turn-off-evil-mode)
:custom
(helm-M-x-fuzzy-match t)
(helm-occur-buffer-substring-default-mode 'buffer-substring)
@@ -2721,6 +2743,7 @@ Auto switch silos (different workspaces/folders) based on preconfigured director
* tags (citre)
+
#+begin_src emacs-lisp
(use-package citre
:ensure t
@@ -2869,6 +2892,32 @@ Auto switch silos (different workspaces/folders) based on preconfigured director
#+end_src
+** go-mode
+
+#+begin_src emacs-lisp
+(use-package go-mode
+ :ensure t
+ :mode (("\\.go\\'" . go-mode)
+ ("\\.templ\\'" . go-mode))
+ :hook (go-mode . (lambda () (setq-local tab-width 4))))
+
+(defun my/go-citre-bindings (&optional mode _maps &rest _)
+ "Apply Citre navigation after Go or Evil Collection loads."
+ (when (and (boundp 'go-mode-map) (or (null mode) (eq mode 'go-mode)))
+ (keymap-set go-mode-map "<remap> <godef-jump>" #'citre-jump)
+ (keymap-set go-mode-map "<remap> <godef-describe>" #'citre-peek)
+ (when (featurep 'evil)
+ (evil-define-key* 'normal go-mode-map
+ (kbd "g d") #'citre-jump
+ (kbd "K") #'citre-peek))))
+
+(with-eval-after-load 'go-mode
+ (my/go-citre-bindings))
+(with-eval-after-load 'evil
+ (my/go-citre-bindings))
+(add-hook 'evil-collection-setup-hook #'my/go-citre-bindings)
+#+end_src
+
** c-ts-mode
Built-in tree-sitter mode (Emacs 29+). The =c= grammar ships via =treesit-grammars.with-all-grammars= in =emacs.nix=, so no on-the-fly compilation is needed.
@@ -3161,6 +3210,15 @@ BODY is the xonsh script. PARAMS may include :dir and :cmdline."
(message "Uncommitted HEAD^ — changes preserved, unstaged"))
#+end_src
+** magit-forge
+
+#+begin_src emacs-lisp
+(use-package forge
+ :ensure t
+ :after magit)
+#+end_src
+
+
* indent-guide
#+begin_src emacs-lisp
@@ -3230,6 +3288,7 @@ BODY is the xonsh script. PARAMS may include :dir and :cmdline."
:after evil
:ensure (:wait t)
:config
+ (setq evil-collection-mode-list (remq 'helm evil-collection-mode-list))
(evil-collection-require 'minibuffer)
(evil-collection-init))
#+end_src
@@ -3734,6 +3793,7 @@ moved to local.el
(fossil-ui-text-wrap 'greedy)
(fossil-ui-preview-lines 20)
(fossil-ui-diff-renderer 'auto)
+ (fossil-ui-delta-color-mode 'auto)
(fossil-ui-stage-directory (expand-file-name "fossil-stage/" user-emacs-directory))
(fossil-ui-max-stage-bytes (* 5 1024 1024))
(fossil-ui-keyed-timeline t)
@@ -4928,7 +4988,7 @@ place. `C-c C-c' commits, `C-c C-k' aborts."
(kbd "g b") #'ghostel-list-buffers))
#+end_src
-** helm
+** COMMENT helm
#+begin_src emacs-lisp
(evil-define-key '(normal insert) helm-map
@@ -5056,6 +5116,7 @@ place. `C-c C-c' commits, `C-c C-k' aborts."
(keymap-set my/org-command-map "," #'org-priority)
(keymap-set my/org-command-map "I" #'org-clock-in)
(keymap-set my/org-command-map "O" #'org-clock-out)
+(keymap-set my/org-command-map "R" #'org-clock-reminder-mode)
(keymap-set my/org-leader-map "l" my/org-link-map)
(keymap-set my/org-link-map "c" #'org-cliplink)
(keymap-set my/org-link-map "i" #'org-download-clipboard)
@@ -5345,6 +5406,7 @@ reset is unnecessary, so do the handler resolution ourselves and skip
* mu4e
+
#+begin_src emacs-lisp
(use-package mu4e
:ensure nil
@@ -5381,12 +5443,21 @@ opened directly with `mu4e-compose-new'."
(setq mu4e-attachment-dir "~/Downloads")
(setq mu4e-change-filenames-when-moving t)
- (setq mu4e-user-mail-address-list
- '("cashmeresamurai@autistici.org"
- "cashmere@cashmere.rs"))
-
(setq mu4e-contexts
`(,(make-mu4e-context
+ :name "riseup"
+ :match-func
+ (lambda (msg)
+ (when msg
+ (string-prefix-p "/riseup/" (mu4e-message-field msg :maildir))))
+ :vars '((user-mail-address . "cashmere1337@riseup.net")
+ (user-full-name . "cashmere")
+ (mu4e-drafts-folder . "/riseup/Drafts")
+ (mu4e-sent-folder . "/riseup/Sent")
+ (mu4e-trash-folder . "/riseup/Trash")
+ (mu4e-refile-folder . "/riseup/Archive")))
+
+ ,(make-mu4e-context
:name "autistici"
:match-func
(lambda (msg)
@@ -5417,23 +5488,30 @@ opened directly with `mu4e-compose-new'."
(setq sendmail-program (executable-find "msmtp"))
(setq send-mail-function 'sendmail-send-it)
- (setq message-send-mail-function 'sendmail-send-it)
+ (setq message-send-mail-function 'message-send-mail-with-sendmail)
(setq message-sendmail-envelope-from 'header)
(setq message-kill-buffer-on-exit t)
- (defun mu4e-set-msmtp-account ()
- (if (message-mail-p)
- (save-excursion
- (let* ((from (save-restriction
- (message-narrow-to-headers)
- (message-fetch-field "from")))
- (account
- (cond
- ((string-match "cashmeresamurai@autistici.org" from) "autistici")
- ((string-match "cashmere@cashmere.rs" from) "cashmere/cashmere"))))
- (setq message-sendmail-extra-arguments (list '"-a" account))))))
+ (defun my/mu4e-set-msmtp-account ()
+ "Select the msmtp account matching the draft's From address."
+ (when (message-mail-p)
+ (save-restriction
+ (message-narrow-to-headers)
+ (let* ((from (message-fetch-field "from"))
+ (address (and from (cadr (mail-extract-address-components from))))
+ (account (cdr (assoc-string
+ (or address "")
+ '(("cashmere1337@riseup.net" . "riseup")
+ ("cashmeresamurai@autistici.org" . "autistici")
+ ("cashmere@cashmere.rs" . "cashmere-rs"))
+ t))))
+ (setq-local message-sendmail-extra-arguments nil)
+ (unless account
+ (user-error "No email From: %s" (or from "<missing>")))
+ (setq-local message-sendmail-extra-arguments (list "-a" account))))))
- (add-hook 'message-send-mail-hook 'mu4e-set-msmtp-account))
+ (remove-hook 'message-send-mail-hook 'mu4e-set-msmtp-account)
+ (add-hook 'message-send-mail-hook #'my/mu4e-set-msmtp-account))
#+end_src
** Himalaya
@@ -5459,6 +5537,7 @@ opened directly with `mu4e-compose-new'."
;; Interesting mail query: unread, exclude trash and sent folders
(setq mu4e-alert-interesting-mail-query
(concat "flag:unread AND NOT flag:trashed"
+ " AND NOT maildir:\"/riseup/Sent\""
" AND NOT maildir:\"/autistici/Sent\""
" AND NOT maildir:\"/cashmere/cashmere/Sent\""))
@@ -5499,17 +5578,19 @@ opened directly with `mu4e-compose-new'."
* envrc
-Automatic flake devshell activation.
-
-Async activation: defer per-buffer =envrc-mode= via idle timer so direnv
-=process-file= does not block buffer display.
-
#+begin_src emacs-lisp
(use-package envrc
:ensure t
:defer 1
:init
(require 'notifications)
+ (defun my/envrc-project-buffer ()
+ "Load the local project environment before terminal startup or task discovery."
+ (unless (file-remote-p default-directory)
+ (require 'envrc)
+ (envrc-mode 1)
+ (when (eq envrc--status 'error)
+ (user-error "Project environment failed; inspect M-x envrc-show-log"))))
(defvar my/envrc-notified (make-hash-table :test 'equal)
"Directories already notified about, to avoid spam.")
(defun my/envrc-notify (dir status)
@@ -5552,7 +5633,9 @@ opening another file in same project does not re-notify."
(when status
(my/envrc-notify default-directory status))))))))))
:hook ((find-file . my/envrc-maybe-async)
- (dired-mode . my/envrc-maybe-async))
+ (dired-mode . my/envrc-project-buffer)
+ (projectile-dashboard-mode . my/envrc-project-buffer)
+ (ghostel-mode . my/envrc-project-buffer))
:config
(when (bound-and-true-p envrc-global-mode)
(envrc-global-mode -1)))
@@ -5943,6 +6026,17 @@ Privileged ZFS operations stay non-interactive by default. Unlocking runs authen
(clatter-org-setup)))
#+end_src
+* apheleia
+
+#+begin_src emacs-lisp
+(use-package apheleia
+ :ensure t
+ :hook (nix-ts-mode . apheleia-mode)
+ :custom
+ (apheleia-mode-alist '((nix-ts-mode . nixfmt))))
+#+end_src
+
+
* provide init
#+begin_src emacs-lisp
diff --git a/lisp/fossil-ui.el b/lisp/fossil-ui.el
index 0e65337..f4202a0 100644
--- a/lisp/fossil-ui.el
+++ b/lisp/fossil-ui.el
@@ -129,6 +129,13 @@ runs an external renderer."
:type '(repeat string)
:group 'fossil-ui)
+(defcustom fossil-ui-delta-color-mode 'auto
+ "Color mode passed to Delta when no explicit color or syntax-theme argument is configured."
+ :type '(choice (const :tag "Follow the current Emacs theme" auto)
+ (const :tag "Light backgrounds" light)
+ (const :tag "Dark backgrounds" dark))
+ :group 'fossil-ui)
+
(defcustom fossil-ui-title-alignment 'center
"TextUI alignment of the project heading."
:type '(choice (const left) (const center) (const right))
@@ -1160,7 +1167,27 @@ MINIMUM and GROW are parent-facing Flex sizing hints."
(add-hook 'fossil-ui-diff-mode-hook #'fossil-ui--own-diff-bindings t)
-(defun fossil-ui--delta-render (diff)
+(defun fossil-ui--delta-color-argument (&optional frame)
+ "Return the Delta color argument for FRAME according to `fossil-ui-delta-color-mode'."
+ (pcase fossil-ui-delta-color-mode
+ ('auto (if (eq (frame-parameter frame 'background-mode) 'dark)
+ "--dark"
+ "--light"))
+ ('light "--light")
+ ('dark "--dark")
+ (_ (user-error "Unknown Fossil Delta color mode: %S" fossil-ui-delta-color-mode))))
+
+(defun fossil-ui--delta-color-argument-p (argument)
+ "Return non-nil when ARGUMENT explicitly controls Delta's color mode or syntax theme."
+ (string-match-p "\\`--\\(?:light\\|dark\\|syntax-theme\\)\\(?:=\\|\\'\\)" argument))
+
+(defun fossil-ui--effective-delta-arguments (&optional frame)
+ "Return Delta arguments for FRAME with an automatic color mode when needed."
+ (if (seq-some #'fossil-ui--delta-color-argument-p fossil-ui-delta-arguments)
+ fossil-ui-delta-arguments
+ (cons (fossil-ui--delta-color-argument frame) fossil-ui-delta-arguments)))
+
+(defun fossil-ui--delta-render (diff &optional frame)
"Render unified DIFF through Delta and return propertized Emacs text.
Return nil when Delta exits unsuccessfully."
(with-temp-buffer
@@ -1169,7 +1196,7 @@ Return nil when Delta exits unsuccessfully."
(apply #'call-process-region
(point-min) (point-max)
fossil-ui-delta-program t t nil
- fossil-ui-delta-arguments)
+ (fossil-ui--effective-delta-arguments frame))
(file-missing 127)
(error 1))))
(when (zerop code)
@@ -1190,21 +1217,72 @@ Return nil when Delta exits unsuccessfully."
(setq position next)))
rendered)))))
-(defun fossil-ui--render-diff (diff)
- "Render unified DIFF according to `fossil-ui-diff-renderer'."
+(defun fossil-ui--render-diff (diff &optional frame)
+ "Render unified DIFF for FRAME according to `fossil-ui-diff-renderer'."
(pcase fossil-ui-diff-renderer
('plain diff)
('delta
(or (and (executable-find fossil-ui-delta-program)
- (fossil-ui--delta-render diff))
+ (fossil-ui--delta-render diff frame))
(user-error "Delta could not render this diff")))
('auto
(or (and (executable-find fossil-ui-delta-program)
- (fossil-ui--delta-render diff))
+ (fossil-ui--delta-render diff frame))
diff))
(_ (user-error "Unknown Fossil diff renderer: %S"
fossil-ui-diff-renderer))))
+(defun fossil-ui--display-diff (diff &optional frame)
+ "Return an actionable rendering of DIFF for FRAME."
+ (let ((rendered (fossil-ui--render-diff diff frame)))
+ (if (equal diff (substring-no-properties rendered))
+ rendered
+ diff)))
+
+(defun fossil-ui--rerender-diff-buffer (&optional frame)
+ "Rerender the current Fossil diff buffer for FRAME without querying Fossil."
+ (when (and (derived-mode-p 'fossil-ui-diff-mode)
+ fossil-ui-diff-context)
+ (let* ((diff (plist-get fossil-ui-diff-context :diff))
+ (display (fossil-ui--display-diff diff frame))
+ (colored (text-property-not-all 0 (length display) 'face nil display))
+ (buffer-point (point))
+ (modified (buffer-modified-p))
+ (windows (mapcar (lambda (window)
+ (list window (window-start window) (window-point window)))
+ (get-buffer-window-list (current-buffer) nil t))))
+ (if colored
+ (font-lock-mode -1)
+ (font-lock-mode 1))
+ (let ((inhibit-read-only t)
+ (buffer-undo-list t))
+ (erase-buffer)
+ (insert display))
+ (set-buffer-modified-p modified)
+ (goto-char (min buffer-point (point-max)))
+ (dolist (state windows)
+ (pcase-let ((`(,window ,start ,window-point) state))
+ (when (window-live-p window)
+ (set-window-start window (min start (point-max)) t)
+ (set-window-point window (min window-point (point-max))))))
+ (unless colored
+ (font-lock-ensure)))))
+
+(defun fossil-ui--theme-enabled (_theme)
+ "Rerender live Fossil diff buffers after a theme has been enabled."
+ (dolist (buffer (buffer-list))
+ (with-current-buffer buffer
+ (when (and (derived-mode-p 'fossil-ui-diff-mode)
+ fossil-ui-diff-context)
+ (let* ((window (get-buffer-window buffer t))
+ (frame (and window (window-frame window))))
+ (condition-case error
+ (fossil-ui--rerender-diff-buffer frame)
+ (error
+ (message "Could not recolor %s: %s" (buffer-name buffer) (error-message-string error)))))))))
+
+(add-hook 'enable-theme-functions #'fossil-ui--theme-enabled)
+
(defun fossil-ui-visit-file ()
"Visit the file at point."
(interactive)
@@ -1991,11 +2069,7 @@ Return nil when Delta exits unsuccessfully."
(plist-get entry :staged)
work))
(diff (fossil-ui--diff path left right))
- (colored (fossil-ui--render-diff diff))
- (display
- (if (equal diff (substring-no-properties colored))
- colored
- diff))
+ (display (fossil-ui--display-diff diff))
(buffer
(get-buffer-create
(format "*fossil %s: %s/%s*"
diff --git a/test/fossil-ui-test.el b/test/fossil-ui-test.el
index 6b61bd9..081a701 100644
--- a/test/fossil-ui-test.el
+++ b/test/fossil-ui-test.el
@@ -231,6 +231,62 @@
(should-not (string-search (string ?\e) rendered))
(should (text-property-not-all 0 (length rendered) 'face nil rendered))))
+(ert-deftest fossil-ui-delta-color-mode-follows-frame-background ()
+ (let ((fossil-ui-delta-color-mode 'auto)
+ (fossil-ui-delta-arguments '("--paging=never" "--color-only")))
+ (cl-letf (((symbol-function 'frame-parameter)
+ (lambda (_frame parameter)
+ (and (eq parameter 'background-mode) 'light))))
+ (should (equal (fossil-ui--effective-delta-arguments)
+ '("--light" "--paging=never" "--color-only"))))
+ (cl-letf (((symbol-function 'frame-parameter)
+ (lambda (_frame parameter)
+ (and (eq parameter 'background-mode) 'dark))))
+ (should (equal (fossil-ui--effective-delta-arguments)
+ '("--dark" "--paging=never" "--color-only"))))))
+
+(ert-deftest fossil-ui-delta-color-mode-respects-explicit-arguments ()
+ (let ((fossil-ui-delta-color-mode 'auto))
+ (dolist (arguments '(("--light" "--color-only")
+ ("--dark" "--color-only")
+ ("--syntax-theme" "GitHub" "--color-only")
+ ("--syntax-theme=GitHub" "--color-only")))
+ (let ((fossil-ui-delta-arguments arguments))
+ (should (eq (fossil-ui--effective-delta-arguments) arguments))))))
+
+(ert-deftest fossil-ui-theme-change-rerenders-open-diff-in-place ()
+ (save-window-excursion
+ (with-temp-buffer
+ (let* ((buffer (current-buffer))
+ (raw (mapconcat (lambda (number) (format "line %d" number)) (number-sequence 1 80) "\n"))
+ (context (list :root "/tmp/project/" :path "example.el" :diff raw)))
+ (fossil-ui-diff-mode)
+ (let ((inhibit-read-only t))
+ (insert raw))
+ (set-buffer-modified-p nil)
+ (setq-local fossil-ui-diff-context context)
+ (switch-to-buffer buffer)
+ (goto-char (point-min))
+ (forward-line 40)
+ (let ((point-before (point))
+ (window-before (selected-window)))
+ (set-window-start window-before (save-excursion
+ (goto-char (point-min))
+ (forward-line 20)
+ (point)))
+ (let ((start-before (window-start window-before)))
+ (cl-letf (((symbol-function 'fossil-ui--display-diff)
+ (lambda (_diff &optional _frame)
+ (propertize raw 'face 'success))))
+ (fossil-ui--theme-enabled 'test-theme))
+ (should (eq (selected-window) window-before))
+ (should (= (point) point-before))
+ (should (= (window-start window-before) start-before))
+ (should-not (buffer-modified-p))
+ (should (equal fossil-ui-diff-context context))
+ (should (equal (buffer-substring-no-properties (point-min) (point-max)) raw))
+ (should (eq (get-text-property (point-min) 'face) 'success))))))))
+
(ert-deftest fossil-ui-status-opens-rendered-dashboard ()
(fossil-ui-test--with-checkout
(fossil-ui-test--write (expand-file-name "tracked.txt" root) "changed content\n")
diff --git a/themes/tinty-theme.el b/themes/tinty-theme.el
index 0e32aac..736d33c 100644
--- a/themes/tinty-theme.el
+++ b/themes/tinty-theme.el
@@ -62,7 +62,10 @@
:type 'boolean
:group 'tinty)
-(deftheme tinty "Noctalia theme with proper color contrast.")
+(deftheme tinty
+ "Noctalia theme with proper color contrast."
+ :background-mode 'dark
+ :kind 'color-scheme)
(let* (;; Tinted8 UI surface colors
(ui-bg "#22212c")
@@ -96,9 +99,9 @@
(ui-search-fg "#ededde")
;; Tinted8 syntax colors
- ;; Comments use dim foreground instead of the raw syntax-comment gray,
- ;; which becomes almost invisible in light variants.
- (syntax-comment "#babaab")
+ ;; The builder already corrects syntax colors for text contrast, including light variants.
+ (syntax-comment "#8a83b4")
+ (syntax-documentation "#8a83b4")
(syntax-constant "#ffca80")
(syntax-function "#8aff80")
(syntax-keyword "#ff80bf")
@@ -167,7 +170,7 @@
`(font-lock-comment-face ((t (:foreground ,syntax-comment :slant ,comment-slant))))
`(font-lock-comment-delimiter-face ((t (:foreground ,syntax-comment :slant ,comment-slant))))
`(font-lock-constant-face ((t (:foreground ,syntax-constant :weight bold))))
- `(font-lock-doc-face ((t (:foreground ,syntax-comment :slant ,comment-slant))))
+ `(font-lock-doc-face ((t (:foreground ,syntax-documentation :slant ,comment-slant))))
`(font-lock-doc-markup-face ((t (:foreground ,syntax-annotation))))
`(font-lock-function-name-face ((t (:foreground ,syntax-function :weight bold))))
`(font-lock-function-call-face ((t (:foreground ,syntax-function))))