summary refs log tree commit diff
path: root/emacs/init.el
blob: ca9edce21b836961b7dda8a13cd79ac85375f5c9 (plain)
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
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
;;; init.el --- my emacs configuration  -*- lexical-binding: t; -*-

(add-to-list 'load-path (expand-file-name (concat user-emacs-directory "site-lisp")))

(setq garbage-collection-messages t)
(add-hook 'emacs-startup-hook
          #'(lambda ()
              (message (format "Initialised in %s seconds with %s garbage collections." (emacs-init-time) gcs-done))
                ;;; reset garbage collector
              (setq
               gc-cons-threshold 800000
               gc-cons-percentage 0.2)))

(setopt user-mail-address "noa@gaiwan.org")

(setopt indent-tabs-mode nil)

;; properly distinguish these chords from their ascii legacy
(define-key input-decode-map [?\C-m] [C-m])

(use-package tubthumping-theme
	:config
	(load-theme 'tubthumping t))


(global-set-key (kbd "<backspace>") 'backward-delete-char-untabify)

;; I make my caps lock a menu key, so i can open the command palette with it
;; https://alexschroeder.ch/wiki/2020-07-16_Emacs_everything

(define-key context-menu-mode-map (kbd "<menu>") nil)
(global-unset-key (kbd "M-x"))
(global-set-key (kbd "<menu>") 'execute-extended-command)

(setopt
	cursor-type 'bar
	minibuffer-depth-indicate-mode t)

(setopt
	url-cookie-trusted-urls '()
	url-cookie-untrusted-urls '(".*"))


;;; candidate completion
(use-package vertico
	:ensure t
	:custom
	(vertico-mode t)
	(vertico-count 12)
	(vertico-cycle t)
	(read-buffer-completion-ignore-case t)
	(read-file-name-completion-ignore-case t)
	(completion-ignore-case t)
)
(use-package vertico-directory
	:after vertico
	:ensure nil
	;; More convenient directory navigation commands
	:bind (
		:map vertico-map
		("RET" . vertico-directory-enter)
		("<backspace>" . vertico-directory-delete-char)
		("C-<backspace>" . vertico-directory-delete-word)
	)
	;; Tidy shadowed file names
	:hook (rfn-eshadow-update-overlay . vertico-directory-tidy)
)

(use-package orderless
	:ensure t
	:custom
	(completion-styles '(orderless basic))
	(completion-category-defaults nil)
	(completion-category-overrides '((file (styles partial-completion)))))

(setopt confirm-nonexistent-file-or-buffer 'after-completion)

(defun noa/mode-line-modified ()
	(cond
		(buffer-read-only "RO")
		((buffer-modified-p) "**")
		(t "RW")))

;;; Replace the mode line with a header line

;; First, we set the mode line to nil.  On my graphical display, this collapses it so all i get is a thin black line separating the buffer from the echo area.
(setq-default mode-line-format nil)
;; But the mode line still holds some useful information that i want to see.  I would rather that be in the header line, because to me it makes sense for this kind of metadata to be /above/ the buffer it is describing.
(setq-default header-line-format '(
	;; First, in white on black text, i want the information about the state of the file.  This will show three hyphens in the top left corner of the header line.  The first two hyphens mean that the file is both writable and unchanged.  If the buffer has been changed, they will change to two asterisks.  If the buffer is read only, they will change two percentage symbols.  And if the buffer is read only and has been changed, the first will change to a percentage symbol, and the second will change to an asterisk.  The final hyphen represents that the file is local, specifically that the default-directory variable is local.  If it is remote, an at symbol will be displayed instead.
	(:propertize
		("" mode-line-modified mode-line-remote)
		face highlight
	)
	;; Next, we want to display the buffer name.  For buffers which belong to files, this will usually be the file name, but it is likely to be something more informative for special buffers.
	" %b"
	;; Below that, show a line and column coördinate.  There are special minor modes that will enable or disable this for the default mode line, but i ignore that and put the formatting code here directly.  The docstring for the mode-line-format variable suggests that the column might not be displayed correctly in some situations without enabling the minor mode, but i haven't noticed that yet so i don't bother.  This column number is zero-indexed; a capital c would make it one-indexed.  For now i stick with zero-indexed as that's the emacs default and i'm not sure which is better.  I guess it makes a bit more sense that the first character on a line is labeled "1".
	":%l,%c"
	"   "
	;; I don't know exactly what this variable covers, so i keep it here so that if something shows up i know that it gets put here.  Because i have a global mode line in my tab bar, some of the things that would otherwise be here (like the time, battery percentage, and notifications for chat buffers) don't show up.
	mode-line-misc-info
	mode-line-end-spaces
	)
)

(use-package beacon
	:ensure t
	:delight
	:custom (beacon-mode t))

(use-package jabber
  :ensure t
  :init
  (setq noa/jabber-activity-dont-show '("#tildetown%town@irc.hmm.st"
                                        "#meta%tilde.chat@irc.hmm.st"
                                        "hmm@conference.hmm.st"))
  (defun noa/jabber-activity-show-p (jid)
    "Return non-nil if JID should be hidden.
A JID should be hidden when there is an invisible buffer for JID,
when JID is not in `noa/jabber-activity-dont-show',
and when JID is not in `jabber-activity-banned'."
    (let ((buffer (jabber-activity-find-buffer-name jid)))
      (and (buffer-live-p buffer)
           (not (get-buffer-window buffer 'visible))
           (not (cl-dolist (entry jabber-activity-banned)
                  (when (string-match entry jid)
                    (cl-return t))))
           (not (cl-dolist (entry noa/jabber-activity-dont-show)
                  (when (string-match entry jid)
                    (cl-return t)))))))
  :custom
  (jabber-history-enabled t)
  (jabber-account-list '(("noa@hmm.st")))
  (jabber-keepalive-interval 60)
  (jabber-activity-show-p #'noa/jabber-activity-show-p))

(use-package nov
	:ensure t
	:mode ("\\.epub\\'" . nov-mode))

(use-package shr
	:custom
	(shr-max-width nil))

;; also check out jinx https://github.com/minad/jinx
(use-package spell-fu
	:ensure t
	:hook (text-mode . spell-fu-mode))

;; consult-buffer replaces the buffer menu.  as well as listing buffers, it lists bookmarks and recent files.
(use-package consult
	:ensure t
	:bind (([remap switch-to-buffer] . consult-buffer)	;; also contains file history, etc
	      ([remap yank-pop] . consult-yank-pop)	;; like normal yank-pop but with live preview
	      ([remap goto-line] . consult-goto-line)))

(use-package marginalia
	:ensure t
	:after vertico
	:custom
	(marginalia-mode t)
	(marginalia-max-relative-age most-positive-fixnum)
	(marginalia-align 'right))

;; My keyboard has a tab key and an i key.  For legacy reasons, by default emacs converts C-i to mean the same thing as the tab key, but i don't really want that.  The tab key is called <tab> and it gets translated to TAB.  C-i is TAB, but i'd rather it by C-i.  That's what this decode line does.
(define-key input-decode-map [?\C-i] [C-i])
;; Now that tab and C-i are properly distinguished, i can bind C-i to completion at point.
(global-set-key (kbd "<C-i>") 'completion-at-point)
;; I also want to make the completion at point function a bit more friendly than the default, so i ask consult to provide the completion functionality.
(setopt completion-in-region-function 'consult-completion-in-region)

;; (use-package org-contacts
;; 	:ensure t
;; 	:pin gnu
;; 	:custom
;; 	(org-contacts-files nil)
;; 	:config
;; 	(add-to-list org-roam-capture-templates
;; 		"c" "Contact" plain "%?" :target (file+head "~/data/notes/contacts/${slug}.org" "#+title: %(org-contacts-template-name)\n")
;; 		"* %(org-contacts-template-name)
;; :PROPERTIES:
;; :ADDRESS: %^{289 Cleveland St. Brooklyn, 11206 NY, USA}
;; :BIRTHDAY: %^{yyyy-mm-dd}
;; :EMAIL: %(org-contacts-template-email)
;; :NOTE: %^{NOTE}
;; :END:"
;; 		:empty-lines 1
;; 	)
;; )

(use-package helpful
	:ensure t
	:bind (
		([remap describe-function] . helpful-callable)
		([remap describe-variable] . helpful-variable)
		([remap describe-key] . helpful-key)
		([remap describe-command] . helpful-command)
		("C-c C-d" . helpful-at-point)
		("C-h F" . helpful-function)
	))

(use-package window
	:custom
	(switch-to-buffer-obey-display-actions t))


(use-package org-fc
	:disabled
	:ensure t
	:custom
	(org-fc-directories (expand-file-name "~/data/notes/")))

(use-package artbollocks-mode
	:disabled t
	:ensure t)

(use-package org
	:custom
	(org-use-sub-superscripts "{}")
	;; If we enable this, emphasis markers will be hidden for a more word processor feel.  This has the downside of meaning you have to delete a hidden character to get rid of bold or italic text.  I don't have much of a problem with seeing the emphasis markers so i'm willing to put up with any aesthetic shortcomings for a better user experience.  The package org-appear solves this by hiding them, but showing them when the point is over them, but i don't think having a whole package just for that is worth it.
	(org-hide-emphasis-markers nil)
	(org-startup-with-inline-images t)
	(org-image-actual-width '(300))
	(org-auto-align-tags nil)
	(org-tags-column 0)
	(org-catch-invisible-edits 'show-and-error)
	(org-special-ctrl-a/e t)
	(org-insert-heading-respect-content t)
	(org-ellipsis "…")
	(org-display-custom-times t)
	(org-time-stamp-custom-formats '("%Y-%m-%d" . "%Y-%m-%d %H:%M"))
	(org-extend-today-until 4)
	(org-adapt-indentation nil)
	(org-log-done 'time)
	(org-return-follows-link t)
	(org-agenda-files '("~/data/notes/notes.org"))
	(org-capture-templates
		`(
			("j" "Journal" entry (file+datetree "~/data/notes/notes.org") "* %?\n" :empty-lines 1)
			
		)
	)
	
	(org-agenda-tags-column t)
	(org-agenda-block-separator ?─)
	(org-agenda-time-grid
		'((daily today require-timed)
			(800 1000 1200 1400 1600 1800 2000)
			" ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄"))
	(org-agenda-current-time-string
		"◀── now ─────────────────────────────────────────────────")

	:custom-face
	;;(org-ellipsis ((t (:inherit default :box nil))))
)

(use-package org-modern
	:ensure t
	:after org
	:hook
	(org-mode . org-modern-mode)
	(org-agenda-finalize . org-modern-agenda)
	:custom
	;; There are three ways to make bullet lists in org mode, which seems a bit excessive to me.  I almost always only use the hyphen, but i like my bullet points to look like bullets, so here i overwrite the hyphen display to show a bullet point.  While i'm at it, i overwrite the others too, because they are functionally identical so should probably look the same too.
	(org-modern-list '(
		(?+ . " • ")
		(?- . " • ")
		(?* . " • ")
	))
	(org-modern-star nil)
	(org-modern-keyword nil)
	(org-modern-checkbox nil)
	(org-modern-table nil))

(defun read-file-as-string (filename)
	"Read file contents from FILENAME."
	(with-temp-buffer
		(insert-file-contents filename)
	(buffer-string)))

(setq noa/website-header (read-file-as-string "/home/noa/projects/org-website/templates/header.html"))
(setq noa/website-footer (read-file-as-string "/home/noa/projects/org-website/templates/footer.html"))


;; The index page generation functions were taken from Dennis Ogbe.  Thank you!
(defun my-blog-parse-sitemap-list (l)
	"Convert the sitemap list in to a list of filenames."
	;; LIST looks like:
	;; (unordered ("[[file:uses.org][Things i use]]") ("[[file:media.org][Media Diary]]") ("[[file:tanklobsters.org][Tank lobsters]]"))
	(mapcar
		#'(lambda (i)
			(let
				(
					(link (with-temp-buffer (
						let (
							(org-inhibit-startup nil)
						)
						(insert (car i))
						(org-mode)
						(goto-char (point-min))
						(org-element-link-parser)
					)))
				)
				(when link
					(plist-get (cadr link) :path)
				)
			)
		)
		(cdr l)
	)
)

(defun my-blog-sort-article-list (l p)
  "sort the article list anti-chronologically."
  (sort l #'(lambda (a b)
              (let ((date-a (org-publish-find-date a p))
                    (date-b (org-publish-find-date b p)))
                (not (time-less-p date-a date-b))))))

(defun noa/naive-org-first-paragraph (file)
	"Naively returns the first paragraph of FILE.

	The way that the first paragraph is determined is to assume that there will be an org metadata block beforehand, so look for the first two consecutive newlines and mark the following paragraph."
	(with-temp-buffer
		(insert-file-contents file)
		(goto-char (point-min))
		(re-search-forward "\n\n")
		(mark-paragraph)
		(let
			(
				(beg (mark))
				(end (point))
			)
			(buffer-substring beg end))))

(defun noa/website-sitemap (title list)
	"Generate the index page for my website."
	;; LIST looks like:
	;; (unordered ("[[file:uses.org][Things i use]]") ("[[file:media.org][Media Diary]]") ("[[file:tanklobsters.org][Tank lobsters]]"))
	(with-temp-buffer
		;; mangle the parsed list given to us into a plain lisp list of files
		(let*
			(
				(filenames (my-blog-parse-sitemap-list list))
				(project-plist (assoc "website-pages" org-publish-project-alist))
				(articles (my-blog-sort-article-list filenames project-plist))
			)
			(message (concat "PLIST: " (plist-get project-plist :base-directory)))
			
			(insert "Several parts of this website are broken as i wrangle with the monstrosity that is programming in emacs lisp.  The content should still be fine, but for further cosmetics please hold <3\n\n")
			(dolist (file filenames)
				(let*
					(
						(abspath (file-name-concat "/home/noa/data/share" file))
						;; (abspath (file-name-concat (plist-get project-plist :base-directory) file))
						(relpath (file-relative-name abspath "/home/noa/data/share"))
						(title (org-publish-find-title file project-plist))
						(date (format-time-string (car org-time-stamp-formats) (org-publish-find-date file project-plist)))
						(preview (noa/naive-org-first-paragraph abspath))
					)
					(insert (concat "* [[file:" relpath "][" title "]]\n"))
					(insert (concat
						"*" date ":*"
						preview
					))
					(insert "\n")
					;; (insert (concat "[[file:" relpath "][Read More...]]\n"))
				)
			)
			;; insert a title and save
			(insert "#+TITLE: noa.pub\n")
			(buffer-string)
		)
	)
)

(setq org-publish-project-alist `(
	("website"
		:components ("website-pages" "website-assets"))
	("website-pages"
		:publishing-function org-html-publish-to-html
		:base-directory "/home/noa/data/share"
		:publishing-directory "/home/noa/projects/org-website"
		:base-extension "org"

		:section-numbers nil
		:with-toc nil
		:with-drawers t
		:with-sub-superscript t
		:html-link-home "/"
		:html-head "<link rel=\"icon\" href=\"data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>𰻝</text></svg>\">"
		:html-head-include-default-style nil
		:html-head-include-scripts nil
		:html-doctype "html5"
		;; :html-validation-link nil

		:html-preamble ""
		:html-postamble ,noa/website-footer

		:html-home/up-format ""
		:html-link-up ""

		:html-html5-fancy t
		:html-indent t

		:html-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"love.css\" />
<meta name=\"color-scheme\" content=\"light dark\">"

		:html-divs (
			(preamble "header" "")
			(content "main" "")
			(postamble "footer" ""))

		:auto-sitemap t
		:sitemap-filename "/home/noa/projects/org-website/index.org"
		:sitemap-title "noa.pub"
		:sitemap-style list
		;; :sitemap-format-entry
		:sitemap-function noa/website-sitemap
		:sitemap-sort-folders ignore
		:sitemap-sort-files anti-chronologically
		:sitemap-ignore-case t
	)
	("website-assets"
		:publishing-function org-publish-attachment
		:base-directory "/home/noa/data/share"
		:publishing-directory "/home/noa/projects/org-website"
		:base-extension "css\\|js\\|png|\\jpg"
		:recursive t)))

;; TODO: replace with visual-fill-column
;; https://codeberg.org/joostkremers/visual-fill-column
(use-package olivetti
	:ensure t
	:hook (
		(text-mode . olivetti-mode)
		(eww-after-render . olivetti-mode)
		(nov-post-html-render . olivetti-mode)
		(mu4e-view-mode . olivetti-mode)
	)
)

(use-package ffap
	:custom
	(ffap-file-name-with-spaces t)
	:bind (
		([remap find-file] . find-file-at-point)
		([remap dired] . dired-at-point)
	)
)

(use-package eww
	:custom
	(browse-url-browser-function 'eww-browse-url))

;; Abbrev mode expands one string into another string.  I use it as a simple autocorrect mode.  If i misspell a word, i run C-x a i g which will prompt me for what to expand the previous word into.  I type the correct spelling, and whenever i make that mistake again, it will automatically be corrected.  It's important to be careful not to set something that could be a typo for two words though, because otherwise it gets even more annoying.  Luckily it's easy to update the abbrevs which are stored in ~/.config/emacs/abbrev_defs.  M-x list-abbrevs is also a nice command which shows all the saved abbrevs and how many times they've been expanded.
(add-hook 'text-mode-hook #'abbrev-mode)


(use-package eldoc
	:delight
	:custom
	(global-eldoc-mode t))

;; use a bar cursor and blink it and don't stop blinking it.  i don't know how i feel about this yet to be honest, but it helps me know which window is active so for now i'm keeping it
(setopt
	cursor-type 'bar
	blink-cursor-mode -1
	blink-cursor-interval 0.7)

;; Dired is a really nice package which, as with a lot of emacs, has some dodgy defaults.  Here we round off some of the sharp edges to make it more enjoyable to use.
(use-package dired
	:custom
	;; By default, dired permanently deletes files.  But i have quite a bit of storage and also make bad decisions regularly, so it seems fitting to make use of the wonderful invention that is the trash.  People who have used systems from the last forty years or so will likely be familiar with this innovation.
	(delete-by-moving-to-trash t)
	;; It's not fun to be asked every time whether we want to delete a directory recursively.  It's an understandable default for safety reasons, but because we are not deleting permanently but rather just moving to the trash, it's not such a concern.
	(dired-recursive-deletes 'always)
	;; Recursive copying isn't even destructive, so i definitely don't want to be asked about that.
	(dired-recursive-copies 'always)
	;; After we delete some files or directories, it makes sense to get rid of any buffers which are looking at those files or directories.
	(dired-clean-up-buffers-too nil)
	;; With this set, if we have two dired buffers open next to one another, a rename operation in one will default to the directory shown in the other.  In this way, we can pretend we are using some kind of norton commander like file browser instead of slumming it in emacs.
	(dired-dwim-target t)
	;; These are some useful ls switches.  We have to keep -l.  To show dotfiles as well, we use -a.  To sort numbers by number order instead of lumping together ones, twos, and so on, we use -v.  Because we don't have colour, it's nice to have a clear indicator of what is a file and what is a directory, as well as other different things like symlinks which i never remember.  By using -F, a forward slash is appended to every directory.  And to get more easily understandable file sizes, we use -h, which will tell us the file size in kilobytes or megabytes rather than a huge number that means nothing to me.  I won't explain the meaning of the long flag.
	(dired-listing-switches "-alvFh --group-directories-first"))

;;; indentation: tabs and whitespace settings

;; my rules for inserting tabs are that the tab key should insert tabs.  i personally prefer tabs.  spaces don't work for me because then people go trigger happy with alignment, which doesn't really work with proportional fonts.

;; todo: elastic tabstops
;; https://nickgravgaard.com/elastic-tabstops/
;; https://github.com/tenbillionwords/spaceship-mode

;; always insert \t for tab.
(setopt electric-indent-mode nil
        backward-delete-char-untabify-method nil
        tab-width 8)

;; do a naive duplicate whitespace on return, vi/nano style.
(define-key global-map (kbd "TAB") 'self-insert-command)
(defun naive-return-and-indent ()
  "insert a newline and copy the indentation of the previous line"
  (interactive)
  (open-line 1)
  (let* ((start (progn (beginning-of-line) (point)))
	 (indent (progn (back-to-indentation) (point)))
	 (end (progn (end-of-line) (point)))
	 (whitespace (buffer-substring start indent)))
    (delete-trailing-whitespace start end)
    (beginning-of-line 2)
    (insert whitespace)))
(define-key global-map (kbd "RET") 'naive-return-and-indent)

;;; Interface

;; I want to make sure that various bits of the interface are hidden.  but this isn't an "all gui chrome is useless" rampage.  I personally think the scrollbar is useful, i like the visual indication it gives of how far i am through a file.

;; At the moment, explicitly disabling the menu bar and tool bar does nothing, because i already set there to be no lines displayed for the tool and menu bars in my early-init.el file.
;; (setopt
;; 	menu-bar-mode nil
;; 	tool-bar-mode nil
;; )

;; Tooltips are little popups next to the mouse cursor.  I think this information is helpful, but i like it to appear in a more consistent position, because i find it frustrating when popups cover parts of the ui that i wanted to see.  By disabling tooltip-mode, the contents that would be in a popup is instead shown in the echo area.
(setopt tooltip-mode nil)

;; I see no reason not to immediately show which chords in a key sequence i have already pressed.  Emacs does, however, and instead of letting me set the value of echo-keystrokes to zero to wait zero seconds to show that information, it repurposes zero as a method of disabling the functionality altogether, and provides no special functionality for setting it to nil that would explain why that's not an acceptable method of disabling a feature.  Instead, i have to deal with setting it to nearly zero, and luckily i can't tell the difference.
(setopt echo-keystrokes 0.1)

;; A useful feature when programming is to show matching parentheses.  Show-paren-mode is a global mode.  By default it runs in all buffers except those inheriting from special mode.
(setopt
	show-paren-mode t
	;; This variable means that if there is no non-whitespace character in between the point and the paren, it will be highlighted.  It's useful to highlight parentheses if the point is at the start of the line and the paren is indented.
	show-paren-when-point-in-periphery t
	;; By default, the point has to be after a paren for it to be highlighted.  But often the point will be just inside, in which case it's also helpful for the pair to be highlighted.
	show-paren-when-point-inside-paren t
)

;; populate and enable the context menu
;; (setopt context-menu-functions '(
;; 	context-menu-ffap
;; 	occur-context-menu
;; 	context-menu-region
;; 	context-menu-undo
;; 	goto-address-context-menu)
;; 	context-menu-mode t)
(use-package mouse
	:custom
	(context-menu-mode nil))

(defun noa/helpline ()
	(concat
		"[C-x C-f] Open \t"
		"[M-w] copy \t"
		"[C-w] Cut \t"
		"[C-s] search \t"
		"[C-x C-s] Save \t"
		"[C-y] Paste \t"
		"[C-/] Undo \t"
		"[M-x] Command \t"
	)
)

(use-package tab-bar
	:custom
	(tab-bar-mode t)
	(tab-bar-format '(
		;; noa/helpline
		tab-bar-format-menu-bar
		tab-bar-format-align-right
		tab-bar-format-global
	))
)


(use-package font-lock
	:custom
	(global-font-lock-mode t)
	(font-lock-maximum-decoration nil))

(setopt
	inhibit-startup-screen t
	mouse-drag-and-drop-region nil
	mouse-yank-at-point t
	delete-selection-mode nil	;; deleting should be an explicit action
	)

(global-set-key (kbd "C-t") 'tab-new)

;; shift click to select region with the mouse.  This annoyingly rings the bell for an error
(global-unset-key (kbd "S-<down-mouse-1>"))
(global-set-key (kbd "S-<down-mouse-1>") 'mouse-save-then-kill)

;;; packages
(setopt package-archives '(
	("gnu" . "https://elpa.gnu.org/packages/")
	("nongnu" . "https://elpa.nongnu.org/nongnu/")
	("melpa-stable" . "https://stable.melpa.org/packages/")
	("melpa" . "https://melpa.org/packages/")))

;;; saving

;; backups are pointless in long emacs sessions imo, but autosaves are useful
(setopt make-backup-files nil
	backup-by-copying t
	create-lockfiles nil
	auto-save-mode 1
	auto-save-interval 20  ;; every twenty keystrokes
	auto-save-timeout 5 ;; every 5 seconds
	auto-save-default t
	auto-save-no-message t
	version-control t
	;; this will auto save to the current file
	auto-save-visited-mode t)
(add-hook 'focus-out-hook (lambda () (interactive) (save-some-buffers t)))
(add-hook 'mouse-leave-buffer-hook (lambda () (interactive) (save-some-buffers t)))

(use-package keyfreq
	:ensure t
	:custom
	(keyfreq-mode t)
	(keyfreq-autosave-mode t)
)

;; Emacs uses choppy scrolling by default.  If i scoll with my trackpad, it's nice to have it move tiny amounts at the same time as my fingers, which pixel-scroll-precision-mode allows for.  This also has the benefit of making scrolling over images a little bit of a nicer experience.
(setopt
	pixel-scroll-precision-mode t
	pixel-scroll-precision-use-momentum t)

(use-package smartscan
	:bind (
		("M-n" . smartscan-symbol-go-forward)
		("M-p" . smartscan-symbol-go-backward)
		("<down-mouse-3>" . nil)
		("<mouse-3>" . smartscan-symbol-go-forward)
	)
)

;;; sentences
(setopt sentence-end-double-space nil)

;;; spellcheck
(setopt
	flyspell-mode t
	ispell-program-name "aspell"
	ispell-dictionary "en_GB"
	ispell-extra-args '("--sug-mode=ultra")
)

;; We are on a unix system, so it makes sense to end files in the unix system way.  I'm surprised this isn't the default.
(setopt require-final-newline t)

(setopt
	window-min-height 1
	window-combination-resize t
	window-resize-pixelwise t
	frame-resize-pixelwise t)

;;; history
(setopt
	history-length 250
	kill-ring-max 25)
	
(use-package savehist
	:custom
	(savehist-file "~/.config/emacs/savehist")
	(savehist-additional-variables '(
		kill-ring
		command-history
		set-variable-value-history
		custom-variable-history
		query-replace-history
		read-expression-history
		minibuffer-history
		read-char-history
		face-name-history
		bookmark-history
		file-name-history))
	(savehist-mode t))

(use-package frame
	:custom
	(window-divider-mode t)
	(window-divider-default-right-width 1)
	(window-divider-default-bottom-width 1)
	(window-divider-default-places t)
	)

(use-package emacs
	:init
	;; Add prompt indicator to `completing-read-multiple'.
	;; We display [CRM<separator>], e.g., [CRM,] if the separator is a comma.
	(defun crm-indicator (args)
		(cons (format "[CRM%s] %s"
			(replace-regexp-in-string
				"\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
				crm-separator)
			(car args))
			(cdr args)))
	(advice-add #'completing-read-multiple :filter-args #'crm-indicator)

	;; Do not allow the cursor in the minibuffer prompt
	(setq minibuffer-prompt-properties
		'(read-only t cursor-intangible t face minibuffer-prompt))
	(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

	:custom
	(display-battery-mode t)
	(display-time-mode t)
	(display-time-default-load-average nil)
	(display-time-24hr-format t)
	
	;; Support opening new minibuffers from inside existing minibuffers.
	(enable-recursive-minibuffers t)
	(debug-on-error t)

	;; Hide commands in M-x which do not work in the current mode.
	(read-extended-command-predicate 'command-completion-default-include-p))

(setopt
	recentf-max-menu-items 25
	recentf-save-file "~/.config/emacs/recentf"
	recentf-mode 1
	bookmark-default-file "~/.config/emacs/bookmarks")



;;; miscellaneous
(setopt
	save-place-mode 1)

(use-package goto-addr
	:custom
	(global-goto-address-mode t)
	(goto-address-mail-face 'link)
	(goto-address-mail-mouse-face 'link)
	(goto-address-url-face 'link)
	(goto-address-url-mouse-face 'link)
)

(setenv "PAGER" "cat")
(setenv "TERM" "dumb")
(setenv "GPG_AGENT_INFO" nil)
(defalias 'yes-or-no-p 'y-or-n-p)
(setq disabled-command-function nil)
(setopt custom-file (make-temp-file "custom"))
(setq inhibit-startup-echo-area-message "noa")  ;; #userfreedom

(use-package simple
	:delight visual-line-mode
	:custom
	(global-visual-line-mode t))

(setopt
	kill-whole-line t
	uniquify-after-kill-buffer-p t
	uniquify-buffer-name-style 'forward
	uniquify-ignore-buffers-re "^\\*"
	uniquify-separator "/")

(setopt
	save-interprogram-paste-before-kill t
	mouse-yank-at-point t
	require-final-newline t
	visible-bell t
	load-prefer-newer t
	ediff-window-setup-function 'ediff-setup-windows-plain)

(use-package server
	:disabled
	:config
	(unless (server-running-p) (server-start)))

(setopt help-at-pt-display-when-idle t)

(use-package ctrlf
	:ensure t
	:bind (
		([remap isearch-forward] . ctrlf-forward-default)
		([remap isearch-backward] . ctrlf-backward-default)
		([remap isearch-forward-regexp] . ctrlf-forward-alternate)
		([remap isearch-backward-regexp] . ctrlf-backward-alternate)
		([remap isearch-forward-symbol] . ctrlf-forward-symbol)
		([remap isearch-forward-symbol-at-point] . ctrlf-forward-symbol-at-point)
	)
	:custom
	(ctrlf-go-to-end-of-match nil "It makes more sense to go to the start of the match, because i start searching where i want to be.")
)
	
(global-set-key (kbd "M-o") 'other-window)

(global-set-key (kbd "C-x k") 'kill-this-buffer)

;; undo C-/
;; redo C-S-/
(setopt undo-no-redo t)

;; mu4e
(use-package mu4e
	:custom
	(mu4e-headers-skip-duplicates t)
	(mu4e-view-show-images t)
	(mu4e-view-show-addresses t)
	(mu4e-compose-format-flowed nil)
	(mu4e-change-filenames-when-moving t)
	(mu4e-use-fancy-chars nil)
	(mu4e-confirm-quit nil)
	(mu4e-headers-leave-behavior 'apply)
	(mu4e-headers-precise-alignment t)
	(mu4e-headers-fields '(
		;; (:human-date . 12)
		(:flags . 6)
		(:from . 32)
		(:subject)))
	(mu4e-search-threads nil)
	(mu4e-hide-index-messages t)
	(mu4e-get-mail-command "mbsync -c ~/.config/mbsyncrc fastmail")
	(mu4e-maildir "~/mail")
	(mu4e-drafts-folder "/Drafts")
	(mu4e-sent-folder   "/Sent")
	(mu4e-refile-folder "/Archive")
	(mu4e-trash-folder  "/Trash")
	(mu4e-bookmarks '(
		(:name "Inbox" :query "maildir:/Inbox" :key ?i)
		(:name "Feeds" :query "maildir:/Feeds" :key ?f)
		(:name "Paper trail" :query "\"maildir:/Paper trail\"" :key ?p)))
	:bind (
		:map mu4e-headers-mode-map
		("d" . my-move-to-trash)
		:map mu4e-view-mode-map
		("d" . my-move-to-trash)
		)
	:init
	(fset 'my-move-to-trash "mTrash")	;; function to move mails to trash
	:config
	(setq
		mu4e-headers-thread-connection-prefix '("│ " . "│ ")
		mu4e-headers-thread-last-child-prefix '("└ " . "└ ")
		mu4e-headers-thread-blank-prefix '("  " . "  ")
		mu4e-headers-thread-root-prefix '("□ " . "□ ")
		mu4e-headers-thread-child-prefix '("│ " . "│ ")
		mu4e-headers-thread-orphan-prefix '("♢ " . "♢ ")
		mu4e-headers-thread-duplicate-prefix '("≡ " . "≡ ")
		mu4e-headers-thread-first-child-prefix '("⚬ " . "⚬ ")
		mu4e-headers-thread-single-orphan-prefix '("♢ " . "♢ ")
	)	
	)

(setopt message-fill-column nil
        ;; (message-signature-file)
        (message-signature "~noa (https://noa.pub)
 • I try to reply to formal emails in three sentences or fewer; excuse my brevity.
 • I queue replies and batch send them at intervals; excuse my untimeliness."))

(use-package button
	:custom-face
	;;(button ((t (:underline t :foreground unspecified))))
	)

(use-package faces
	:custom-face
	;; (link ((t (:underline t :foreground unspecified))))
	;; (fringe ((t (:background unspecified))))
	;; (mode-line ((t (:background unspecified :box 2))))
	;; (mode-line-active ((t (:inverse-video t))))
	;; (mode-line-inactive ((t (:background unspecified :box 2 :weight unspecified))))
	;; (help-key-binding ((t (:inherit default :background unspecified :foreground unspecified :box 1))))
	)

(global-set-key (kbd "M-z") 'zap-up-to-char)

(use-package markdown-mode
	:ensure t
	:mode ("\\.md\\'" . markdown-mode)
	)
(use-package valign
	:ensure t
	:hook (markdown-mode . valign-mode)
	:hook (org-mode . valign-mode)
	:custom
	(valign-fancy-bar t)
	(valign-max-table-size 0))

(use-package eat
	:ensure t
	:hook (
		(eshell-load . eat-eshell-mode)
		(eshell-load . eat-eshell-visual-command-mode)
	)
)

	

(use-package which-key
	:ensure t
	:custom
	(which-key-popup-type 'side-window)
	(which-key-side-window-location 'bottom)
	(which-key-side-window-max-height 3)
	(which-key-persistent-popup nil)
	(which-key-show-prefix 'echo)
	(which-key-idle-delay 0)
	(which-key-mode t)
)

(use-package fixed-pitch
	:custom
	(fixed-pitch-dont-change-cursor t)
	(fixed-pitch-blacklist-hooks '(
		prog-mode-hook
		comint-mode-hook
	))
	(fixed-pitch-whitelist-hooks '(
		calendar-mode-hook
		dired-mode-hook
		magit-mode-hook
		profiler-report-mode-hook
		which-key-init-buffer-hook
		jabber-roster-mode-hook
		mu4e-headers-mode-hook
	))
)

;; My current favourite font is sn pro, which feels like comic sans for grown ups.  It's friendly but consistent and well thought out.  However, it's also a proportional font, which obviously is the right way to do things, but emacs is very old and comes from a time before the innovation of legibility.  As a result, there are some things that require a monospaced font, so i set one here.  I chose go mono for two reasons: the first is because i think it looks really nice; the second is because it has serifs and is very visually distinct from sn pro, so i can notice and shame those buffers which require a fixed width font to operate properly.
(custom-set-faces
	'(fixed-pitch ((t (:family "Go Mono" :height 110))))
	'(variable-pitch ((t (:family "SN Pro" :height 110))))
)
;; For some frustrating reason, emacs does not respect fontconfig font settings.  What this means in practice is that emacs by default draws cjk characters with the korean variant.  Luckily emacs has its own obscure and poorly documented way of doing things, so i can iterate over the relevant charsets and set the font specifically for those characters.
(dolist (
	charset '(
		han
		;; symbol
		cjk-misc))
	(set-fontset-font t charset (
		font-spec :family "Noto Sans CJK SC"
	))
)

;; Describe a key based on a string like "C-SPC"
(defun describe-key-shortcut (shortcut)
	(interactive "MShortcut: ")
	(describe-key (kbd shortcut)))

(setopt calendar-week-start-day 1)

;;; TOUCHSCREEN
;; this should be obsolete in emacs 30
;; Copyright 2024-present Naheel Azawy.  All rights reserved.
(defvar touchscreen-last-time)
(defvar touchscreen-last-pos-pixel)
(defvar touchscreen-last-dist 0)
(defvar touchscreen-begin-char)

(defun touchscreen-time ()
  "Time in seconds."
  (time-convert (current-time) 'integer))

(defun touchscreen-handle-touch-begin (input)
  "Handle touch begining at input INPUT."
  (interactive "e")
  (let* ((event     (nth 1 input))
	   (pos-pixel (nth 3 event))
	   (pos-char  (nth 6 event))
	   (win	 (nth 1 event)))
    ;; (message (format "%s" input))
    (if (not (equal (selected-window) win))
	  ;; switch window
	  (select-window win))
    ;; set globals
    (setq touchscreen-last-time (touchscreen-time))
    (setq touchscreen-last-pos-pixel pos-pixel)
    (setq touchscreen-begin-char pos-char)
    ))

(defun touchscreen-handle-touch-update (input)
  "Handle touch update at input INPUT."
  (interactive "e")
  (let* ((event	(nth 0 (nth 1 input)))
	   (pos-pixel  (nth 3 event))
	   (pos-char   (nth 6 event))
	   (diff-time  (- (touchscreen-time) touchscreen-last-time))
	   (diff-pixel (- (cdr touchscreen-last-pos-pixel) (cdr pos-pixel)))
	   (diff-char  (abs (- touchscreen-begin-char pos-char))))

    (if (= (length (nth 1 input)) 2)
	  ;; pinch zoom
	  (let* ((event2     (nth 1 (nth 1 input)))
		   (pos-pixel2 (nth 3 event2))
		   (dist	 (sqrt (+ (expt (- (car pos-pixel2) (car pos-pixel)) 2)
						(expt (- (cdr pos-pixel2) (cdr pos-pixel)) 2))))
		   (dist-diff  (- dist touchscreen-last-dist)))
	    (setq touchscreen-last-dist dist)
	    (if (> dist-diff 0)
		  (text-scale-increase 0.1)
		(if (< dist-diff 0)
		    (text-scale-decrease 0.1)))
	    )
	
	(if (> diff-time 1)
	    ;; TODO: set marker on long press
	    (goto-char pos-char))
	(if (> diff-char 1)
	    ;; scroll
	    (progn
		(move-to-window-line nil)
		(if (> diff-pixel 0)
		    (pixel-scroll-pixel-up diff-pixel)
		  (if (< diff-pixel 0)
			(pixel-scroll-pixel-down (* -1 diff-pixel))))
		(setq touchscreen-last-time (touchscreen-time))
		(setq touchscreen-last-pos-pixel pos-pixel))
	  ))))

(defun touchscreen-handle-touch-end (input)
  "Handle touch end at input INPUT."
  (interactive "e")
  (let* ((event    (nth 1 input))
	   (pos-char (nth 6 event)))
    (if (= touchscreen-begin-char pos-char)
	  ;; move cursor
	  (goto-char pos-char))))

(global-set-key [touchscreen-begin]  #'touchscreen-handle-touch-begin)
(global-set-key [touchscreen-update] #'touchscreen-handle-touch-update)
(global-set-key [touchscreen-end]    #'touchscreen-handle-touch-end)