-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathsupertag-view-helper.el
More file actions
948 lines (843 loc) · 43.6 KB
/
Copy pathsupertag-view-helper.el
File metadata and controls
948 lines (843 loc) · 43.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
;;; supertag/supertag-view-helper.el --- Reusable view helpers for Supertag -*- lexical-binding: t; -*-
;;; Commentary:
;; This file provides common, reusable functions for building view buffers,
;; such as rendering headers, formatting values, and managing windows.
;;; Code:
(require 'cl-lib)
(require 'org)
(require 'supertag-core-transform)
(require 'supertag-view-api)
;; Forward declare functions to avoid circular dependencies
(declare-function supertag-field-get-with-default "supertag-ops-field")
(declare-function supertag-field-normalize-node-reference-list "supertag-ops-field")
(declare-function supertag-svg-tag--refresh-all-buffers "supertag-view-svg-tag")
;;;----------------------------------------------------------------------
;;; Constants and Configuration
;;;----------------------------------------------------------------------
(defvar supertag-view-helper--valid-tag-chars nil
"Character class used to detect inline tags.
This string is spliced directly into [] expressions, so \"^\" negates the set.
Anything except whitespace-like characters and another # counts as part of the tag,
allowing hierarchies (/) and arbitrary unicode/emoji symbols.")
;; Always refresh the value so reloading this file picks up updates.
(setq supertag-view-helper--valid-tag-chars "^[:space:]#")
;; Legacy alias for backward compatibility
(defvaralias 'supertag-view-style--valid-tag-chars
'supertag-view-helper--valid-tag-chars)
(defgroup supertag-view-style nil
"Customization options for supertag inline tag styling."
:group 'supertag)
(defcustom supertag-view-style-tag-face-properties
'(:foreground "snow3")
"Face properties for inline supertags.
This should be a plist of face attributes."
:type '(plist :key-type symbol :value-type sexp)
:group 'supertag-view-style)
(defcustom supertag-view-style-auto-enable t
"Whether to automatically enable supertag-view-style-mode in org buffers."
:type 'boolean
:group 'supertag-view-style)
(defun supertag-view-helper--inline-tag-range-at (position)
"Return the range-aware prose Tag match starting at POSITION."
(save-match-data
(save-excursion
(goto-char position)
(let* ((context (org-element-context))
(type (org-element-type context))
(heading (org-element-lineage context '(headline) t)))
(when (and (memq type '(headline paragraph))
(not (org-element-lineage
context '(drawer property-drawer) t))
(not (and heading
(org-element-property :commentedp heading))))
(cl-find position
(supertag-transform-inline-tag-matches-in-region
(line-beginning-position) (line-end-position) nil type)
:key #'car :test #'=))))))
(defun supertag-view-helper--font-lock-matcher (limit)
"Find the next range-aware prose Tag before LIMIT."
(catch 'match
(while (re-search-forward supertag-inline-tag-regexp limit t)
(let* ((tag-start (1- (match-beginning 2)))
(range (supertag-view-helper--inline-tag-range-at tag-start)))
(when range
(set-match-data (list (nth 0 range) (nth 1 range)))
(throw 'match t))))
nil))
;;;----------------------------------------------------------------------
;;; Minor Mode Definition and Public API
;;;----------------------------------------------------------------------
;;;###autoload
(define-minor-mode supertag-view-style-mode
"Minor mode for styling supertag inline tags.
When `supertag-svg-tag-enable' is non-nil, uses SVG pill badges.
Otherwise uses face-based rendering via `supertag-inline-face'."
:lighter " Tag-Style"
:group 'supertag-view-style
(if supertag-view-style-mode
(progn
;; Allow font-lock to manage the `display' property for SVG rendering
(make-local-variable 'font-lock-extra-managed-props)
(cl-pushnew 'display font-lock-extra-managed-props)
(font-lock-add-keywords nil (supertag-view-helper--get-font-lock-keywords) t)
(supertag-view-helper--refresh-fontification))
(font-lock-remove-keywords nil supertag-view-helper--font-lock-keywords)
(when (bound-and-true-p supertag-view-svg-tag--font-lock-keywords)
(font-lock-remove-keywords nil supertag-view-svg-tag--font-lock-keywords))
(supertag-view-helper--refresh-fontification)))
(defun supertag-view-helper--get-font-lock-keywords ()
"Return the appropriate font-lock keywords based on current config.
Prefers SVG keywords when `supertag-svg-tag-enable' is non-nil."
(if (and (bound-and-true-p supertag-svg-tag-enable)
(bound-and-true-p supertag-view-svg-tag--font-lock-keywords)
(fboundp 'svg-create))
supertag-view-svg-tag--font-lock-keywords
supertag-view-helper--font-lock-keywords))
(defun supertag-view-helper--refresh-fontification ()
"Refresh font-lock fontification in the current buffer."
(if (fboundp 'font-lock-flush)
(font-lock-flush)
(font-lock-fontify-buffer)))
(defun supertag-view-helper--auto-enable ()
"Auto-enable supertag-view-style-mode in org buffers if configured."
(when supertag-view-style-auto-enable
(supertag-view-style-mode 1)))
(defun supertag-view-helper--enable-existing-org-buffers ()
"Auto-enable inline tag styling in already existing Org buffers."
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when (derived-mode-p 'org-mode)
(supertag-view-helper--auto-enable)))))
;; Hook into org-mode
(add-hook 'org-mode-hook #'supertag-view-helper--auto-enable)
;;;###autoload
(defun supertag-view-style-refresh ()
"Manually refresh tag highlighting in the current buffer."
(interactive)
(when supertag-view-style-mode
(supertag-view-helper--refresh-fontification)
(message "Supertag highlighting refreshed")))
;;;###autoload
(defun supertag-view-style-toggle ()
"Toggle supertag-view-style-mode in the current buffer."
(interactive)
(supertag-view-style-mode 'toggle)
(message "Supertag styling %s"
(if supertag-view-style-mode "enabled" "disabled")))
(defconst supertag-view-helper--section-separator "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
"Separator line for sections.")
(defconst supertag-view-helper--subsection-separator "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄"
"Separator line for subsections.")
;;;----------------------------------------------------------------------
;;; Face Definitions and Font-lock Integration
;;;----------------------------------------------------------------------
(defface supertag-inline-face
`((t ,supertag-view-style-tag-face-properties))
"Face for supertag inline tags."
:group 'supertag-view-style)
(defvar supertag-view-helper--font-lock-keywords
'((supertag-view-helper--font-lock-matcher
(0 'supertag-inline-face t)))
"Font-lock keywords for highlighting inline tags.")
(supertag-view-helper--enable-existing-org-buffers)
(with-eval-after-load 'supertag-view-svg-tag
(supertag-svg-tag--refresh-all-buffers))
(defun supertag-view-helper-insert-section-header (title icon)
"Insert a section header with ICON and TITLE."
(insert (propertize (format "%s %s\n" icon title) 'face '(:weight bold :height 1.2)))
(insert (propertize (format "%s\n" supertag-view-helper--section-separator) 'face '(:foreground "gray50")))
(insert "\n"))
(defun supertag-view-helper-insert-subsection-header (title)
"Insert a subsection header with TITLE."
(insert (propertize (format " %s\n" title) 'face '(:weight bold)))
(insert (propertize (format " %s\n" supertag-view-helper--subsection-separator) 'face '(:foreground "gray70")))
(insert "\n"))
;;; --- Theme Adaptive Colors ---
(defun supertag-view-helper-get-theme-adaptive-color (light-color dark-color)
"Get color that adapts to current theme."
(if (eq (frame-parameter nil 'background-mode) 'dark)
dark-color
light-color))
(defun supertag-view-helper-get-accent-color ()
"Get accent color that works well in both light and dark themes."
(supertag-view-helper-get-theme-adaptive-color "#0066CC" "#66B3FF"))
(defun supertag-view-helper-get-emphasis-color ()
"Get emphasis color that works well in both light and dark themes."
(supertag-view-helper-get-theme-adaptive-color "#003D82" "#99D6FF"))
(defun supertag-view-helper-get-muted-color ()
"Get muted color that works well in both light and dark themes."
(supertag-view-helper-get-theme-adaptive-color "#666666" "#AAAAAA"))
(defun supertag-view-helper-get-success-color ()
"Get success color that works well in both light and dark themes."
(supertag-view-helper-get-theme-adaptive-color "#22C55E" "#4ADE80"))
(defun supertag-view-helper-get-warning-color ()
"Get warning color that works well in both light and dark themes."
(supertag-view-helper-get-theme-adaptive-color "#F59E0B" "#FCD34D"))
(defun supertag-view-helper-get-error-color ()
"Get error color that works well in both light and dark themes."
(supertag-view-helper-get-theme-adaptive-color "#EF4444" "#F87171"))
(defun supertag-view-helper-get-background-color ()
"Get background color that works well in both light and dark themes."
(supertag-view-helper-get-theme-adaptive-color "#F8FAFC" "#1E293B"))
(defun supertag-view-helper-get-border-color ()
"Get border color that works well in both light and dark themes."
(supertag-view-helper-get-theme-adaptive-color "#E2E8F0" "#334155"))
;;; --- Value Formatting ---
(defun supertag-view-helper-format-value (value)
"Format VALUE for display. Handles lists and nil."
(let ((formatted-value (if (listp value)
(mapconcat #'identity value " / ")
(format "%s" (or value "")))))
(if (string-empty-p formatted-value)
(propertize "[Empty]" 'face `(:foreground ,(supertag-view-helper-get-muted-color) :slant italic))
(supertag-view-helper-render-org-links formatted-value))))
(defun supertag-view-helper-render-org-links (text)
"Return TEXT with Org-style links rendered as clickable buttons.
TEXT can be any value convertible to string."
(let ((string (cond
((null text) "")
((stringp text) (substring-no-properties text))
(t (format "%s" text)))))
(if (string-empty-p string)
string
(with-temp-buffer
(insert string)
(goto-char (point-min))
(while (re-search-forward "\\[\\[\\([^]\n]+\\)\\]\\(\\[\\([^]]+\\)\\]\\)?\\]" nil t)
(let* ((match-start (match-beginning 0))
(link (match-string 1))
(desc (or (match-string 3) (match-string 1)))
(link-target link)
(keymap (let ((map (make-sparse-keymap)))
(define-key map (kbd "RET")
(lambda ()
(interactive)
(org-link-open-from-string (format "[[%s]]" link-target))))
(define-key map [mouse-1]
(lambda ()
(interactive)
(org-link-open-from-string (format "[[%s]]" link-target))))
map)))
(delete-region match-start (match-end 0))
(goto-char match-start)
(insert desc)
(add-text-properties match-start (+ match-start (length desc))
`(face org-link
help-echo ,link
mouse-face highlight
keymap ,keymap))
(goto-char (+ match-start (length desc)))))
(buffer-substring (point-min) (point-max))))))
(defun supertag-view-helper-format-tag-value (value)
"Format tag VALUE with special styling for tags."
(if (or (null value) (string-empty-p (format "%s" value)))
(propertize "[No tags]" 'face `(:foreground ,(supertag-view-helper-get-muted-color) :slant italic))
(let ((tags (if (stringp value)
(split-string value "," t "[ \t\n\r]+")
(if (listp value) value (list (format "%s" value))))))
(mapconcat (lambda (tag)
(propertize (concat "#" (string-trim tag))
'face `(:foreground ,(supertag-view-helper-get-emphasis-color)
:weight bold
:box (:line-width 1 :color ,(supertag-view-helper-get-border-color)))))
tags " "))))
(defun supertag-view-helper-format-boolean-value (value)
"Format boolean VALUE with visual indicators."
(let ((bool-val (cond
((or (eq value t) (string= value "true") (string= value "yes") (string= value "1")) t)
((or (eq value nil) (string= value "false") (string= value "no") (string= value "0")) nil)
(t nil))))
(if bool-val
(propertize "✓ True" 'face `(:foreground ,(supertag-view-helper-get-success-color) :weight bold))
(propertize "✗ False" 'face `(:foreground ,(supertag-view-helper-get-muted-color))))))
(defun supertag-view-helper-format-number-value (value)
"Format numeric VALUE with proper styling."
(if (or (null value) (string-empty-p (format "%s" value)))
(propertize "[No value]" 'face `(:foreground ,(supertag-view-helper-get-muted-color) :slant italic))
(propertize (format "%s" value) 'face `(:foreground ,(supertag-view-helper-get-accent-color) :weight bold))))
(defun supertag-view-helper-format-date-value (value)
"Format date VALUE with calendar icon."
(if (or (null value) (string-empty-p (format "%s" value)))
(propertize "[No date]" 'face `(:foreground ,(supertag-view-helper-get-muted-color) :slant italic))
(propertize (concat "📅 " (format "%s" value))
'face `(:foreground ,(supertag-view-helper-get-accent-color)))))
(defun supertag-view-helper-format-url-value (value)
"Format URL VALUE as clickable link."
(if (or (null value) (string-empty-p (format "%s" value)))
(propertize "[No URL]" 'face `(:foreground ,(supertag-view-helper-get-muted-color) :slant italic))
(propertize (concat "🔗 " (format "%s" value))
'face `(:foreground ,(supertag-view-helper-get-accent-color) :underline t)
'mouse-face 'highlight
'help-echo "Click to open URL")))
(defun supertag-view-helper-format-field-value (field-def value)
"Format field VALUE according to its FIELD-DEF type."
(let ((field-type (plist-get field-def :type)))
(pcase field-type
(:tag (supertag-view-helper-format-tag-value value))
(:boolean (supertag-view-helper-format-boolean-value value))
((or :number :integer) (supertag-view-helper-format-number-value value))
((or :date :timestamp) (supertag-view-helper-format-date-value value))
(:url (supertag-view-helper-format-url-value value))
;; For node-reference fields, prefer showing human-readable titles.
(:node-reference
(let ((ids (and value (supertag-field-normalize-node-reference-list value))))
(if (or (null ids) (null (car ids)))
;; Reuse generic empty formatting to show [Empty].
(supertag-view-helper-format-value nil)
(let* ((titles (mapcar (lambda (id)
(let* ((node (and (stringp id) (supertag-view-api-get-entity :nodes id)))
(title (and node (or (plist-get node :title) id))))
(or title id)))
ids))
(joined (mapconcat #'identity titles ", ")))
(supertag-view-helper-render-org-links joined)))))
(_ (supertag-view-helper-format-value value)))))
;;; --- Simple and Clean Components ---
(defun supertag-view-helper-insert-simple-header (title &optional stats)
"Insert a simple header with TITLE and optional STATS."
(insert (propertize title 'face `(:weight bold :height 1.3 :foreground ,(supertag-view-helper-get-emphasis-color))))
(when stats
(insert (propertize (format " %s" stats)
'face `(:foreground ,(supertag-view-helper-get-muted-color)))))
(insert "\n")
(insert (propertize (make-string 60 ?─) 'face `(:foreground ,(supertag-view-helper-get-border-color))))
(insert "\n\n"))
(defun supertag-view-helper-insert-section-title (title &optional icon)
"Insert a simple section title with optional ICON."
(insert (propertize (if icon (format "%s %s" icon title) title)
'face `(:weight bold :foreground ,(supertag-view-helper-get-emphasis-color))))
(insert "\n"))
(defun supertag-view-helper-insert-field-line (field-name value field-def &optional interactive-props)
"Insert a simple field line with FIELD-NAME, VALUE, FIELD-DEF and optional INTERACTIVE-PROPS."
(let* ((formatted-value (supertag-view-helper-format-field-value field-def value))
(field-type (plist-get field-def :type))
(type-icon (pcase field-type
(:string "📝")
(:number "🔢")
(:integer "🔢")
(:boolean "☑")
(:date "📅")
(:timestamp "⏰")
(:url "🔗")
(:email "📧")
(:tag "🏷")
(_ "📄")))
(start-pos (point)))
(insert " ")
(insert (propertize (format "%s %s" type-icon field-name)
'face `(:weight bold :foreground ,(supertag-view-helper-get-emphasis-color))))
(let* ((dots-length (max 3 (- 35 (length field-name)))))
(insert (propertize (concat " " (make-string dots-length ?.))
'face `(:foreground ,(supertag-view-helper-get-muted-color)))))
(insert " ")
(let ((value-start (point)))
(insert formatted-value)
;; Mark value column for precise cursor placement in views
(add-text-properties value-start (point) '(supertag-value-column t)))
(insert "\n")
;; Add text properties for interactivity
(when interactive-props
(add-text-properties start-pos (point) interactive-props))))
(defun supertag-view-helper-insert-simple-footer (&rest help-lines)
"Insert a simple footer with essential HELP-LINES."
(insert "\n")
(insert (propertize (make-string 60 ?─) 'face `(:foreground ,(supertag-view-helper-get-border-color))))
(insert "\n")
(dolist (line help-lines)
(insert (propertize (format "%s\n" line)
'face `(:foreground ,(supertag-view-helper-get-muted-color) :height 0.9)))))
(defun supertag-view-helper-insert-simple-empty-state (message)
"Insert a simple empty state MESSAGE."
(insert (propertize (format " %s\n" message)
'face `(:foreground ,(supertag-view-helper-get-muted-color) :slant italic))))
(defun supertag-view-helper-insert-tag-block (tag-id fields node-id)
"Insert a simple tag block with TAG-ID, FIELDS for NODE-ID."
(insert (propertize (format "🏷️ %s\n" tag-id)
'face `(:weight bold :foreground ,(supertag-view-helper-get-accent-color))
'supertag-context t
'type :tag
'tag-id tag-id
'field-name nil
'id tag-id))
(if (or (null fields) (zerop (length fields)))
(supertag-view-helper-insert-simple-empty-state "No fields defined")
(dolist (field-def fields)
(let* ((field-name (plist-get field-def :name))
(value (supertag-field-get-with-default node-id tag-id field-name))
(interactive-props `(field-name ,field-name
tag-id ,tag-id
field-def ,field-def
supertag-context t
type :field-value
mouse-face highlight
help-echo "Click to edit this field")))
(supertag-view-helper-insert-field-line field-name value field-def interactive-props))))
(insert "\n"))
;;; --- Card-like Components ---
(defun supertag-view-helper-insert-card-start (title &optional icon color)
"Start a card-like container with TITLE, optional ICON and COLOR."
(let ((card-color (or color (supertag-view-helper-get-border-color)))
(title-with-icon (if icon (format "%s %s" icon title) title)))
(insert (propertize (format "┌─ %s " title-with-icon)
'face `(:weight bold :foreground ,(supertag-view-helper-get-emphasis-color))))
(insert (propertize (make-string (max 0 (- 60 (length title-with-icon) 3)) ?─)
'face `(:foreground ,card-color)))
(insert (propertize "┐\n" 'face `(:foreground ,card-color)))))
(defun supertag-view-helper-insert-card-content (content &optional padding)
"Insert CONTENT inside a card with optional PADDING."
(let ((pad (or padding "│ "))
(border-color (supertag-view-helper-get-border-color)))
(dolist (line (split-string content "\n"))
(insert (propertize pad 'face `(:foreground ,border-color)))
(insert line "\n"))))
(defun supertag-view-helper-insert-card-end ()
"End a card-like container."
(let ((border-color (supertag-view-helper-get-border-color)))
(insert (propertize "└" 'face `(:foreground ,border-color)))
(insert (propertize (make-string 58 ?─) 'face `(:foreground ,border-color)))
(insert (propertize "┘\n" 'face `(:foreground ,border-color)))))
(defun supertag-view-helper-insert-field-row (field-name value field-def &optional interactive-props)
"Insert a formatted field row with FIELD-NAME, VALUE, FIELD-DEF and optional INTERACTIVE-PROPS."
(let ((border-color (supertag-view-helper-get-border-color))
(formatted-value (supertag-view-helper-format-field-value field-def value))
(field-type (plist-get field-def :type))
(type-icon (pcase (plist-get field-def :type)
(:string "📝")
(:number "🔢")
(:integer "🔢")
(:boolean "☑️")
(:date "📅")
(:timestamp "⏰")
(:url "🔗")
(:email "📧")
(:tag "🏷️")
(_ "📄")))
(start-pos (point)))
;; Left border
(insert (propertize "│ " 'face `(:foreground ,border-color)))
;; Field icon and name
(insert (propertize (format "%s %s" type-icon field-name)
'face `(:weight bold :foreground ,(supertag-view-helper-get-emphasis-color))))
;; Separator dots
(let ((dots-length (max 2 (- 25 (length field-name)))))
(insert (propertize (concat " " (make-string dots-length ?.))
'face `(:foreground ,(supertag-view-helper-get-muted-color)))))
;; Field value (mark the value region so views can place point directly here)
(let ((value-start (1+ (point)))) ; after the separating space
(insert " " formatted-value)
(add-text-properties value-start (point) '(supertag-value-column t)))
;; Right border and padding
(let ((line-length (- (point) start-pos 2))
(target-length 56))
(when (< line-length target-length)
(insert (make-string (- target-length line-length) ?\ ))))
(insert (propertize " │\n" 'face `(:foreground ,border-color)))
;; Add text properties for interactivity
(when interactive-props
(add-text-properties start-pos (point) interactive-props))))
(defun supertag-view-helper-insert-separator-line (&optional style)
"Insert a separator line with optional STYLE (:thin, :thick, :dotted)."
(let ((char (pcase style
(:thin "─")
(:thick "━")
(:dotted "┄")
(_ "─")))
(border-color (supertag-view-helper-get-border-color)))
(insert (propertize "│" 'face `(:foreground ,border-color)))
(insert (propertize (make-string 58 (string-to-char char)) 'face `(:foreground ,border-color)))
(insert (propertize "│\n" 'face `(:foreground ,border-color)))))
;;; --- Interactive Line Highlighting ---
(defun supertag-view-helper-highlight-current-line ()
"Highlight the current line for better visibility."
(let ((inhibit-read-only t))
(remove-overlays (point-min) (point-max) 'category 'current-line)
(let ((overlay (make-overlay (line-beginning-position) (1+ (line-end-position)))))
(overlay-put overlay 'category 'current-line)
(overlay-put overlay 'face `(:background ,(supertag-view-helper-get-theme-adaptive-color "#F1F5F9" "#334155")
:extend t
:box (:line-width 1 :color ,(supertag-view-helper-get-accent-color))))
;; Add subtle animation effect
(overlay-put overlay 'priority 100))))
(defun supertag-view-helper-unhighlight-all-lines ()
"Remove all line highlighting."
(remove-overlays (point-min) (point-max) 'category 'current-line))
(defun supertag-view-helper-enable-line-highlighting ()
"Enable enhanced line highlighting for the current buffer."
;; Line highlighting disabled to prevent visual flickering during cursor movement
;; (supertag-view-helper-unhighlight-all-lines)
;; (add-hook 'post-command-hook #'supertag-view-helper-highlight-current-line nil t)
)
;;; --- Status and Statistics Display ---
(defun supertag-view-helper-insert-status-badge (status &optional label)
"Insert a status badge with STATUS and optional LABEL."
(let* ((badge-text (or label (format "%s" status)))
(badge-face (pcase status
('active `(:background ,(supertag-view-helper-get-success-color)
:foreground "white" :weight bold :box 2))
('warning `(:background ,(supertag-view-helper-get-warning-color)
:foreground "black" :weight bold :box 2))
('error `(:background ,(supertag-view-helper-get-error-color)
:foreground "white" :weight bold :box 2))
('inactive `(:background ,(supertag-view-helper-get-muted-color)
:foreground "white" :weight bold :box 2))
(_ `(:background ,(supertag-view-helper-get-accent-color)
:foreground "white" :weight bold :box 2)))))
(insert " ")
(insert (propertize (format " %s " badge-text) 'face badge-face))
(insert " ")))
(defun supertag-view-helper-insert-stats-summary (stats)
"Insert a statistics summary from STATS plist."
(let ((total (or (plist-get stats :total) 0))
(active (or (plist-get stats :active) 0))
(modified (or (plist-get stats :modified) 0)))
(insert (propertize "\n📊 Statistics: " 'face `(:weight bold :foreground ,(supertag-view-helper-get-emphasis-color))))
(insert (propertize (format "Total: %d" total) 'face `(:weight bold :foreground ,(supertag-view-helper-get-accent-color))))
(insert (propertize " • " 'face `(:foreground ,(supertag-view-helper-get-muted-color))))
(insert (propertize (format "Active: %d" active) 'face `(:weight bold :foreground ,(supertag-view-helper-get-success-color))))
(when (> modified 0)
(insert (propertize " • " 'face `(:foreground ,(supertag-view-helper-get-muted-color))))
(insert (propertize (format "Modified: %d" modified) 'face `(:weight bold :foreground ,(supertag-view-helper-get-warning-color)))))
(insert "\n\n")))
;;; --- Help Text and Empty State ---
(defun supertag-view-helper-insert-help-text (text)
"Insert help TEXT with consistent styling."
(insert (propertize (format " %s\n" text)
'face `(:foreground ,(supertag-view-helper-get-muted-color) :height 0.9))))
(defun supertag-view-helper-insert-empty-state (message)
"Insert empty state MESSAGE with consistent styling."
(insert (propertize (format " %s\n" message)
'face `(:foreground ,(supertag-view-helper-get-muted-color) :slant italic))))
(defun supertag-view-helper-insert-field-help ()
"Insert standard field help text."
(supertag-view-helper-insert-help-text "[RET] Edit Value [E] Edit Field Definition [a] Add Field [d] Delete Field [M-↑/↓] Reorder"))
;;; --- Node and File Information Display ---
(defun supertag-view-helper-insert-node-info (title file)
"Insert node information with TITLE and FILE in consistent format."
(insert (propertize (format " 📄 %s\n" title) 'face '(:weight bold :foreground "default")))
(when file
(insert (propertize (format " 📁 %s\n" (file-name-nondirectory file))
'face `(:foreground ,(supertag-view-helper-get-muted-color) :slant italic)))))
;;; --- Footer and Navigation ---
(defun supertag-view-helper-insert-footer-start ()
"Insert footer separator and navigation title."
(insert (propertize (format "%s\n" supertag-view-helper--section-separator) 'face `(:foreground ,(supertag-view-helper-get-muted-color))))
(insert (propertize "Navigation:\n" 'face '(:weight bold :foreground "default"))))
(defun supertag-view-helper-insert-footer-with-content (&rest help-lines)
"Insert complete footer with custom HELP-LINES."
(supertag-view-helper-insert-footer-start)
(dolist (line help-lines)
(supertag-view-helper-insert-help-text line)))
;;;----------------------------------------------------------------------
;;; Tag Text Operations
;;;----------------------------------------------------------------------
(defun supertag-view-helper-find-node-location (node-id)
"Find the location of a node by its ID.
Returns a cons (POSITION . FILE-PATH) if found, nil otherwise."
(when node-id
(when-let* ((node (supertag-view-api-get-entity :nodes node-id)))
(let ((file-path (plist-get node :file))
(position (plist-get node :position)))
(when (and file-path (file-exists-p file-path))
(cons (or position 1) file-path))))))
(defun supertag-view-helper-at-tag-line-p ()
"Check if the current line is a tag line (contains #tags)."
(save-excursion
(beginning-of-line)
(looking-at-p (concat "^[ \t]*#[" supertag-view-helper--valid-tag-chars "]+"))))
(defun supertag-view-helper-find-tag-insertion-point ()
"Find the best position to insert tags in the current node.
Returns the position where tags should be inserted."
(save-excursion
(org-back-to-heading t)
(let ((drawer-end (supertag-view-helper--find-drawer-end))
(existing-tag-line (supertag-view-helper--find-existing-tag-line)))
(cond
;; If there's already a tag line, position at the end of it
(existing-tag-line
(goto-char existing-tag-line)
(end-of-line)
(point))
;; If there's a drawer, position after it
(drawer-end
(goto-char drawer-end)
(unless (bolp) (forward-line 1))
;; Skip empty lines after drawer
(while (and (not (eobp))
(not (org-at-heading-p))
(looking-at-p "^[ \t]*$"))
(forward-line 1))
(beginning-of-line)
(point))
;; No drawer, position directly after headline
(t
(end-of-line)
(insert "\n")
(beginning-of-line)
(point))))))
(defun supertag-view-helper--find-drawer-end ()
"Find the end position of the current node's drawer."
(when (and (eq major-mode 'org-mode)
(fboundp 'org-element-at-point))
(save-excursion
(org-back-to-heading t)
(forward-line 1)
(let ((end-pos nil)
(section-end (save-excursion
(org-end-of-subtree t t)
(point))))
;; Find the end position of the last drawer
(while (and (< (point) section-end)
(not (org-at-heading-p)))
(let ((element (org-element-at-point)))
(cond
;; Property drawer
((eq (org-element-type element) 'property-drawer)
(setq end-pos (org-element-property :end element))
(goto-char end-pos))
;; Other drawers
((eq (org-element-type element) 'drawer)
(setq end-pos (org-element-property :end element))
(goto-char end-pos))
;; Other elements, continue forward
(t
(forward-line 1)))))
end-pos))))
(defun supertag-view-helper--find-existing-tag-line ()
"Find the position of an existing tag line in the current node."
(save-excursion
(org-back-to-heading t)
(let ((drawer-end (supertag-view-helper--find-drawer-end))
(section-end (save-excursion
(org-end-of-subtree t t)
(point)))
(tag-line-pos nil))
;; Start searching from the drawer end position, or from the heading if no drawer
(goto-char (or drawer-end (progn (end-of-line) (point))))
(forward-line 1)
;; Search for lines containing #tags within the node range
(while (and (< (point) section-end)
(not (org-at-heading-p))
(not tag-line-pos))
(beginning-of-line)
;; Check if the current line contains #tags (but not comment lines)
(when (and (looking-at-p (concat "^[ \t]*#[" supertag-view-helper--valid-tag-chars "]+"))
(not (looking-at-p "^[ \t]*#\\+"))) ; Exclude org keywords
(setq tag-line-pos (point)))
(forward-line 1))
tag-line-pos)))
(defun supertag-view-helper-insert-tag-text (tag-name &optional position)
"Insert tag text with intelligent spacing.
TAG-NAME is the tag name to display.
POSITION is optional insertion position.
This function works in any location within a node - heading or content area."
(when position (goto-char position))
(let* ((prev-char (char-before))
(next-char (char-after))
(at-tag-line (supertag-view-helper-at-tag-line-p))
;; Need space before if previous char exists and is not whitespace
(need-space-before
(and prev-char
(not (memq prev-char '(?\s ?\t ?\n)))
(or at-tag-line
;; Check for alphanumeric or punctuation that needs separation
(and (>= prev-char ?a) (<= prev-char ?z))
(and (>= prev-char ?A) (<= prev-char ?Z))
(and (>= prev-char ?0) (<= prev-char ?9))
;; Include common punctuation that should be separated
(memq prev-char '(?. ?, ?\; ?: ?! ?\? ?\)))
;; Chinese/Japanese/Korean characters
(and (>= prev-char ?\u4e00) (<= prev-char ?\u9fff)))))
;; Need space after only if next char exists and is alphanumeric or chinese
(need-space-after
(and next-char
(not (eq next-char ?\n))
(not (memq next-char '(?\s ?\t ?. ?, ?\; ?: ?! ?\?)))
(or (and (>= next-char ?a) (<= next-char ?z))
(and (>= next-char ?A) (<= next-char ?Z))
(and (>= next-char ?0) (<= next-char ?9))
;; Chinese/Japanese/Korean characters
(and (>= next-char ?\u4e00) (<= next-char ?\u9fff))))))
;; Insert space before tag if needed
(when need-space-before
(insert " "))
;; Insert the tag
(insert (concat "#" tag-name))
;; Insert space after tag if needed
(when need-space-after
(insert " "))))
(defun supertag-view-helper-remove-tag-text (tag-name)
"Remove all occurrences of #TAG-NAME from the current node.
TAG-NAME is the tag name to remove."
(save-excursion
(org-back-to-heading t)
(let ((subtree-end (save-excursion
(if (outline-next-heading) (point) (point-max))))
(case-fold-search nil)
(removed-count 0))
(beginning-of-line)
(while (re-search-forward supertag-inline-tag-regexp subtree-end t)
(when (equal tag-name (match-string-no-properties 2))
(delete-region (1- (match-beginning 2)) (match-end 2))
(setq subtree-end (- subtree-end (1+ (length tag-name))))
(setq removed-count (1+ removed-count))))
removed-count)))
(defun supertag-view-helper-get-tag-at-point ()
"Get the inline supertag name at point.
Returns the tag name (without #) if found, nil otherwise."
(when (derived-mode-p 'org-mode)
(save-excursion
(let ((origin (point))
(line-end (line-end-position)))
(goto-char (line-beginning-position))
(catch 'tag
(while (supertag-view-helper--font-lock-matcher line-end)
(when (and (<= (match-beginning 0) origin)
(< origin (match-end 0)))
(throw 'tag (substring (match-string-no-properties 0) 1)))))))))
(defun supertag-view-helper-rename-tag-text-in-node (old-tag-name new-tag-name)
"Rename #OLD-TAG-NAME to #NEW-TAG-NAME within the current node."
(save-excursion
(org-back-to-heading t)
(let ((beg (point))
(end (save-excursion
(if (outline-next-heading) (point) (point-max))))
(renamed-count 0))
(narrow-to-region beg end)
(goto-char (point-min))
(while (re-search-forward supertag-inline-tag-regexp nil t)
(when (and (equal old-tag-name (match-string-no-properties 2))
(not (or (save-excursion
(goto-char (match-beginning 0))
(org-in-src-block-p))
(save-excursion
(goto-char (match-beginning 0))
(beginning-of-line)
(looking-at-p "^[ \t]*#\\+")))))
(replace-match new-tag-name t t nil 2)
(setq renamed-count (1+ renamed-count))))
(widen)
renamed-count)))
;;;----------------------------------------------------------------------
;;; Cross-File Tag Text Operations
;;;----------------------------------------------------------------------
(defun supertag-view-helper-remove-tag-text-from-files (tag-name files)
"Remove all occurrences of #TAG-NAME from specified FILES.
TAG-NAME is the tag name to remove.
FILES is a list of file paths.
Returns the total number of instances removed."
(let ((total-removed 0))
(dolist (file files)
(when (and file (file-exists-p file))
(with-current-buffer (find-file-noselect file)
(save-excursion
(goto-char (point-min))
(let ((removed-count 0))
(while (re-search-forward supertag-inline-tag-regexp nil t)
(let ((start (1- (match-beginning 2)))
(end (match-end 2)))
;; Only remove tags that are not in comments or code blocks
(when (and
(equal tag-name (match-string-no-properties 2))
(not
(or (save-excursion
(goto-char start)
(org-in-src-block-p))
(save-excursion
(goto-char start)
(beginning-of-line)
(looking-at-p "^[ \t]*#\\+")))))
;; Smart deletion with space handling
(let* ((prev-char (char-before start))
(next-char (char-after end))
(delete-start start)
(delete-end end))
;; Keep one separator when the token sits between prose.
(cond
((and prev-char next-char
(eq prev-char ?\s) (eq next-char ?\s))
(setq delete-end (1+ delete-end)))
((and prev-char (eq prev-char ?\s)
(or (null next-char) (eq next-char ?\n)))
(setq delete-start (1- delete-start))))
(delete-region delete-start delete-end)
(setq removed-count (1+ removed-count))))))
(when (> removed-count 0)
(save-buffer)
(setq total-removed (+ total-removed removed-count))
(message "Removed %d instances of #%s from %s"
removed-count tag-name
(file-name-nondirectory file))))))))
total-removed))
(defun supertag-view-helper-rename-tag-text-in-buffer (old-tag-name new-tag-name)
"Rename OLD-TAG-NAME to NEW-TAG-NAME in inline tags and FILETAGS.
Returns the total number of instances renamed."
(save-excursion
(goto-char (point-min))
(let ((renamed-count 0))
(while (re-search-forward supertag-inline-tag-regexp nil t)
(when (and (string= (match-string-no-properties 2) old-tag-name)
(not (or (save-excursion
(goto-char (match-beginning 0))
(org-in-src-block-p))
(save-excursion
(goto-char (match-beginning 0))
(beginning-of-line)
(looking-at-p "^[ \t]*#\\+")))))
(replace-match new-tag-name t t nil 2)
(setq renamed-count (1+ renamed-count))))
(goto-char (point-min))
(let ((case-fold-search t)
(pattern
(concat "\\(^\\|[[:space:]:]\\)\\("
(regexp-quote old-tag-name)
"\\)\\([[:space:]:]\\|$\\)")))
(while (re-search-forward "^#\\+FILETAGS:[ \t]*\\(.*\\)$" nil t)
(let ((begin (match-beginning 1))
(end (copy-marker (match-end 1))))
(save-restriction
(narrow-to-region begin end)
(goto-char (point-min))
(while (re-search-forward pattern nil t)
(let ((prefix (match-string-no-properties 1))
(suffix (match-string-no-properties 3)))
(replace-match (concat prefix new-tag-name suffix) t t)
;; Revisit a consumed separator so adjacent tokens match.
(unless (string-empty-p suffix)
(backward-char)))
(setq renamed-count (1+ renamed-count))))
(set-marker end nil))))
renamed-count)))
(defun supertag-view-helper-rename-tag-text-in-files (old-tag-name new-tag-name files)
"Rename all occurrences of #OLD-TAG-NAME to #NEW-TAG-NAME in specified FILES.
OLD-TAG-NAME is the current tag name.
NEW-TAG-NAME is the new tag name.
FILES is a list of file paths.
Returns the total number of instances renamed."
(let ((total-renamed 0))
(dolist (file files)
(when (and file (file-exists-p file))
(with-current-buffer (find-file-noselect file)
(save-excursion
(let ((renamed-count
(supertag-view-helper-rename-tag-text-in-buffer
old-tag-name new-tag-name)))
(when (> renamed-count 0)
(save-buffer)
(setq total-renamed (+ total-renamed renamed-count))
(message "Renamed %s instances from #%s to #%s in %s"
renamed-count old-tag-name new-tag-name
(file-name-nondirectory file))))))))
;; Ensure we always return a number, never nil
(or total-renamed 0)))
;;;----------------------------------------------------------------------
;;; Display Buffer Management
;;;----------------------------------------------------------------------
(defun supertag-view-helper-display-buffer-right (buffer)
"Display BUFFER in a window to the right."
(let ((window (display-buffer buffer '(display-buffer-in-side-window
(side . right)
(window-width . 0.4)))))
(select-window window)
(goto-char (point-min))))
(provide 'supertag-view-helper)
;;; supertag-view-helper.el ends here