diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-12 00:30:11 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-12 00:30:11 +0200 |
| commit | 32329d9a384642dd231ed1f5a16aa6c85b8d0ef0 (patch) | |
| tree | 4b2f793909a1f97e52213467cc9f60e810a886df | |
| parent | a2151bcd260a50f697220abf75f522623b0960c6 (diff) | |
| download | emacs-32329d9a384642dd231ed1f5a16aa6c85b8d0ef0.tar.gz | |
relic: 2026-09-12T00:30:15+02:00
| -rwxr-xr-x | bin/project-ctags | 69 | ||||
| -rw-r--r-- | config.org | 152 | ||||
| -rw-r--r-- | lisp/fossil-ui.el | 96 | ||||
| -rw-r--r-- | test/fossil-ui-test.el | 56 | ||||
| -rw-r--r-- | themes/tinty-theme.el | 13 |
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: @@ -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)))) |
