-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathq4.el
More file actions
1913 lines (1649 loc) · 74.6 KB
/
q4.el
File metadata and controls
1913 lines (1649 loc) · 74.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
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
;; [Q4 Mode by @desvox (Blake DeMarcy)]
;; [ https://github.com/desvox/q4 ]
;; last changes around March of 2017. Please read the commentary below.
;; You can get this running in your emacs by downloading/copying q4.el
;; and doing M-x eval-buffer.
;; This file is free software; you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by the
;; Free Software Foundation; either version 3, or (at your option) any
;; later version.
;; This file is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
;; more details.
;; You should have received a copy of the GNU General Public License along
;; with GNU Emacs; see the file COPYING. If not, write to the Free Software
;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
;; 02110-1301, USA.
;;;;;;;;;;;;;;;;; commentary, usage, compatibility ;;;;;;;;;;;;;;;;;;;;;;
;; for media viewing, you should have the third party packages mpv and feh
;; installed. Emacs has a fairly basic image viewer built into it, but I haven't
;; bothered to integrate it with Q4 yet, as feh offers a much better experience
;; for static images, and emacs cannot handle webms at all. They should both be
;; available from your package manager, are free software, and aren't
;; particularly bloated.
;; If the colors are ugly, they try to set themselves based on whether your
;; theme is dark or light using emacs' own face system. If this fails for you,
;; please look at the functions q4/set-light-colors and q4/set-dark-colors: they
;; will force the right ones into place.
;; If you're not interested in reading all this crap, skip down to compatibility.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; USAGE ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; The following is an introduction of how to use Q4, with default keybinds
;; placed in [brackets]. Be mindful of the casing; an upper case letter means to
;; hold shift, or maybe caps lock, if you're into that sort of crazy thing.
;; The entry point to start browsing is the interactive funtion q4/browse-board.
;; That'll prompt for a board and open its catalog. A listing of all boards is
;; available through the function q4/board-overview. browse-board is not bound
;; to any keys by default, but board-overview is available with [B] in all Q4
;; buffers. Everything below this point assumes you're already in a Q4 buffer.
;; In any context, [n/p] ([j/k] with evil) will move from one item to the next
;; and recenter the content so an item's head is always at the top of the
;; screen. [N/P] (or evil's [J/K]) will scroll the screen up or down by one line
;; without jumping between posts or recentering. Use this to scroll posts whose
;; text is overflowing the screen.
;; [o] opens things. It opens boards in the board overview, opens threads in
;; catalogs, and opens images in threads. If you want to view an image of a
;; catalog post without opening its thread, use [i]. This also works in threads,
;; in fact o just calls the same function when you're in a thread.
;; [g] (or [R] for evil users) will refresh things. In threads, this adds new
;; content to the end of the buffer and updates the reply info of all existing
;; posts. This is seamless and your point position is not lost. In catalogs, the
;; whole buffer is scrapped and point is brought back up to the top (I'll figure
;; something more useful out later)
;; [TAB] and shift+TAB [backtab] will move point from one button widget to
;; another. This is mostly unused, as dedicated keybinds are available
;; to action upon these buttons in all cases, however as you will see in the
;; next line, this can be useful when there are multiple targets.
;; In threads, you can use the square bracket keys to climb up a chain of
;; replies. ] will locate the first quote (ie. >>28340238) between point and the
;; end of the post, and jump up to it. Pressing [ will bring you back to your
;; last jump. These jumps are tracked and you're free to climb all the way up to
;; OP or meander around a bit, just mash [ to get back to where you started. If
;; there are multiple quotes in a post, you can use [TAB/backtab] to move to it
;; and then use the ] key to jump up to it. If point is already on a quote, RET
;; will work too.
;; Downward reply navigation works a bit differently (although I think I will
;; implement this method for upward navigation as well). [r] will split the
;; window and create a new buffer, who contains all of the replies to the parent
;; post. You can continue moving downward by pressing [r] on these posts as
;; well; [q] (evil users can also use [d]) will go back to the parent node. When
;; you've reached the root, [q] will close the navigation window. Aside from the
;; bracket jumping discussed above, all the normal features will work on posts
;; in the reply buffer. You also get a header line here which will give you an
;; idea of where you are. If you want out and you want it now, use [Q].
;; The curly brace keys will expand quoted posts in place instead of jumping to
;; them in the buffer. } will expand them and { will collapse them back.
;; [a] and [A] offer integration with feh and mpv: [a] will feed feh all the
;; URLs for images in the current catalog or thread. [A] will prompt for a
;; directory and pass them to wget for archival.
;; [u] will scrape up all urls in the current post and prompt you to browse one
;; with a completion widget. This will pass it off to your native browser.
;; Although Q4 cant post directly, it can easily get you there in your real web
;; browser. [U] will prompt at the minibuffer for exactly where you want to go.
;; [p]ost will jump to a particular comment in a thread, or open OP of a thread
;; if you are in a catalog. [b]uffer will jump to OP of a thread, or will open
;; the catalog itself. If the current post has an image, [i]mage will open it in
;; your browser.
;; [t] will toggle whether or not thumbnails are rendered when creating buffers.
;; This can speed things up a bit, but thumbnailing is asynchronous, so the
;; difference is not that major. This flips the variable q4/thumbnails on and
;; off. If you want them permanantly disabled, just setq it to nil after Q4 is
;; loaded.
;; [T] will toggle the rendering method used for thumbnailing. One will load
;; thumbnails only when they are near point (q4/repeating-thumbnail-dispatch)
;; and the latter will try to load all of them in the background
;; (q4/async-thumbnail-dispatch)
;; [q] (and evil users can also use [d]) is the universal 'back button'.
;; Depending on context, it will navigate backward, bury, or kill buffers. [Q]
;; (also evil [D]) will surpress any odd behaviour and just quit things
;; unconditionally.
;; [@] is a convenience bind to rename a buffer. If you rename a thread's buffer,
;; Q4 will prompt for confirmation when using [q] (but not [Q])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;; COMPATIBILITY ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Sometimes, 4chan sends back gzipped responses from their API. There is
;; no indication of when or why this happens, and it seems to only happen
;; every few dozen requests. First, Q4 will check if your build of emacs
;; has zlib support compiled in. Most do, including the windows builds
;; linked below. If it doesn't, your system will need to have the the gzip
;; utility installed, but its a very standard util thats most likely
;; installed already if you use a Unix/GNU Linux operating system. You can
;; double check By typing 'gzip' in a command line and it will spit out a
;; message whether or not its installed.
;; If your machine is not capable of either, the worst case scenario is
;; that sometimes you will have to call an action twice if it errors out.
;; the relevant error message is a json-readtable-error.
;; Q4 attempts to bind keys to Evil's normal mode if it is installed. It
;; also attempts to utilize helm or ivy for prompts when they are
;; installed. It will fall back to the built-in ido-mode, and if for some
;; arcane resason that fails, falls back to completing-read which is the
;; same component used by vanilla functions like M-x, switch-buffer, etc.
;; Q4 was built on GNU/Linux, in Spacemacs, but I also do testing on
;; vanilla (unconfigured, standard) emacs installs, and on Virtualbox'd
;; Windows 7 and 8. I have no way to test OSX support at this time.
;; As of this time, the Windows builds found at
;; https://sourceforge.net/projects/emacsbinw64/
;; are fully operational (as far as I can tell) except for external media
;; support. Thumbnails work but don't expect the i key to pop open Windows
;; Photo Viewer yet :^)
;; DO NOT USE THE OFFICAL GNU WINDOWS BUILDS. They DO NOT have the xml, html,
;; or image libraries Q4 depends on. Either compile it yourself or use the
;; link above. Emacs 24.x and below are not supported on any platform; the
;; shr library included before 25 lacks functionality that Q4 expects.
;; As of this time, emacs snapshots for version 26 have not been tested.
;; ====================== PROGRESS ======================
;; DONE:
;; Zero third party emacs dependencies, works on the standard elisp library
;; Evil (when installed) and vanilla emacs keybinds out of the box.
;; Fairly robust property-based navigation. (refactoring and optimization still ongoing)
;; Full thumbnail support, works async in stages to keep browsing snappy
;; Color highlighting for greentext, quotes, IDs, headers and seperators.
;; Detects quotes that reference to deleted posts, applies a
;; different face with no navigation callbacks.
;; Cap and /pol/ flag icon support. Flags are already showing, but I still need to
;; add display hooks for the caps.
;; Tripcode and name support.
;; External media support via feh and mpv
;; Download full thread/catalog content with wget with interactive directory prompt
;; Tracked navigation up a reply tree with a buffer local marker stack
;; Tracked navigation down a reply tree in a dedicated buffer/window
;; Inline quote expansion to read quotes without changing position
;; Generate permalinks to open threads, posts and images externally and pass
;; them to the native default browser
;; Scrape up all URLs from a post or buffer, using helm, ivy, ido, or vanilla
;; completeing-read to pop one open in an external browser. Super comfy.
;; Cleans up all of its http request buffers.
;; In-place thread refreshing, appending new posts at the end of the buffer.
;; Catalogs need something better than the current "throw it all out" method.
;; Gzip API response support (requires either compiled-in zlib support or gzip installed)
;;
;; ======================== TODO (deprecated? maybe not...) ========================
;; lainchan/8chan support
;; extract urls from parenthesis if needed when using list-urls
;; viper-mode support
;; Utilize defcustom where it makes sense.
;; add /t/ magnet support in addition the URLs
;; set up photo download dir to prompt for full, not relative path when var is set to nil
;; add optional faces for tripcodes, names, dubs/trips/quads/etc..
;; MS Windows support for external media
;; get /pol/ flags centered in the row instead of at the bottom (looks weird af)
;; /pol/ ID support
;; tree based browsing
;; =================================================================================
(require 'derived)
(require 'json)
(require 'shr)
(require 'url)
(require 'cl)
(when (< emacs-major-version 25)
(warn "Q4 will not work on emacs 24! See here for info
on how to upgrade outside of your package manager:
https://www.emacswiki.org/emacs/EmacsSnapshotAndDebian
Compiling emacs is also not so hard; check out the GNU website."))
(defvar q4/thumbnail-method #'q4/repeating-thumbnail-dispatch
"Defines the callback function for thumbnail rendering. This can
either be `q4/repeating-thumbnail-dispatch' or `q4/async-thumbnail-dispatch'.
The async dispatcher is often smoother but has shown rather unpredictable
behaviour. The repeating dispatch needs more testing but it seems more
reliable.")
(defvar q4/wrapwidth 80
"The width, in characters, of post seperators and when post texts will be
word wrapped.")
(defvar q4/keep-point-centered t
"Keep point position consistent when navigating.")
(defvar q4/show-namefags t
"Acknowledge attention whores. When non-nil, will show names next to post
IDs if the user has chosen to use one. Also see `q4/show-tripfags'")
(defvar q4/show-tripfags t
"Acknowledge attention whores. When non-nil, will show tripcodes next to
post IDs if the user has chosen to use one. Also see `q4/show-namefags'")
(defvar q4/photo-download-directory "~/Pictures/q4/"
;; TODO: Set this var to nil to prompt for new dir every time
"The top level folder where thread content can be downloaded to.")
(defvar q4/show-countries t
"Display country names or flags in supported boards (/pol/). See
`q4/country-type' to choose how this is displayed.")
(defvar q4/country-type 'flag
"When `q4/show-countries' is non-nil, this symbol determines the
rendering type. You can use the symbols 'flag, 'name, 'abbrev, 'flag/name,
and 'flag/abbrev. Pretend, in this example, that % is a flag icon :^)
'flag: %
'name: Great Britain
'abbrev: GB
'flag/name: % Great Britain
'flag/abbrev: % GB")
(defvar q4/expand-images-with-quotes t
"When non nil, `q4/expand-quotes' will also insert thumbnails if the
parent post has one.")
(defvar q4/catalog-pages 6
"number of pages to load from the catalog. Max is 10.")
(defvar q4/dead-quote-string ">>DEAD"
"String to insert for quotes that refer to deleted posts. The face
`q4/dead-quote-face' is applied as well.")
(defvar q4/thumbnails t
;; tfw performance is so good you dont have to warn people about
;; this anymore
"Render thumbnails in the catalog and threads when non nil. Disabling
this speeds up browsing a bit. Use the t key to switch this on the fly. Any
value set here is silently nil'd if you are using a terminal or your emacs
build doesn't have image support.")
(defvar q4/header-indicator "||>"
"A string to insert at the beginning of each post/thread before any other
info. Must be at least one character, and navigation will be more reliable
if it is fairly unique (though text properties are also checked).")
(defvar q4/seperator-char "-"
"A 1-length string to draw seperators with.")
(defvar q4/discard-request-buffers t
"Whether HTTP request buffers should be killed after their data is
extracted. This is t by default, but disabling it is useful for debugging.")
;;;;;;;;;;;;;;; blah blah "user servicable parts" blah blah "high quality code" ;;;;;;;;;;;;;;;
(defvar q4/base "https://a.4cdn.org/"
"Base URL for all requests.")
(defvar q4/icon-base "http://s.4cdn.org/image/"
"Base URL for cap and country icons.")
(defvar q4/url-regexp
;; TODO: Add /t/ magnet support
"https*://[^ \n\r\t]+")
(defvar q4/icon-cache (make-hash-table :test 'equal :weakness nil)
;; https://www.youtube.com/watch?v=hU7EHKFNMQg
"A hash table containing the gif image data for cap and flag icons, with
their names as keys.")
(defvar q4/all-4chan-boards '()
"A list of all available boards for 4chan. This variable is initilized
on the first call to `q4/browse-board'.")
(defvar q4/icon-path (expand-file-name "q4-icons" user-emacs-directory)
"Path where cap and flag icons are stored in. This can
be safely changed, the contents will be redownloaded.")
;; these are all buffer local. would it be better to use a class? maybe. fuck it.
(make-variable-buffer-local (defvar q4/establish-data t
"When this is non nil, `q4/render-content' and all of it's worker
functions will do side effects to buffer variables, like pushing new data
to `q4/metadata' and `q4/postnos', etc. This can be bound as nil in a `let'
block to temporatily disable side effects for the renderer."))
(make-variable-buffer-local (defvar q4/metadata '()
"A buffer local alist with cars for each post number, containing
information about the posts like replies, image data, etc. The keys are
stored as integers and not strings! `q4/get-post-property' is a wrapper to
access data from this list; it will convert input from a string to integer
if needed."))
(make-variable-buffer-local (defvar q4/threadpics '()
"Buffer-local list containing links to the full-resolution photos in a
thread in the order they were posted."))
(make-variable-buffer-local (defvar q4/reply-ring '()
"Buffer-local list which stores post IDs and point positions while browsing
through replies."))
(make-variable-buffer-local (defvar q4/mark-ring '()
"Buffer-local list which stores navigation marks for quote hopping."))
(make-variable-buffer-local (defvar q4/thumblist '()
"Buffer local containment list while rendering thumbnails from their urls."))
(make-variable-buffer-local (defvar q4/postnos '()
"A list of all the thread/reply numbers (as strings) contained in the
current buffer."))
(make-variable-buffer-local (defvar q4/content-type nil
"Buffer-local symbol indicating the type of content this buffer holds.
Used to adjust actions for things like keybinds and content refreshing."))
(make-variable-buffer-local (defvar q4/threadno nil
"Buffer local string that is either 'catalog' or the OPs post number.
Also see `q4/extlink'"))
(make-variable-buffer-local (defvar q4/extlink ""
"Buffer local string containing the URL for this thread or catalog."))
(make-variable-buffer-local (defvar q4/board ""
"Buffer local string containing the board this buffer is visting." ))
(make-variable-buffer-local (defvar q4/op-image nil
"Buffer-local variable that caches the imagedata of OPs thumbnail
when moving from the catalog into a thread."))
(make-variable-buffer-local (defvar q4/expansion-offset 0
"An integer representing the number of characters used by quote expansions.
This allows asynchronous thumbnailing to jump to a char position reliably."))
(defface q4/greentext-face
'((((type graphic) (background dark))
:background nil :foreground "#90a959")
(((type graphic) (background light))
:background nil :foreground "DarkOliveGreen")
(t :backround nil :foreground "green"))
"Face for rendering greentexts."
:group 'q4-mode)
(defface q4/gray-face
'((((type graphic) (background dark))
:background nil :foreground "#666")
(((type graphic) (background light))
:background nil :foreground "grey60")
(t :background nil :foreground nil))
"Face for rendering seperators, timestamps, and other
frilly UI elements."
:group 'q4-mode)
(defface q4/id-face
'((((type graphic) (background dark))
:background nil :foreground "#d28445")
(((type graphic) (background light))
:background nil :foreground "IndianRed4")
(t :background nil :foreground "cyan"))
"Face for rendering comment and thread ID's."
:group 'q4-mode)
(defface q4/quote-face
'((((type graphic) (background dark))
:background nil :foreground "#aa759f")
(((type graphic) (background light))
:background nil :foreground "MediumOrchid4")
(t :background nil :foreground "magenta"))
"Face for rendering quotes (ie. >>2903242)"
:group 'q4-mode)
(defface q4/dead-quote-face
'((((type graphic))
:inherit 'error :strike-through t)
(t (:inherit 'error :underline t)))
"Face for rendering quotes that refer to
deleted posts."
:group 'q4-mode)
(defface q4/country-name-face
'((t :inherit 'q4/id-face))
"Face for country name and abbreviation texts."
:group 'q4-mode)
(defun q4/set-light-colors ()
"Force the built-in faces to use the colors for light themes."
(interactive)
(set-face-attribute 'q4/greentext-face nil :foreground "DarkOliveGreen")
(set-face-attribute 'q4/id-face nil :foreground "IndianRed4")
(set-face-attribute 'q4/quote-face nil :foreground "MediumOrchid4")
(set-face-attribute 'q4/gray-face nil :foreground "grey60"))
(defun q4/set-dark-colors ()
"Force the built-in faces to use the colors for dark themes."
(interactive)
(set-face-attribute 'q4/greentext-face nil :foreground "#90a959")
(set-face-attribute 'q4/id-face nil :foreground "#d28445")
(set-face-attribute 'q4/quote-face nil :foreground "#aa759f")
(set-face-attribute 'q4/gray-face nil :foreground "#666"))
(defun q4/recenter ()
;; TODO: Add preference var to change from top of screen (default) to center of screen.
(when q4/keep-point-centered
(recenter 1)))
(defun q4/next-pos (string &optional regex prop backward group bound)
"Takes a STRING and returns the char position of the beginning of its
next occurence from point in `current-buffer'. Returns nil if not found.
A simpler way to call this is to use `q4/next-prop'.
When REGEX is non-nil, STRING is interpreted as a regular expression.
PROP, when non-nil, will only return matches if they have the corresponding
value for a property. This can either be a symbol or a cons cell. If it's
a symbol, the property key used is :q4type. As a cons, The key and expected
value are given, eg '(:q4type . end)
BACKWARD, when non-nil, does what it says on the tin.
When GROUP is non-nil and an integer, returns start pos of that match
group. When PROP is in effect, it checks property at this position instead
of 0.
BOUND can be a buffer position (integer) that the search will not exceed."
(save-excursion
(let ((search (if backward (if regex 're-search-backward 'search-backward)
(if regex 're-search-forward 'search-forward)))
(group (or group 0))
(propkey (if (consp prop) (car prop) :q4type))
(propval (if (consp prop) (cdr prop) prop))
found)
;; for the unaware: searches through this function,
;; when passed t as seen here, will return nil when
;; it reaches end of buffer. This loop will not get
;; stuck because of this.
(while (and (not found) (funcall search string bound t))
(if prop (setq found (eql propval
(get-char-property
(match-beginning group)
propkey)))
(setq found t)))
(when found (match-beginning group)))))
;; and lets also add a few shorthand functions for the
;; ones searched the most...
(defun q4/next-prop (prop &optional backward bound)
"A simpler way to call `q4/next-pos' that only looks for properties and
doesn't match an input string. This is morally equivalent to:
(q4/next-pos \".\" t PROP BACKWARD nil BOUND)"
(q4/next-pos "." t prop backward nil bound))
;; A+ symmetry
(defun q4/head-pos (&optional backward)
"Return char position of the next header block of a post from point.
BACKWARD, when non nil, goes...uh, backward."
(q4/next-prop 'head backward))
(defun q4/sep-pos (&optional backward)
"Return char position of the next seperator block between
posts. BACKWARD, when non nil, goes...uh, to Vegas."
(q4/next-prop 'end backward))
(defun q4/assert-post-start ()
"Makes sure the point is at the head of a post before doing any side
effects."
(unless (eql 'head (get-char-property (point) :q4type))
;; I haven't really found a case where this would be nil,
;; and am also not currently sure how that should be handled.
;; Eh, fuck it.
(let ((check (q4/head-pos t))) (when check (goto-char check)))))
(defun q4/current-post (&optional int buffer)
"Returns the post number point is currently focused on."
(with-current-buffer (or buffer (current-buffer))
(let ((no (save-excursion
(q4/assert-post-start)
(get-char-property (point) :no))))
(if int (string-to-number no) no))))
(defun q4/get-post-property (prop &optional post buffer)
"Consults `q4/metadata' for PROP of POST in BUFFER. POST, if omitted,
uses `q4/current-post'. POST can be provided either as an integer or a
string. BUFFER defaults to `current-buffer', remember that `q4/metadata'
is buffer-local.
Returns either the cdr of PROP (which can be a nil value) or nil if it
isn't in the list."
(when (stringp (setq post (or post (q4/current-post t))))
(setq post (string-to-number post)))
(with-current-buffer (or buffer (current-buffer))
(alist-get prop (assq post q4/metadata))))
(defun q4/prop-at-point (prop)
"Gets the property PROP from the character at point. See
`get-char-property'."
(get-char-property (point) prop))
(defun q4/inboundp (marker &optional endbound)
"Returns t when a given char position is within the boundaries of a
single post or catalog entry (or ENDBOUND)."
(> (or endbound (q4/sep-pos)) marker))
(defun q4/boip (&optional marker)
"Returns t when point is at the beginning of indentation OR at the
beginning of the line at column 0. This is equivalent to `bolp' except it
also checks `back-to-indentation'"
(let ((point (or marker (point))))
(save-excursion
(back-to-indentation)
(or (= point (point))
(= point (point-at-bol))))))
(defmacro q4/append (newelt list)
"Adds NEWELT to the end of LIST in place. LIST may be nil, this
will add its first element if needed."
;; blah blah nconc does shit in place blah blah doesnt work when the list is nil
`(setq ,list (nconc ,list (cons ,newelt nil))))
(defun q4/fuck-whitespace (string &optional newlines-btfo)
"Trim leading/trailing whitespace, and optionally remove all inner
newlines."
(while (and newlines-btfo (string-match "[\n\r]+" string))
(setq string (replace-match "" t t string)))
(string-trim string))
(defun q4/threadpics-string ()
"Returns a string with urls of the current buffer's photos, in the order
they were posted. This also works in the catalogs."
;; The laziest possible way of turning a list to
;; a usable string is to pass it to format, and chop
;; the parens off of it's representation :^)
(when q4/threadpics
(substring (format "%s" q4/threadpics) 1 -1)))
(defun q4/point-to-post (dir &optional nocenter)
"Move point to the head of next post in DIR. DIR can be one of the symbols
'next and 'prev."
(let ((check
(case dir
('prev (q4/head-pos t))
('next
(save-excursion ;; or else point will stick
(while (eq 'head (q4/prop-at-point :q4type))
(goto-char (next-property-change (point))))
(q4/head-pos))))))
(when check
(goto-char check)
(back-to-indentation)
(unless nocenter
(q4/recenter)))))
(defun q4/point-to-next-post ()
"Feeds starving children in Africa, and does a better job at it then Vim."
(interactive)
(q4/point-to-post 'next)
;; Whenever using quote-hop-backward, its easy to lose track of what you're
;; doing and possibly leave the ring in an unclean state. This clears the
;; list if manual navigation exceeds the position of first jump
(let ((lastmark (car (last q4/mark-ring))))
(when (and (integerp lastmark) (> (point) lastmark))
(setq q4/mark-ring nil))))
(defun q4/point-to-previous-post ()
"Elects Donald Trump for president of the United States."
(interactive) (q4/point-to-post 'prev))
(defun q4/point-to-first-post ()
"Moves point to the first post head, below the first seperator."
(interactive)
(goto-char (+ 1 (length (q4/seperator)) (point-min))))
(defun q4/seek-next-button (&optional goto)
"Returns buffer position if the next button from POINT. if GOTO is
non-nil, moves point to the button."
(interactive)
(let ((pos (overlay-start (next-button (point)))))
(if goto (goto-char pos) pos)))
(defun q4/seek-post (number &optional mark forward nocenter)
"Takes a post NUMBER, which is actually a string :^), searches backward
for it unless FORWARD is non-nil, and pushes current cursor position to
`q4/mark-ring' when MARK is non-nil
When NOCENTER is non nil, suppresses calling on `q4/recenter'."
(let ((search (q4/next-pos (concat q4/header-indicator number)
nil 'head (not forward))))
(if search (progn (when mark (push (point) q4/mark-ring))
(goto-char search)
(unless nocenter
(q4/recenter)))
(message "Post %s not found" number))))
(defun q4/quote-hop-backward (&optional seek)
"Stores point position in a buffer-local mark ring, and jumps to the post
number being referenced.
SEEK, if provided as a string, will search for that post number
directly. When nil, it will locate the next quote within the current post
and jump to it, if it exists."
(interactive)
(if (or seek ;; if the caller didn't provide a num, find next quote
(let ((next (q4/next-prop 'quoted nil (q4/sep-pos))))
(when next (setq seek (get-char-property next :no)))))
(q4/seek-post seek t)
(message "No quotes between point and end of post.")))
(defun q4/complete-collection (prompt collection)
"Prompts the user with the string PROMPT to select an item from
COLLECTION. Will check for the following packages to make this as comfy as
possible:
helm-mode
ivy-mode
ido-mode
vanilla emacs `completing-read'"
(let ((choice
(cond
((boundp 'helm-mode)
(require 'helm)
(helm-comp-read prompt collection :must-match t))
((boundp 'ivy-mode)
(require 'ivy)
(ivy-read prompt collection :require-match t))
((boundp 'ido-mode)
(require 'ido)
(ido-completing-read prompt collection nil t))
(t (completing-read
"(Use TAB to complete)> "
collection nil t)))))
choice))
(defun q4/list-urls (&optional whole-buffer)
"Collects all urls in the current post and lets you pick one to pass to
to `browse-url'. Searches for the following packages (in the following
order) to provide only The Comfiest Selection Experience™
helm-mode
ivy-mode
ido-mode
vanilla emacs `completing-read'
If WHOLE-BUFFER is non nil, it will also put AMD out of
busi....errr...collect all urls in the buffer."
;; TODO: Isolate URLs from parenthesis if needed
(interactive)
(save-excursion
(if whole-buffer (goto-char (point-min)) (q4/assert-post-start))
(let ((bound (if whole-buffer (point-max) (q4/sep-pos))) collection)
(save-excursion
;; [[ D O U B L E D E C K E R E X C U R S I O N P R O T E C T I O N S Q U A D ]]
(while (re-search-forward q4/url-regexp bound t)
(q4/append (match-string 0) collection)))
(if collection
(let ((choice (q4/complete-collection
"(Browse URL)> "
collection)))
(if choice (browse-url choice) (message "Nevermind then!")))
(message "No URLs in this post.")))))
(defun q4/view-content-externally ()
"Prompts the user to browse either the post or buffer in the default
external browser. In this context, post is either a thread in a catalog, or
a reply in a thread. A buffer is either a catalog or a thread number. If
the current post has an image, includes an option for it as well."
(interactive)
(let* ((imglink (q4/get-post-property 'image))
(postlink (q4/get-post-property 'link))
(prompt (format "Open [b]uffer, [p]ost, %sor [c]ancel?\n(C-g/q/b/p/c%s)>"
(if imglink "[i]mage, " "")
(if imglink "/i" "")))
;; additionally, allow q, ESC and C-c to bail. read-char handles C-g
;; for free.
(gtfo `(?q ?Q ?c ?C ?\C-c ?\C-\[))
(buffer '(?b ?B))
(post '(?p ?P))
(image '(?i ?I))
(response (progn ;; workaround for it not always displaying...
(message prompt)
(read-char prompt))))
(while (not (member response (concatenate
'list buffer post
gtfo (if imglink image))))
(setq response (read-char prompt)))
(cond
((member response buffer)
(browse-url q4/extlink))
((member response post)
(browse-url postlink))
((member response image)
(browse-url imglink))
((member response gtfo)
(message "Nevermind then!")))))
(defun q4/pop-mark ()
"Flies backward to the last post in the mark ring at about mach 6, and
securely disposes of the previous position."
(interactive)
(if q4/mark-ring
(progn (goto-char (pop q4/mark-ring)) (q4/recenter))
(message "Stack is empty.")))
(defun q4/ext-program-p (program &optional whine)
"Returns whether or not PROGRAM is installed. When WHINE is non-nil,
and a string, will `message' WHINE to the user.
The only programs q4 takes advantage of at the moment is feh and mpv, for
viewing images and vids/gifs respectively."
;; TODO: Figure out something for windows users.
(let ((check (eql 0 (shell-command (format "which %s" program)))))
(when (and (not check) whine)
;; I say "congigure an alt.+" here because I'll add hooks
;; for custom functions Soon™. Also this message should
;; be less shit...
(message whine))
check))
(defun q4/pass-to-feh ()
"Passes the list of photos in this thread or catalog buffer to the
external program, feh. Doesn't download them though, if you want that, see
`q4/wget-threadpics'
In feh, SPC/DEL and n/p can be used to navigate forward and backward in the
list. Press d to show the number of photos in the list."
(interactive)
(let ((urls (q4/threadpics-string)))
(if urls
(start-process-shell-command
"feh" nil (format "feh -FZ %s" urls))
(message "Photo stack for this thread is empty."))))
(defun q4/open-post-image ()
"Opens the current post's image in feh."
(interactive)
(save-excursion
(q4/assert-post-start)
(let ((image (q4/next-prop 'image nil (q4/sep-pos))))
(if image (push-button image)
(message "No image in this post.")))))
(defun q4/open-item ()
"When in the catalog, this will open the current post in a new buffer.
When in a thread, this will open an image with `q4/open-post-image' if an
image is available. In a board overview, open the current board."
(interactive)
(save-excursion
(q4/assert-post-start)
(case q4/content-type
('catalog
(push-button (q4/next-prop 'thread nil (q4/sep-pos))))
('thread (q4/open-post-image))
('boardview
(let ((board
(save-excursion
(q4/assert-post-start)
(q4/prop-at-point 'board))))
(bury-buffer)
(q4/query "catalog.json" 'q4/catalog board))))))
(defun q4/wget-threadpics (&optional name)
"When called without args/interactively, will prompt for a folder name,
or use the string NAME if you supply it. If you supply an empty string by
just hitting return at the prompt, or passing in an empty string as the
argument, will use the current board and thread number.
The folder is created as a subdirectory of `q4/photo-download-directory'
and wget will populate it with the images of the current buffer (catalog or
thread)."
(interactive)
(let ((pics (q4/threadpics-string)))
(if pics
(let* ((input
(or name (read-string
(format "(Folder Name)> %s"
q4/photo-download-directory))))
(path
(expand-file-name
(if (string= input "")
(format "%s.%s" q4/board q4/threadno)
input)
q4/photo-download-directory)))
(start-process-shell-command
"q4-wget" nil (format "DEST=%s; mkdir $DEST; cd $DEST; wget %s"
path pics)))
(message "No photos in this buffer."))))
(defun q4/toggle-thumbnails ()
"Uh-huh.
For real crowd-pleasing precision, you can also just setq `q4/thumbnails'
yourself :^)"
(interactive)
(message
"Thumbs now %s."
(setq q4/thumbnails (not q4/thumbnails))))
(defun q4/toggle-thumbnailing-method ()
"Switches between the async and repeating dispatcher. Only affects
newly loaded buffer after switching."
(interactive)
(message "Switched to %s."
(setq q4/thumbnail-method
(if (eq q4/thumbnail-method
#'q4/repeating-thumbnail-dispatch)
#'q4/async-thumbnail-dispatch
#'q4/repeating-thumbnail-dispatch))))
(when (bound-and-true-p evil-mode)
(evil-define-key 'normal q4-mode-map
"j" 'q4/point-to-next-post
"k" 'q4/point-to-previous-post
"H" 'evil-backward-char
"J" 'scroll-up-line
"K" 'scroll-down-line
"L" 'evil-forward-char
"u" 'q4/list-urls
"U" 'q4/view-content-externally
"q" 'q4/go-back
"Q" (lambda () (interactive) (quit-window t))
"d" 'q4/go-back
"D" (lambda () (interactive) (quit-window t))
"]" 'q4/quote-hop-backward
"[" 'q4/pop-mark
"a" 'q4/pass-to-feh
"A" 'q4/wget-threadpics
"t" 'q4/toggle-thumbnails
"T" 'q4/toggle-thumbnailing-method
"i" 'q4/open-post-image
"o" 'q4/open-item
"@" 'rename-buffer
"r" 'q4/show-replies
"R" 'q4/refresh-page
"B" 'q4/board-overview
"gg" 'q4/point-to-first-post
"}" 'q4/expand-quotes
"{" 'q4/unexpand-quotes))
(define-derived-mode q4-mode fundamental-mode "Q4"
"Mode for browsing 4chan."
:group 'q4-mode
(local-set-key (kbd "SPC") 'q4/point-to-next-post)
(local-set-key (kbd "RET") 'q4/point-to-next-post)
(local-set-key (kbd "DEL") 'q4/point-to-previous-post)
(local-set-key (kbd "n") 'q4/point-to-next-post)
(local-set-key (kbd "p") 'q4/point-to-previous-post)
(local-set-key (kbd "N") 'scroll-up-line)
(local-set-key (kbd "P") 'scroll-down-line)
(local-set-key (kbd "r") 'q4/show-replies)
(local-set-key (kbd "l") 'q4/recenter)
(local-set-key (kbd "q") 'q4/go-back)
(local-set-key (kbd "Q") (lambda () (interactive) (quit-window t)))
(local-set-key (kbd "]") 'q4/quote-hop-backward)
(local-set-key (kbd "[") 'q4/pop-mark)
(local-set-key (kbd "{") 'q4/unexpand-quotes)
(local-set-key (kbd "}") 'q4/expand-quotes)
(local-set-key (kbd "t") 'q4/toggle-thumbnails)
(local-set-key (kbd "T") 'q4/toggle-thumbnailing-method)
(local-set-key (kbd "a") 'q4/pass-to-feh)
(local-set-key (kbd "A") 'q4/wget-threadpics)
(local-set-key (kbd "i") 'q4/open-post-image)
(local-set-key (kbd "o") 'q4/open-item)
(local-set-key (kbd "u") 'q4/list-urls)
(local-set-key (kbd "U") 'q4/view-content-externally)
(local-set-key (kbd "g") 'q4/refresh-page)
(local-set-key (kbd "B") 'q4/board-overview)
(local-set-key (kbd "M-<") 'q4/point-to-first-post)
(local-set-key (kbd "<f5>") 'q4/refresh-page)
(local-set-key (kbd "@") 'rename-buffer)
(local-set-key (kbd "<tab>") 'forward-button)
(local-set-key (kbd "<backtab>") 'backward-button))
(defun q4/query (dest callback board &optional buffer &rest cbargs)
"Call to the mother ship and apply CALLBACK. DEST is a string
representing the resource you're craving. BOARD is also a string,
representing the sorry state of your....errr, the board you want to access.
BUFFER, which is optional or can be nil when passing addional callbacks,
is a the buffer passed to the callback. If you don't give one yourself,
this function will create a new buffer and name it according to DEST.
CBARGS are all passed to the callback in the order provided.
A call to this looks like:
(q4/query (format \"thread/%s.json\" no) 'q4/thread board nil no)
The callback function recieves the following arguments in this order;
json - the rendered json response
buffer
board
CBARGS"
(let ((url-request-extra-headers
'(("Accept-Encoding" . "identity")
("Connection" . "close")))
(endpoint (concat q4/base board "/" dest))
(url-request-method "GET")
(buffer (or buffer
(generate-new-buffer
(format "/%s/%s" board
(substring dest 0 -5))))))
(url-retrieve
endpoint
`(lambda (status)
(if (setq status (plist-get status :error))
(case (caddr status)
(404 (message "Thread has 404'd")
(kill-buffer (current-buffer))
(unless (bound-and-true-p q4/threadno)
(kill-buffer ,buffer))))
(apply
',callback
(q4/get-response-data nil t)
,buffer ,board ',cbargs))))))
(defmacro q4/@ (key)
"A dumb macro to fetch KEY from the variable called alist, which is
assumed to already be bound outside of this macro. It keeps code (slightly)