git.fiddlerwoaroof.com
Browse code

feat(hn-browser): initial version

Edward authored on 02/02/2022 19:02:25
Showing 2 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,30 @@
1
+(defun utf-8-file-encoding (pathname ef-spec buffer length)
2
+  (declare (ignore pathname buffer length))
3
+  (system:merge-ef-specs ef-spec :utf-8))
4
+
5
+(setq system:*file-encoding-detection-algorithm*
6
+      (substitute 'utf-8-file-encoding
7
+                  'system:locale-file-encoding
8
+                  system:*file-encoding-detection-algorithm*))
9
+(set-default-character-element-type 'simple-char)
10
+
11
+(in-package :cl-user)
12
+
13
+(format t "~&CURDIR: ~a~%" (truename "."))
14
+(load-all-patches)
15
+(load "~/quicklisp/setup.lisp")
16
+(ql:quickload '(:cells :data-lens :drakma :lquery :fwoar-lisputils :alexandria :serapeum))
17
+(compile-file "/Users/edwlan/git_repos/git.fiddlerwoaroof.com/lisp-sandbox/hn-browser.lisp")
18
+(load "/Users/edwlan/git_repos/git.fiddlerwoaroof.com/lisp-sandbox/hn-browser")
19
+
20
+(deliver (intern "STARTUP" "FWOAR.LISP-SANDBOX.HN-BROWSER")
21
+         (create-macos-application-bundle
22
+          "HNReader.app"
23
+          :document-types nil
24
+          :identifier "fwoar.cj.HNReader"
25
+          :version "0.0.never")
26
+         0
27
+         :split :resources
28
+         :interface :capi
29
+         ;;:startup-bitmap-file nil
30
+         )
0 31
new file mode 100644
... ...
@@ -0,0 +1,206 @@
1
+(defpackage :fwoar.lisp-sandbox.hn-browser
2
+  (:use :cl))
3
+(in-package :fwoar.lisp-sandbox.hn-browser)
4
+
5
+(lquery:define-lquery-macro progn (nodes &rest args)
6
+  `(lquery:$
7
+     (inline ,nodes)
8
+     ,@args))
9
+
10
+(lquery:define-lquery-function hn-score (item)
11
+  (lquery:$1 (inline item)
12
+             (next)
13
+             ".score"
14
+             (text)))
15
+
16
+(lquery:define-lquery-function hn-age (item)
17
+  (lquery:$1 (inline item)
18
+             (next)
19
+             ".age"
20
+             (text)))
21
+
22
+(lquery:define-lquery-function hn-comments (item)
23
+  (lquery:$1 (inline item)
24
+             (next)
25
+             ".age"
26
+             (lquery-funcs:next)
27
+             (next)
28
+             (next)
29
+             (attr "href")))
30
+
31
+(defun get-hn-data-cached ()
32
+  (let ((a (load-time-value (list nil))))
33
+    (if (car a)
34
+        (car a)
35
+      (setf (car a)
36
+            (drakma:http-request "http://news.ycombinator.com")))))
37
+
38
+(defun scrape-hn-page (text)
39
+  (let* ((hnmain (lquery:$ (initialize text)
40
+                   "#hnmain .itemlist tr.athing"
41
+                   (combine (progn ".title a" (attr "href")
42
+                              (node))
43
+                            (progn ".title a" (text)
44
+                              (node))
45
+                            (hn-score)
46
+                            (hn-age)
47
+                            (hn-comments)))))
48
+    (map 'vector
49
+         (serapeum:op (apply 'make-hn-item _*))
50
+         hnmain)))
51
+
52
+(defclass hn-item ()
53
+  ((%url :initarg :url :reader url)
54
+   (%title :initarg :title :reader title)
55
+   (%score :initarg :score :reader score)
56
+   (%age :initarg :age :reader age)
57
+   (%comments :initarg :comments :reader comments)))
58
+
59
+(defun make-hn-item (url title score age comments)
60
+  (flet ((normalize-uri (url)
61
+           (puri:merge-uris (puri:parse-uri url)
62
+                            "https://news.ycombinator.com")))
63
+    (make-instance 'hn-item
64
+                   :url (normalize-uri url)
65
+                   :title title
66
+                   :score (when score (parse-integer score :junk-allowed t))
67
+                   :age age
68
+                   :comments (when comments (normalize-uri comments)))))
69
+
70
+(defclass hn-store ()
71
+  ((%items :initarg :items :accessor items)
72
+   (%selected-item-idx :initarg :selected :accessor selected-item-idx)
73
+   (%url-type :accessor url-type :initform :|Article|)))
74
+
75
+(fw.lu:defclass+ get-page ()
76
+  ((%page-url :initarg :page-url :reader page-url)))
77
+
78
+(fw.lu:defclass+ select-item ()
79
+  ((%item :initarg :item :reader item)))
80
+
81
+(fw.lu:defclass+ update-url-type ()
82
+  ((%new-type :initarg :new-type :reader new-type)))
83
+
84
+(defun selected-item (store)
85
+  (elt (items store)
86
+       (selected-item-idx store)))
87
+
88
+(defun get-hn-data (suffix)
89
+  (drakma:http-request (format nil "https://news.ycombinator.com/~a" suffix)))
90
+
91
+(serapeum:defalias ui-data
92
+  (data-lens:<>1
93
+   (data-lens:transform-tail (data-lens:over (data-lens:applicable-when (lambda (it)
94
+                                                                          (puri:render-uri it nil))
95
+                                                                        (complement 'null))))
96
+   (data-lens:juxt 'title 'url 'comments)))
97
+
98
+(defgeneric apply-action (store action)
99
+  (:method :around (store action)
100
+   (prog1 store
101
+     (call-next-method)))
102
+  (:method ((store hn-store) (action null))
103
+   (let ((hn-items (scrape-hn-page (get-hn-data-cached))))
104
+     (setf (items store) hn-items
105
+           (selected-item-idx store) 0)))
106
+  (:method ((store hn-store) (action get-page))
107
+   (let ((hn-items (scrape-hn-page (get-hn-data (page-url action)))))
108
+     (setf (items store) hn-items)))
109
+  (:method ((store hn-store) (action select-item))
110
+   (let ((item (item action)))
111
+     (setf (selected-item-idx store) (position item (items store)))))
112
+  (:method ((store hn-store) (action update-url-type))
113
+   (setf (url-type store) (new-type action))))
114
+
115
+(defun request-new-items (interface page)
116
+  (apply-action interface (get-page page)))
117
+
118
+(defun open-item (interface item)
119
+  (apply-action interface
120
+                (select-item item)))
121
+  
122
+(defun switch-article-comments (item interface)
123
+  (apply-action interface (update-url-type item)))
124
+  
125
+(capi:define-interface hn-reader (capi:interface hn-store)
126
+  ()
127
+  (:panes
128
+   (pages capi:list-panel
129
+          :reader hnr-pages
130
+          :items (list "news" "newest" "ask" "show" "jobs")
131
+          :initial-constraints '(:visible-max-width (:string "newestest"))
132
+          :selection-callback 'request-new-items
133
+          :callback-type :interface-item)
134
+   (item-panel capi:list-panel
135
+               :reader hnr-item-panel
136
+               :print-function 'title
137
+               :selection-callback 'open-item
138
+               :callback-type :interface-item)
139
+   (browser capi:browser-pane
140
+            :reader hnr-browser
141
+            :url "https://fwoar.co"))
142
+  (:layouts
143
+   (browser-tabs capi:tab-layout
144
+                 '(browser)
145
+                 :selection-callback 'switch-article-comments
146
+                 :items '(:|Article| :|Comments|))
147
+   (right-side capi:column-layout
148
+               '(item-panel :divider browser-tabs)
149
+               :y-ratios '(1 nil 2)
150
+               :uniform-size-p nil)
151
+   (main-layout capi:row-layout
152
+                '(pages :divider right-side)
153
+                :visible-min-width '(:character 120)
154
+                :visible-min-height '(:character 40)
155
+                :x-ratios '(1 nil 2)
156
+                :uniform-size-p nil))
157
+  (:default-initargs
158
+   :layout 'main-layout
159
+   :title "HN Reader"))
160
+
161
+(defun refresh-items (interface)
162
+  (let ((pane (hnr-item-panel interface)))
163
+    (capi:apply-in-pane-process
164
+     pane
165
+     (lambda (pane)
166
+       (let ((cleaned-items (remove-if 'null (items interface) :key 'title)))
167
+         (capi:remove-items pane
168
+                            (constantly t))
169
+         (capi:append-items pane 
170
+                            cleaned-items)))
171
+     pane)))
172
+
173
+(defmethod apply-action :after ((store hn-reader) (action null))
174
+  (refresh-items store))
175
+
176
+(defmethod apply-action :after ((store hn-reader) (action get-page))
177
+  (refresh-items store))
178
+
179
+(defmethod apply-action :after ((store hn-reader) (action update-url-type))
180
+  (apply-action store (select-item (selected-item store))))
181
+
182
+(defmethod apply-action :after ((store hn-reader) (action select-item))
183
+  (let ((pane (hnr-browser store)))
184
+    (capi:apply-in-pane-process
185
+     pane
186
+     (lambda (pane)
187
+       ;;(format *debug-stream* "> ~{~s~^ ~}~%" (list item interface))
188
+       (let ((current-item (capi:choice-selected-item (hnr-item-panel store)))
189
+             (url-type (url-type store)))
190
+         (ecase url-type
191
+           (:|Article|
192
+            (capi:browser-pane-navigate pane
193
+                                        (second (ui-data (selected-item store)))))
194
+           (:|Comments|
195
+            (with-accessors ((comments comments)) current-item
196
+              (when comments
197
+                (capi:browser-pane-navigate pane
198
+                                            (third (ui-data (selected-item store))))))))))
199
+     pane)))
200
+
201
+(defmethod initialize-instance :after ((o hn-reader) &key)
202
+  (apply-action o nil))
203
+
204
+(defun startup ()
205
+  (capi:display (make-instance 'hn-reader)))
206
+  
0 207
\ No newline at end of file