iens.scm (57120B)
1 ; Copyright (c) 2023-2026, Natacha Porté 2 ; 3 ; Permission to use, copy, modify, and distribute this software for any 4 ; purpose with or without fee is hereby granted, provided that the above 5 ; copyright notice and this permission notice appear in all copies. 6 ; 7 ; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 ; WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 ; MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 ; ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 ; WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 ; ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 ; OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 15 (import (chicken condition) 16 (chicken file) 17 (chicken file posix) 18 (chicken io) 19 (chicken process) 20 (chicken process signal) 21 (chicken process-context) 22 (chicken sort) 23 (chicken string) 24 (chicken time) 25 (chicken time posix) 26 breadline 27 breadline-scheme-completion 28 http-client 29 lowdown 30 ncurses 31 rss 32 sql-de-lite 33 srfi-1 34 sxml-serializer) 35 36 (define (starts-with? maybe-prefix s) 37 (and (<= (string-length maybe-prefix) (string-length s)) 38 (substring=? s maybe-prefix 0 0 (string-length maybe-prefix)))) 39 40 (define (ends-with? maybe-suffix s) 41 (let ((ls (string-length s)) 42 (lms (string-length maybe-suffix))) 43 (and (>= ls lms) 44 (substring=? s maybe-suffix (- ls lms))))) 45 46 (define (time->rfc-3339 time) 47 (let ((time-str (time->string time "%FT%T%z"))) 48 (assert (= 24 (string-length time-str))) 49 (if (equal? "0000" (substring time-str 20)) 50 (string-append (substring time-str 0 19) "Z") 51 (string-append (substring time-str 0 22) 52 ":" 53 (substring time-str 22))))) 54 55 (define (rfc-3339-local seconds) 56 (time->rfc-3339 (seconds->local-time seconds))) 57 (define (rfc-3339-utc seconds) 58 (time->rfc-3339 (seconds->utc-time seconds))) 59 (define rfc-3339 rfc-3339-local) 60 61 (define (terminate-line line) 62 (let ((l (string-length line))) 63 (if (or (zero? l) 64 (eqv? (string-ref line (sub1 l)) #\newline)) 65 line 66 (string-append line "\n")))) 67 68 (define cmd-list '()) 69 70 (define-syntax defcmd 71 (syntax-rules () 72 ((defcmd (name . args) str first . rest) 73 (begin 74 (set! cmd-list (cons (list (symbol->string 'name) str first) cmd-list)) 75 (define (name . args) . rest))))) 76 77 (define vt100-alert "\033[31m") 78 (define vt100-entry-header "\033[34m") 79 (define vt100-reset "\033[0m") 80 81 ;;;;;;;;;;;;;;;;;;;;;;;;;;;; 82 ;; Command-Line Processing 83 84 (define db-filename #f) 85 (define arg-replay #f) 86 87 (let ((arg-list (command-line-arguments))) 88 (when (>= (length arg-list) 2) (set! arg-replay (cadr arg-list))) 89 (when (>= (length arg-list) 1) (set! db-filename (car arg-list)))) 90 91 ;;;;;;;;;;;;; 92 ;; Tracing 93 94 (define trace-port #f) 95 (define display-trace #t) 96 97 (define (trace obj) 98 (when display-trace 99 (write obj) 100 (newline)) 101 (when trace-port 102 (write obj trace-port) 103 (newline trace-port))) 104 105 ;;;;;;;;;;;;;;;;;;;;;;; 106 ;; Persistent Storage 107 108 (define db-name 109 (if db-filename db-filename "iens.sqlite")) 110 111 (define db 112 (open-database db-name)) 113 (write-line (conc "Using database " db-name " with SQLite " library-version)) 114 (exec (sql db "PRAGMA foreign_keys = ON;")) 115 116 (include "common.scm") 117 118 (assert (= 4 (db-version))) 119 120 ;;;;;;;;;;;;;;;;;; 121 ;; Configuration 122 123 (define config-author-name #f) 124 (define config-author-email #f) 125 (define config-author-uri #f) 126 (define config-autogenerate #f) 127 (define config-editor #f) 128 (define config-entry-id-prefix "") 129 (define config-list-tagged-count 0) 130 (define config-verbose #f) 131 132 (define default-editor 133 (let ((term (get-environment-variable "TERM")) 134 (visual (get-environment-variable "VISUAL")) 135 (editor (get-environment-variable "EDITOR")) 136 (fallback "vi")) 137 (cond 138 ((and visual term (not (equal? "dumb" term))) visual) 139 (editor editor) 140 (else fallback)))) 141 142 (define (get-config key) 143 (query fetch-value (sql db "SELECT val FROM config WHERE key = ?;") key)) 144 145 (define (get-config/default key default-value) 146 (let ((result (get-config key))) 147 (if result 148 result 149 default-value))) 150 151 (define (string->filename data) 152 (cond ((not data) #f) 153 ((starts-with? "~/" data) 154 (string-append (get-environment-variable "HOME") 155 (substring data 1))) 156 (else data))) 157 158 (define (read-config!) 159 (set! display-trace (not (zero? (get-config/default "display-trace" 0)))) 160 (set! config-verbose (not (zero? (get-config/default "verbose" 0)))) 161 (set! rfc-3339 (if (zero? (get-config/default "local-time" 1)) 162 rfc-3339-utc rfc-3339-local)) 163 (set! config-author-name (get-config "author-name")) 164 (set! config-author-email (get-config "author-email")) 165 (set! config-author-uri (get-config "author-uri")) 166 (set! config-autogenerate (not (zero? (get-config/default "autogenerate" 0)))) 167 (set! config-editor (get-config/default "editor" default-editor)) 168 (set! config-entry-id-prefix (get-config/default "entry-id-prefix" "")) 169 (set! config-list-tagged-count (get-config/default "list-tagged-count" 0)) 170 (let ((trace-filename (get-config "trace"))) 171 (when trace-port (close-output-port trace-port)) 172 (set! trace-port 173 (if trace-filename 174 (open-output-file (string->filename trace-filename) #:text #:append) 175 #f))) 176 (history-file (string->filename (get-config "histfile")))) 177 178 (read-config!) 179 180 (defcmd (print-config . args) 181 "[key ...]" "Print configuration" 182 (if (null? args) 183 (query 184 (for-each-row* 185 (lambda (key val) (write-line (conc key ": " val)))) 186 (sql db "SELECT key,val FROM config ORDER BY key;")) 187 (let loop ((todo args)) 188 (unless (null? todo) 189 (write-line (conc (car todo) ": " (get-config (car todo)))) 190 (loop (cdr todo)))))) 191 192 (defcmd (set-config key val) 193 "key value" "Set configuration variable" 194 (trace `(set-config ,key ,val)) 195 (exec (sql db "INSERT OR REPLACE INTO config VALUES (?,?);") key val) 196 (read-config!)) 197 198 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 199 ;; Configurable Query Selectors 200 201 (defcmd (add-selector text) 202 "\"WHERE …\"" "Creates a pre-defined query selector" 203 (trace `(add-select ,text)) 204 (exec (sql db "INSERT INTO selector(text) VALUES (?);") text) 205 (write-line (conc " -> " (last-insert-rowid db)))) 206 207 (define (call-with-selector arg proc) 208 (cond ((string? arg) (proc arg #f)) 209 ((number? arg) (let ((selector (get-selector arg))) 210 (if selector 211 (proc selector arg) 212 (write-line 213 (conc "No selector #" arg " found"))))) 214 (else (write-line (conc "Invalid selection argument " arg))))) 215 216 (define (get-selector id) 217 (query fetch-value (sql db "SELECT text FROM selector WHERE id=?;") id)) 218 219 (defcmd (list-selectors) 220 "" "List pre-defined query selectors" 221 (query 222 (for-each-row 223 (lambda (row) 224 (write-line (conc "#" (car row) ": \"" (cadr row) "\"")))) 225 (sql db "SELECT id,text FROM selector;"))) 226 227 (defcmd (set-selector id text) 228 "id \"WHERE …\"" "Sets a pre-defined query selector" 229 (trace `(set-selector ,id ,text)) 230 (exec (sql db "INSERT OR REPLACE INTO selector(id,text) VALUES (?,?);") 231 id text)) 232 233 ;;;;;;;;;;;;;;;;;;;;; 234 ;; Database Updates 235 236 ;; Feed Management 237 238 (define (set-feed-active id n) 239 (exec (sql db "UPDATE feed SET active=? WHERE id=?;") n id)) 240 241 (defcmd (activate-feed feed-id) 242 "feed-id" "Activate the given feed" 243 (trace `(activate-feed ,feed-id)) 244 (set-feed-active feed-id 1)) 245 246 (defcmd (add-feed filename url selector title) 247 "filename url selector title" "Add a new feed" 248 (trace `(add-feed ,filename ,url ,selector ,title)) 249 (exec (sql db 250 "INSERT INTO feed(filename,url,selector,title) VALUES (?,?,?,?);") 251 filename url selector title) 252 (write-line (conc "Added feed " (last-insert-rowid db)))) 253 254 (defcmd (disable-feed feed-id) 255 "feed-id" "Disable the given feed" 256 (trace `(disable-feed ,feed-id)) 257 (set-feed-active feed-id 0)) 258 259 (defcmd (list-feeds) 260 "" "Display all feeds" 261 (query 262 (map-rows* 263 (lambda (id filename url selector title active-int mtime) 264 (write-line (conc (if (zero? active-int) 265 (conc "(" id ")") 266 (conc "#" id)) 267 " " 268 filename 269 " - " 270 title)) 271 (write-line (conc " " url)) 272 (write-line (conc " " selector)) 273 (unless (null? mtime) 274 (write-line (conc " Updated " (rfc-3339 mtime)))))) 275 (sql db "SELECT id,filename,url,selector,title,active,mtime FROM feed;"))) 276 277 (defcmd (remove-feed feed-id) 278 "feed-id" "Remove the given feed" 279 (trace `(remove-feed ,feed-id)) 280 (exec (sql db "DELETE FROM feed WHERE id=?;") feed-id)) 281 282 (define (touch-feed mtime feed-id) 283 (trace `(touch-feed ,mtime ,feed-id)) 284 (exec (sql db "UPDATE feed SET mtime=? WHERE id=?;") mtime feed-id)) 285 286 ;; Feed Caching 287 288 (define (build-signature selector) 289 (query fetch-rows 290 (sql db (string-append "SELECT id,mtime FROM entry " selector ";")))) 291 292 (define (car< a b) (< (car a) (car b))) 293 294 (define (diff-signature old-sig new-sig) 295 (let loop ((old (sort old-sig car<)) 296 (new (sort new-sig car<)) 297 (result '())) 298 (cond ((and (null? old) (null? new)) 299 result) 300 ((null? old) 301 (loop old 302 (cdr new) 303 (cons `(add ,@(car new)) result))) 304 ((null? new) 305 (loop (cdr old) 306 new 307 (cons `(del ,@(car old)) result))) 308 ((equal? (car new) (car old)) 309 (loop (cdr old) 310 (cdr new) 311 result)) 312 ((= (caar new) (caar old)) 313 (loop (cdr old) 314 (cdr new) 315 (cons `(chg ,@(car old) ,(cadar new)) result))) 316 ((< (caar new) (caar old)) 317 (loop old 318 (cdr new) 319 (cons `(add ,@(car new)) result))) 320 ((> (caar new) (caar old)) 321 (loop (cdr old) 322 new 323 (cons `(del ,@(car old)) result))) 324 (else (assert #f "Should be unreachable"))))) 325 326 (define (write-diff sig-diff) 327 (for-each 328 (lambda (hunk) 329 (cond ((eqv? (car hunk) 'add) 330 (write-line (conc " added item #" (cadr hunk) 331 " at " (rfc-3339 (caddr hunk))))) 332 ((eqv? (car hunk) 'del) 333 (write-line (conc " removed item #" (cadr hunk) 334 " at " (rfc-3339 (caddr hunk))))) 335 ((eqv? (car hunk) 'chg) 336 (write-line (conc " updated item #" (cadr hunk) 337 ": " (rfc-3339 (caddr hunk)) 338 " → " (rfc-3339 (cadddr hunk))))) 339 (else (assert #f "Should be unreachable")))) 340 sig-diff)) 341 342 (define feed-cache 343 (query (map-rows* (lambda (id selector) 344 (cons id (build-signature selector)))) 345 (sql db "SELECT id,selector FROM feed WHERE active=1;"))) 346 (define dirty-feeds '()) 347 348 (define (check-feed* id) 349 (let ((new (query fetch-value 350 (sql db "SELECT selector FROM feed WHERE id=?;") 351 id)) 352 (old (alist-ref id feed-cache = '()))) 353 (cond ((and (not new) (null? old)) 354 (write-line (conc "Feed #" id " does not exist"))) 355 ((not new) 356 (write-line (conc "Feed #" id " does not exist anymore"))) 357 ((null? old) 358 (write-line (conc "Feed #" id " is not cached"))) 359 (else 360 (let ((sig-diff (diff-signature old (build-signature new)))) 361 (if (null? sig-diff) 362 (write-line (conc "Feed #" id " has not changed")) 363 (write-line (conc "Feed #" id " was modified:"))) 364 (write-diff sig-diff)))))) 365 366 (defcmd (check-feed . args) 367 "[feed-id ...]" "Check the cache for the given feeds, or all active feeds" 368 (for-each check-feed* 369 (if (null? args) 370 (query fetch-column (sql db "SELECT id FROM feed WHERE active=1;")) 371 args))) 372 373 (define (update-feed-cache* mtime id) 374 (let ((data (query fetch-row 375 (sql db "SELECT mtime,selector,filename,title,url 376 FROM feed WHERE id=?;") 377 id)) 378 (old-sig (alist-ref id feed-cache = '()))) 379 (if (null? data) 380 (write-line (conc "Feed #" id " does not exist")) 381 (let ((new-sig (build-signature (cadr data)))) 382 (unless (equal? old-sig new-sig) 383 (when (or (null? (car data)) 384 (> mtime (car data))) 385 (touch-feed mtime id) 386 (set! (car data) mtime)) 387 (when config-verbose 388 (write-line (if config-autogenerate 389 (conc "Autogenerating feed " id) 390 (conc "Marking feed " id " as dirty:"))) 391 (write-diff (diff-signature old-sig new-sig))) 392 (if config-autogenerate 393 (with-output-to-file (caddr data) ;filename 394 (cut write-feed 395 (car data) ;mtime 396 (list-ref data 3) ;title 397 (list-ref data 4) ;url 398 (query fetch-rows 399 (sql db (string-append "SELECT id,url,type,description, 400 notes,ptime,ctime,mtime 401 FROM entry " (cadr data) ";"))))) 402 (unless (any (cut = id <>) dirty-feeds) 403 (set! dirty-feeds (cons id dirty-feeds)))) 404 (set! feed-cache 405 (alist-update! id new-sig feed-cache =))))))) 406 407 (define (update-feed-cache mtime . id-list) 408 (for-each 409 (cut update-feed-cache* mtime <>) 410 (if (null? id-list) 411 (query fetch-column (sql db "SELECT id FROM feed WHERE active=1;")) 412 id-list))) 413 414 ;; Tag Management 415 416 (define (set-tag-auto name auto) 417 (exec (sql db "UPDATE tag SET auto=? WHERE name=?;") auto name)) 418 419 (defcmd (add-auto-tag name . rest) 420 "tag-name [tag-name ...]" "Set tags as automatic" 421 (trace `(add-auto-tag ,name)) 422 (set-tag-auto name 1) 423 (unless (null? rest) 424 (apply add-auto-tag rest))) 425 426 (defcmd (add-tag name . rest) 427 "tag-name [tag-name ...]" "Create a new tag" 428 (trace `(add-tag ,name)) 429 (exec (sql db "INSERT INTO tag(name) VALUES (?);") name) 430 (unless (null? rest) 431 (apply add-tag rest))) 432 433 (defcmd (auto-tags . tag-list) 434 "[tag-name ...]" "Set the list of automatic tags" 435 (trace `(auto-tags . ,tag-list)) 436 (with-transaction db 437 (lambda () 438 (exec (sql db "UPDATE tag SET auto=0;")) 439 (let loop ((todo tag-list)) 440 (unless (null? todo) 441 (set-tag-auto (car todo) 1) 442 (loop (cdr todo))))))) 443 444 (define (n-split l n) 445 (let loop ((todo-l l) (todo-n n) (acc '())) 446 (if (or (zero? todo-n) (null? todo-l)) 447 (reverse acc) 448 (let ((chunk-size (ceiling (/ (length todo-l) todo-n)))) 449 (loop (drop todo-l chunk-size) 450 (sub1 todo-n) 451 (cons (take todo-l chunk-size) acc)))))) 452 453 (define (expand-cols cols) 454 (let loop ((todo cols) (acc '())) 455 (if (> (length todo) 1) 456 (loop 457 (cons (append (cadr todo) 458 (make-list (- (length (car todo)) (length (cadr todo))) 459 "")) 460 (cddr todo)) 461 (let ((width (apply max (map string-length (car todo))))) 462 (cons 463 (append 464 (map (lambda (s t) 465 (string-append 466 s 467 (make-string (- width -2 (string-length s)) 468 #\space))) 469 (car todo) 470 (cadr todo)) 471 (drop (car todo) (length (cadr todo)))) 472 acc))) 473 (reverse (append todo acc))))) 474 475 (defcmd (list-tags #!optional (cols 1) (threshold 0)) 476 "[n-columns [min-count]]" 477 "List available tag, automatic tags are marked with *" 478 (apply for-each 479 (lambda row 480 (write-line (apply string-append row))) 481 (expand-cols 482 (n-split 483 (query 484 (map-rows* 485 (lambda (name auto count) 486 (conc name (if (zero? auto) " (" "* (") count ")"))) 487 (sql db "SELECT name,auto,COUNT(tagrel.url_id) AS cnt 488 FROM tag OUTER LEFT JOIN tagrel ON id=tagrel.tag_id 489 GROUP BY id HAVING cnt >= ? ORDER BY name;") 490 threshold) 491 cols)))) 492 493 (defcmd (remove-auto-tag name . rest) 494 "[tag-name ...]" "Set tags as not automatic" 495 (trace `(remove-auto-tag ,name)) 496 (set-tag-auto name 0) 497 (unless (null? rest) 498 (apply remove-auto-tag rest))) 499 500 (defcmd (remove-tag name . rest) 501 "tag-name [tag-name ...]" "Remove tags" 502 (trace `(remove-tag ,name)) 503 (exec (sql db "DELETE FROM tag WHERE name=?;") name) 504 (unless (null? rest) 505 (apply remove-tag rest))) 506 507 (defcmd (rename-tag old-name new-name) 508 "old-tag-name new-tag-name" "Rename a tag, preserving associations" 509 (trace `(rename-tag ,old-name ,new-name)) 510 (exec (sql db "UPDATE tag SET name=? WHERE name=?;") new-name old-name)) 511 512 ;; Entry Protection 513 514 (define (is-protected? entry-id) 515 (not (zero? 516 (query fetch-value 517 (sql db "SELECT protected FROM entry WHERE id=?;") 518 entry-id)))) 519 520 (define protection-overrides '()) 521 522 (define (is-overridden? entry-id) 523 (any (cut = entry-id <>) protection-overrides)) 524 525 (define (update-allowed? entry-id) 526 (or (not (is-protected? entry-id)) (is-overridden? entry-id))) 527 528 (define-syntax unless-protected 529 (syntax-rules () 530 ((unless-protected entry-id . form) 531 (if (update-allowed? entry-id) 532 (begin . form) 533 (write-line (conc "Warning: entry " entry-id " is protected")))))) 534 535 (define (unoverride! entry-id) 536 (trace `(unoverride! ,entry-id)) 537 (set! protection-overrides (delete! entry-id protection-overrides =))) 538 539 (define (protect* ptime entry-id) 540 (trace `(protect ,ptime ,entry-id)) 541 (unless-protected entry-id 542 (exec (sql db "UPDATE entry SET protected=1,ptime=? WHERE id=?;") 543 ptime entry-id) 544 (update-feed-cache ptime))) 545 546 (defcmd (protect . args) 547 "[[timestamp] entry-id]" "Protect entries from modification" 548 (cond ((null? args) 549 (protect* (current-seconds) cur-entry)) 550 ((null? (cdr args)) 551 (protect* (current-seconds) (car args))) 552 (else 553 (protect* (car args) (cadr args))))) 554 555 (define (override! entry-id) 556 (trace `(override! ,entry-id)) 557 (unless (update-allowed? entry-id) 558 (set! protection-overrides (cons entry-id protection-overrides)))) 559 560 (define (unprotect* mtime entry-id) 561 (trace `(unprotect ,mtime ,entry-id)) 562 (exec (sql db "UPDATE entry SET protected=0,ptime=NULL,mtime=? WHERE id=?;") 563 mtime entry-id) 564 (update-feed-cache mtime)) 565 566 (defcmd (unprotect . args) 567 "[[timestamp] entry-id]" "Unprotect entries from modification" 568 (cond ((null? args) 569 (unprotect* (current-seconds) cur-entry)) 570 ((null? (cdr args)) 571 (unprotect* (current-seconds) (car args))) 572 (else 573 (unprotect* (car args) (cadr args))))) 574 575 (define (without-protection* entry-id proc) 576 (if (or (procedure? proc) (list? proc)) 577 (let ((prev-cur-entry-id cur-entry)) 578 (set! cur-entry entry-id) 579 (if (is-protected? entry-id) 580 (begin 581 (override! entry-id) 582 (if (procedure? proc) (proc) (eval proc)) 583 (unoverride! entry-id)) 584 (if (procedure? proc) (proc) (eval proc))) 585 (set! cur-entry prev-cur-entry-id)) 586 (write-line (conc "Invalid procedure " proc)))) 587 588 (defcmd (without-protection! first . args) 589 "[entry-id] '(...)" "Perform updates bypassing protection" 590 (cond ((null? args) 591 (without-protection* cur-entry first)) 592 ((and (null? (cdr args)) (integer? first)) 593 (without-protection* first (car args))) 594 (else (assert #f "Invalid arguments " (cons first args))))) 595 596 ;; Entry Management 597 598 (define cur-entry 599 (query fetch-value 600 (sql/transient db "SELECT id FROM entry ORDER BY id DESC LIMIT 1;"))) 601 602 (define (time-id-strings args) 603 (cond ((or (null? args) (string? (car args))) 604 (list (current-seconds) cur-entry args)) 605 ((not (integer? (car args))) 606 (assert #f "Unknown type parameter for " (car args))) 607 ((or (null? (cdr args)) (string? (cadr args))) 608 (list (current-seconds) (car args) (cdr args))) 609 ((integer? (cadr args)) 610 (list (car args) (cadr args) (cddr args))) 611 (else (assert #f "Unknown type parameter for " (cadr args))))) 612 613 (define (add-entry* ctime url notes) 614 (trace `(add-entry ,ctime ,url ,notes)) 615 (let ((new-id 616 (with-transaction db 617 (lambda () 618 (exec (sql db "INSERT INTO entry(url,notes,ctime,mtime) VALUES (?,?,?,?);") 619 url notes ctime ctime) 620 (let ((new-id (last-insert-rowid db))) 621 (exec (sql db "INSERT INTO tagrel SELECT ?,id FROM tag WHERE auto=1;") 622 new-id) 623 new-id))))) 624 (set! cur-entry new-id) 625 (write-line (conc "Added " new-id))) 626 (update-feed-cache ctime)) 627 628 (defcmd (add-entry first second . rest) 629 "[timestamp] URL note-line [note-line ...]" "Create a new entry" 630 (if (or (null? rest) (string? first)) 631 (add-entry* (current-seconds) 632 first 633 (apply string-append (map terminate-line (cons second rest)))) 634 (add-entry* first 635 second 636 (apply string-append (map terminate-line rest))))) 637 638 (define (add-notes* mtime entry-id lines) 639 (unless (null? lines) 640 (trace `(add-notes ,mtime ,entry-id . ,lines)) 641 (with-transaction db 642 (lambda () 643 (let ((prev-notes (query fetch-value 644 (sql db "SELECT notes FROM entry WHERE id=?;") 645 entry-id))) 646 (unless-protected entry-id 647 (exec (sql db "UPDATE entry SET notes=?,mtime=? WHERE id=?;") 648 (apply string-append prev-notes 649 (map terminate-line lines)) 650 mtime 651 entry-id)))))) 652 (update-feed-cache mtime)) 653 654 (defcmd (add-notes . args) 655 "[[timestamp] entry-id] note-line [note-line ...]" 656 "Append new lines of notes" 657 (apply add-notes* (time-id-strings args))) 658 659 (define (print-entry-row id url type descr notes protected ptime ctime mtime tags) 660 (write-line (conc vt100-entry-header 661 "#" id (if (zero? protected) "" "*") " - " url 662 vt100-reset)) 663 (unless (null? ctime) (write-line (conc "Created " (rfc-3339 ctime)))) 664 (unless (null? ptime) (write-line (conc "Protected " (rfc-3339 ptime)))) 665 (unless (null? mtime) (write-line (conc "Modified " (rfc-3339 mtime)))) 666 (unless (null? descr) 667 (if (null? type) 668 (write-line "Descripiton:") 669 (write-line (conc "Description (" type "):"))) 670 (write-string descr)) 671 (unless (null? notes) 672 (write-line (conc "Notes:")) 673 (write-string notes)) 674 (if (null? tags) 675 (write-line "No tags.") 676 (write-line (string-append "Tags: " tags)))) 677 678 (define (print-listed-entry-row id url notes protected) 679 (write-line (conc vt100-entry-header 680 "#" id (if (zero? protected) "" "*") " - " url 681 vt100-reset)) 682 (write-string notes)) 683 684 (define (count-selection* text id) 685 (write-line (string-append (if id (conc "#" id ": ") "") 686 "\"" text "\"")) 687 (write-line (conc " -> " (query fetch-value 688 ((if id sql sql/transient) 689 db 690 (string-append 691 "SELECT COUNT(id) FROM entry " 692 text ";")))))) 693 694 (defcmd (count-selection . args) 695 "\"WHERE ...\"|selector-id ..." "Count results of a custom queries" 696 (if (null? args) 697 (query (for-each-row* count-selection*) 698 (sql db "SELECT text,id FROM selector;")) 699 (let loop ((todo args)) 700 (unless (null? todo) 701 (call-with-selector (car todo) count-selection*) 702 (loop (cdr todo)))))) 703 704 (defcmd (list-selection arg) 705 "\"WHERE ...\"|selector-id" "Display a custom query as an entry list" 706 (call-with-selector arg 707 (lambda (selector id) 708 (query (for-each-row* print-listed-entry-row) 709 ((if id sql sql/transient) db 710 (string-append "SELECT id,url,notes,protected FROM entry " 711 selector ";")))))) 712 713 (defcmd (list-tagged tag-name #!optional (count config-list-tagged-count)) 714 "tag-name [limit]" "Display entries with the given tag" 715 (query (for-each-row* print-listed-entry-row) 716 (sql db (cond ((positive? count) 717 "SELECT * FROM 718 (SELECT id,url,notes,protected FROM entry 719 WHERE id IN (SELECT url_id FROM tagrel 720 WHERE tag_id IN (SELECT id FROM tag 721 WHERE name=?)) 722 ORDER BY id DESC LIMIT ?) 723 ORDER BY id ASC;") 724 ((negative? count) 725 "SELECT id,url,notes,protected FROM entry 726 WHERE id IN (SELECT url_id FROM tagrel 727 WHERE tag_id IN (SELECT id FROM tag 728 WHERE name=?)) 729 ORDER BY id ASC LIMIT ?;") 730 (else ; (zero? count) 731 "SELECT id,url,notes,protected FROM entry 732 WHERE id IN (SELECT url_id FROM tagrel 733 WHERE tag_id IN (SELECT id FROM tag 734 WHERE name=?)) 735 OR id=? 736 ORDER BY id ASC;"))) 737 tag-name 738 (abs count))) 739 740 (defcmd (list-untagged) 741 "" "Display entries without any tag" 742 (query (for-each-row* print-listed-entry-row) 743 (sql db "SELECT id,url,notes,protected FROM entry 744 WHERE id NOT IN (SELECT url_id FROM tagrel);"))) 745 746 (define (print-entry* entry-id) 747 (query (for-each-row* print-entry-row) 748 (sql db "SELECT entry.id,url,type,description,notes, 749 protected,ptime,ctime,mtime,group_concat(tag.name,' ') 750 FROM entry 751 LEFT OUTER JOIN tagrel ON entry.id=tagrel.url_id 752 LEFT OUTER JOIN tag ON tag.id=tagrel.tag_id 753 WHERE entry.id=? GROUP BY entry.id;") 754 entry-id)) 755 756 (defcmd (print-entry . args) 757 "[entry-id]" "Display an entry" 758 (if (null? args) 759 (print-entry* cur-entry) 760 (let loop ((todo args)) 761 (unless (null? todo) 762 (print-entry* (car todo)) 763 (loop (cdr todo)))))) 764 765 (defcmd (print-selection arg) 766 "\"WHERE ...\"|selector-id" "Display entries from a custom query" 767 (call-with-selector arg 768 (lambda (selector id) 769 (query 770 (for-each-row* print-entry-row) 771 ((if id sql sql/transient) db 772 (string-append 773 "SELECT entry.id,url,type,description,notes, 774 protected,ptime,ctime,mtime,group_concat(tag.name,' ') 775 FROM entry 776 LEFT OUTER JOIN tagrel ON entry.id=tagrel.url_id 777 LEFT OUTER JOIN tag ON tag.id=tagrel.tag_id " 778 selector 779 " GROUP BY entry.id;")))))) 780 781 (defcmd (random-tagged tag-name) 782 "tag" "Select a random entry with the given tag" 783 (let ((entry-id (query fetch-value 784 (sql db "SELECT url_id FROM tagrel WHERE tag_id IN 785 (SELECT id FROM tag WHERE name=?) 786 ORDER BY RANDOM() LIMIT 1;") 787 tag-name))) 788 (if entry-id 789 (begin 790 (set! cur-entry entry-id) 791 (print-entry)) 792 (write-line "No such entry found")))) 793 794 (defcmd (random-untagged) 795 "" "Select a random entry without tag" 796 (let ((entry-id (query fetch-value 797 (sql db "SELECT id FROM entry WHERE id NOT IN 798 (SELECT url_id FROM tagrel) 799 ORDER BY RANDOM() LIMIT 1;")))) 800 (if entry-id 801 (begin 802 (set! cur-entry entry-id) 803 (print-entry)) 804 (write-line "No such entry found")))) 805 806 (define (guess-type str) 807 (cond ((null? str) '()) 808 ((starts-with? "<" str) "html") 809 ((or (starts-with? " - " str) 810 (starts-with? " + " str)) "markdown-li") 811 (else "text"))) 812 813 (define (set-descr* mtime entry-id type text) 814 (trace `(set-descr ,mtime ,entry-id ,type ,text)) 815 (unless-protected entry-id 816 (exec (sql db "UPDATE entry SET type=?,description=?,mtime=? WHERE id=?;") 817 type text mtime entry-id) 818 (update-feed-cache mtime))) 819 820 (defcmd (set-descr first . args) 821 "[[[mtime] entry-id] type] description" "Sets an entry description" 822 (case (length args) 823 ((0) (set-descr* (current-seconds) cur-entry (guess-type first) first)) 824 ((1) (set-descr* (current-seconds) cur-entry first (car args))) 825 ((2) (set-descr* (current-seconds) first (car args) (cadr args))) 826 ((3) (set-descr* first (car args) (cadr args) (caddr args))) 827 (else (assert #f "Too many arguments to set-descr " (cons first args))))) 828 829 (defcmd (set-entry arg) 830 "entry-id|url" "Set current entry" 831 (cond ((integer? arg) 832 (set! cur-entry arg) 833 (when config-verbose (print-entry))) 834 ((string? arg) 835 (let ((id (query fetch-value 836 (sql db "SELECT id FROM entry WHERE url=?;") 837 arg))) 838 (if id 839 (begin 840 (set! cur-entry id) 841 (when config-verbose (print-entry))) 842 (write-line (conc "No entry found for \"" arg "\""))))) 843 (else (assert #f "Unsupported argument type for " arg)))) 844 845 (define (touch* mtime entry-id) 846 (trace `(touch ,mtime ,entry-id)) 847 (unless-protected entry-id 848 (exec (sql db "UPDATE entry SET mtime=? WHERE id=?;") mtime entry-id) 849 (update-feed-cache mtime))) 850 851 (define (touch . args) 852 (cond ((null? args) 853 (touch* (current-seconds) cur-entry)) 854 ((not (integer? (car args))) 855 (assert #f "Bad type for " (car args))) 856 ((null? (cdr args)) 857 (touch* (current-seconds) (car args))) 858 ((not (integer? (cadr args))) 859 (assert #f "Bad type for " (car args))) 860 (else 861 (touch* (car args) (cadr args))))) 862 863 (define (without-mtime* entry-id proc) 864 (if (or (procedure? proc) (list? proc)) 865 (let ((prev-entry cur-entry) 866 (prev-mtime (query fetch-value 867 (sql db "SELECT mtime FROM entry WHERE id=?;") 868 entry-id))) 869 (set! cur-entry entry-id) 870 (if (procedure? proc) (proc) (eval proc)) 871 (touch* prev-mtime entry-id) 872 (set! cur-entry prev-entry)) 873 (write-line (conc "Invalid procedure " proc)))) 874 875 (defcmd (without-mtime! first . args) 876 "[entry-id] '(...)" "Perform updates and restore entry mtime" 877 (cond ((null? args) 878 (without-mtime* cur-entry first)) 879 ((and (null? (cdr args)) (integer? first)) 880 (without-mtime* first (car args))) 881 (else (assert #f "Invalid arguments " (cons first args))))) 882 883 ;; Entry Tagging 884 885 (define (print-tags* entry-id) 886 (write-line (apply conc (append (list "Tags for " entry-id ":") 887 (query (map-rows (lambda (x) (string-append " " (car x)))) 888 (sql db "SELECT tag.name FROM tagrel 889 OUTER LEFT JOIN tag ON tagrel.tag_id=tag.id 890 WHERE url_id=? ORDER BY tag.name;") 891 entry-id))))) 892 893 (defcmd (print-tags . args) 894 "[entry-id ...]" "Print tags associated with an entry" 895 (if (null? args) 896 (print-tags* cur-entry) 897 (let loop ((todo args)) 898 (unless (null? todo) 899 (print-tags* (car todo)) 900 (loop (cdr todo)))))) 901 902 903 (define (resolve-tag-id tag-name) 904 (let ((result (query fetch-value 905 (sql db "SELECT id from tag WHERE name=?;") 906 tag-name))) 907 (unless result 908 (write-line (conc "Unknown tag " tag-name))) 909 result)) 910 911 (define (exec-on-tags stmt mtime entry-id tag-list) 912 (with-transaction db 913 (lambda () 914 (unless-protected entry-id 915 (let ((tag-id-list (map resolve-tag-id tag-list))) 916 (when (every identity tag-id-list) 917 (let loop ((todo tag-id-list)) 918 (if (null? todo) 919 (exec (sql db "UPDATE entry SET mtime=? WHERE id=?;") 920 mtime entry-id) 921 (begin 922 (exec stmt entry-id (car todo)) 923 (loop (cdr todo)))))))))) 924 (print-tags entry-id) 925 (update-feed-cache mtime)) 926 927 (define (retag* mtime entry-id tag-list) 928 (trace `(retag ,mtime ,entry-id . ,tag-list)) 929 (unless-protected entry-id 930 (exec (sql db "DELETE FROM tagrel WHERE url_id=?;") entry-id) 931 (exec-on-tags (sql db "INSERT OR IGNORE INTO tagrel VALUES (?,?);") 932 mtime entry-id tag-list))) 933 934 (defcmd (retag . args) 935 "[[timestamp] entry-id] tag-name [tag-name ...]" 936 "Overwrite tag list for an entry" 937 (apply retag* (time-id-strings args))) 938 939 (define (tag* mtime entry-id tag-list) 940 (unless (null? tag-list) 941 (trace `(tag ,mtime ,entry-id . ,tag-list)) 942 (exec-on-tags (sql db "INSERT OR IGNORE INTO tagrel VALUES (?,?);") 943 mtime entry-id tag-list))) 944 945 (defcmd (tag . args) 946 "[[timestamp] entry-id] tag-name [tag-name ...]" 947 "Associate tags to an entry" 948 (apply tag* (time-id-strings args))) 949 950 (define (untag* mtime entry-id tag-list) 951 (unless (null? tag-list) 952 (trace `(untag ,mtime ,entry-id . ,tag-list)) 953 (exec-on-tags (sql db "DELETE FROM tagrel WHERE url_id=? AND tag_id=?;") 954 mtime entry-id tag-list))) 955 956 (defcmd (untag . args) 957 "[[timestamp] entry-id] tag-name [tag-name ...]" 958 "Disssociates tags from an entry" 959 (apply untag* (time-id-strings args))) 960 961 ;;;;;;;;;;;;;;;;;;;; 962 ;; Editor Spawning 963 964 (define (comment-link section url) 965 (let* ((rss-url (query fetch-value 966 (sql db "SELECT url FROM source_rss WHERE name=?;") 967 section))) 968 (if rss-url 969 (let ((rss (with-input-from-request rss-url #f rss:read))) 970 (let loop ((items (rss:feed-items rss))) 971 (cond 972 ((null? items) #f) 973 ((string=? url (rss:item-link (car items))) 974 (alist-ref 'comments (rss:item-attributes (car items)))) 975 (else (loop (cdr items)))))) 976 #f))) 977 978 (define (edit-descr* entry-id) 979 (let ((file-name (create-temporary-file 980 (string-append "." 981 (get-config/default "description-ext" "txt")))) 982 (fields 983 (query fetch-row 984 (sql db "SELECT description,notes,url FROM entry WHERE id=?;") 985 entry-id))) 986 (unless (null? fields) 987 (call-with-output-file file-name 988 (lambda (port) 989 (if (or (null? (car fields)) (string=? (car fields) "")) 990 (let* ((s-sec (substring-index "[" (cadr fields))) 991 (e-sec (if s-sec 992 (substring-index "]" (cadr fields) s-sec) 993 #f)) 994 (sect (if e-sec 995 (substring (cadr fields) (+ s-sec 1) e-sec) #f)) 996 (comm (if sect (comment-link sect (caddr fields)) #f))) 997 (write-string (conc " + [](" (caddr fields) ")\n") #f port) 998 (when sect 999 (write-string 1000 (conc "(via " 1001 (if comm (conc "[" sect "](" comm ")") sect) 1002 " sur #gcufeed)\n") 1003 #f port))) 1004 (write-string (car fields) #f port)) 1005 (unless (null? (cadr fields)) 1006 (write-string "-+-+-\n" #f port) 1007 (write-string (cadr fields) #f port))))) 1008 (when config-editor 1009 (process-wait 1010 (process-run (string-append config-editor " " (qs file-name))))) 1011 (let ((result (call-with-input-file file-name 1012 (lambda (port) 1013 (let* ((text (read-string #f port)) 1014 (end (substring-index-ci "-+-+-\n" text))) 1015 (if end 1016 (substring text 0 end) 1017 text)))))) 1018 (delete-file file-name) 1019 (if (or (zero? (string-length result)) 1020 (equal? (if (or (null? fields) (null? (car fields))) 1021 "" (car fields)) 1022 result)) 1023 #f 1024 result)))) 1025 1026 1027 (defcmd (edit-descr . args) 1028 "[[mtime] entry-id]" "Describe using an external editor" 1029 (let ((new-value (case (length args) 1030 ((0) (edit-descr* cur-entry)) 1031 ((1) (edit-descr* (car args))) 1032 ((2) (edit-descr* (cadr args))) 1033 (else 1034 (assert #f "Too many arguments to edit-descr " args))))) 1035 (when new-value 1036 (case (length args) 1037 ((0) (set-descr* (current-seconds) 1038 cur-entry 1039 (guess-type new-value) 1040 new-value)) 1041 ((1) (set-descr* (current-seconds) 1042 (car args) 1043 (guess-type new-value) 1044 new-value)) 1045 ((2) (set-descr* (car args) 1046 (cadr args) 1047 (guess-type new-value) 1048 new-value)) 1049 (else (assert #f "Too many arguments to edit-descr " args)))))) 1050 1051 (define (auto-cols widths avail) 1052 (letrec ((len (vector-length widths)) 1053 (w-slice (lambda (start len acc) 1054 (if (< len 1) 1055 acc 1056 (w-slice (+ start 1) (- len 1) 1057 (max acc (vector-ref widths start)))))) 1058 (w-total (lambda (start stride acc) 1059 (if (< (+ start stride) len) 1060 (w-total (+ start stride) 1061 stride 1062 (cons (+ (car acc) 1 (w-slice start stride 0)) 1063 acc)) 1064 (cons (+ (car acc) (w-slice start (- len start) 0)) 1065 acc)))) 1066 (h-cols (lambda (ncols) (quotient (+ len ncols -1) ncols))) 1067 (w-cols (lambda (ncols) (w-total 0 (h-cols ncols) (list 0))))) 1068 (let loop ((ncols len) (best #f)) 1069 (if (zero? ncols) best 1070 (let ((w (w-cols ncols)) (h (h-cols ncols))) 1071 (loop (- ncols 1) 1072 (if (and (< (car w) avail) 1073 (or (not best) (<= h (car best)))) 1074 (list h (list->vector (reverse (cdr w)))) 1075 best))))))) 1076 1077 (define (select-tags** entry-id tags) 1078 (let* ((ntags (vector-length tags)) 1079 (state (list->vector (map cadddr (vector->list tags)))) 1080 (cols (auto-cols (list->vector 1081 (map (lambda (x) 1082 (+ (string-length (cadr x)) 1083 (string-length (caddr x)))) 1084 (vector->list tags))) (COLS))) 1085 (stride (car cols)) 1086 (x-cols (cadr cols)) 1087 (show-tag (lambda (index sel) 1088 (unless (zero? (vector-ref state index)) 1089 ; (attron (COLOR_PAIR 1))) 1090 (attron A_REVERSE)) 1091 (when (= index sel) 1092 ; (attron A_REVERSE)) 1093 (attron A_UNDERLINE)) 1094 (mvprintw 1095 (remainder index stride) 1096 (vector-ref x-cols (quotient index stride)) 1097 "~A~A" 1098 (cadr (vector-ref tags index)) 1099 (caddr (vector-ref tags index))) 1100 (when (= index sel) 1101 ; (attroff A_REVERSE)) 1102 (attroff A_UNDERLINE)) 1103 (unless (zero? (vector-ref state index)) 1104 ; (attroff (COLOR_PAIR 1))))) 1105 (attroff A_REVERSE)))) 1106 (update-tags (lambda (old new) (show-tag old new) (show-tag new new)))) 1107 (keypad (stdscr) #t) 1108 (noecho) 1109 (curs_set 0) 1110 ; (start_color) 1111 ; (init_pair 1 COLOR_BLUE COLOR_BLACK) 1112 (let init ((index 0)) 1113 (when (< index ntags) 1114 (show-tag index 0) 1115 (init (+ index 1)))) 1116 (let loop ((sel 0)) 1117 (let ((c (char->integer (getch)))) 1118 (cond 1119 ((= c KEY_UP) 1120 (let ((next-sel (modulo (- sel 1) ntags))) 1121 (update-tags sel next-sel) 1122 (loop next-sel))) 1123 ((= c KEY_DOWN) 1124 (let ((next-sel (modulo (+ sel 1) ntags))) 1125 (update-tags sel next-sel) 1126 (loop next-sel))) 1127 ((= c KEY_LEFT) 1128 (let ((next-sel (if (>= sel stride) 1129 (- sel stride) 1130 (min (+ sel (- ntags (modulo ntags stride))) 1131 (- ntags 1))))) 1132 (update-tags sel next-sel) 1133 (loop next-sel))) 1134 ((= c KEY_RIGHT) 1135 (let ((next-sel (cond ((< (+ sel stride) ntags) 1136 (+ sel stride)) 1137 ((< sel (- ntags (modulo ntags stride))) 1138 (- ntags 1)) 1139 (else (modulo sel stride))))) 1140 (update-tags sel next-sel) 1141 (loop next-sel))) 1142 ((= c 32) 1143 (vector-set! state sel (- 1 (vector-ref state sel))) 1144 (show-tag sel sel) 1145 (loop sel)) 1146 ((= c 10) 1147 (let result ((index 0) (add '()) (del '())) 1148 (cond 1149 ((>= index ntags) 1150 (list add del)) 1151 ((= (cadddr (vector-ref tags index)) (vector-ref state index)) 1152 (result (+ index 1) add del)) 1153 ((zero? (vector-ref state index)) 1154 (result (+ index 1) add 1155 (cons (cadr (vector-ref tags index)) del))) 1156 (else 1157 (result (+ index 1) 1158 (cons (cadr (vector-ref tags index)) add) 1159 del))))) 1160 ((= c 27) '(()())) 1161 ((or (<= 65 c 90) (<= 97 c 122)) 1162 (let search ((prev-sel sel) 1163 (prev-ch (char->integer (string-ref 1164 (cadr (vector-ref tags sel)) 0)))) 1165 (let* ((next-sel (modulo (+ prev-sel 1) ntags)) 1166 (next-ch (char->integer (string-ref 1167 (cadr (vector-ref tags next-sel)) 0)))) 1168 (cond 1169 ((= next-sel sel) 1170 (loop sel)) 1171 ((or (= next-ch c) (< prev-ch c next-ch)) 1172 (update-tags sel next-sel) 1173 (loop next-sel)) 1174 (else (search next-sel next-ch)))))) 1175 (else (mvprintw (+ 1 stride) 0 "~S ~S" KEY_DOWN c) (loop sel))))))) 1176 1177 (define (select-tags* entry-id) 1178 (if (update-allowed? entry-id) 1179 (let ((tags (list->vector (query 1180 (map-rows* (lambda (id name count active) 1181 (list id name (conc " (" count ")") 1182 active))) 1183 (sql db 1184 "SELECT id,name,COUNT(url_id),COALESCE(MAX(url_id==?),0) 1185 FROM tag LEFT OUTER JOIN tagrel ON tag_id=tag.id 1186 GROUP BY tag.name;") 1187 entry-id)))) 1188 (dynamic-wind initscr (lambda () (select-tags** entry-id tags)) endwin)) 1189 '(()()))) 1190 1191 (defcmd (select-tags . args) 1192 "[[mtime] entry-id]" "Interactively select tags using dialog(1)" 1193 (let* ((entry-id (case (length args) 1194 ((0) cur-entry) 1195 ((1) (car args)) 1196 ((2) (cadr args)) 1197 (else 1198 (assert #f "Too many arguments to select-tags " args)))) 1199 (mtime (if (= 2 (length args)) (car args) (current-seconds))) 1200 (changes (select-tags* entry-id)) 1201 (added (car changes)) 1202 (removed (cadr changes))) 1203 (unless-protected entry-id 1204 (untag* (- mtime 1) entry-id removed) 1205 (tag* mtime entry-id added)))) 1206 1207 ;;;;;;;;;;;;;;;;;;;;; 1208 ;; Gruik Management 1209 1210 (define (pull-gruiks* mtime mark) 1211 (let ((last-id (query fetch-value (sql db "SELECT MAX(id) FROM entry;")))) 1212 (exec 1213 (sql db "INSERT OR IGNORE 1214 INTO entry(url,type,description,notes,ctime,mtime) 1215 SELECT url, 1216 CASE WHEN description IS NULL THEN NULL 1217 WHEN substr(description,1,1)='<' THEN 'html' 1218 WHEN substr(description,1,3)=' - ' 1219 OR substr(description,1,3)=' + ' THEN 'markdown-li' 1220 ELSE 'text' END, 1221 trim(description,char(10))||char(10), 1222 trim(notes,char(10))||char(10), 1223 stime,? 1224 FROM gruik 1225 WHERE mark=? AND url NOT IN (SELECT url FROM entry);") 1226 mtime 1227 mark) 1228 (exec 1229 (sql db "INSERT OR IGNORE INTO tagrel(url_id,tag_id) 1230 SELECT entry.id,tag_id 1231 FROM gruik_tags LEFT OUTER JOIN gruik ON gruik_id = gruik.id 1232 LEFT OUTER JOIN entry ON gruik.url = entry.url 1233 WHERE gruik.mark=?;") 1234 mark) 1235 (exec 1236 (sql db "DELETE FROM gruik WHERE mark=?;") 1237 mark) 1238 (print-selection (conc "WHERE entry.id > " last-id))) 1239 (update-feed-cache mtime)) 1240 1241 (defcmd (pull-gruiks mark) 1242 "mark" "import gruiks at the given mark level" 1243 (let* ((wh (conc "WHERE url IN (SELECT url FROM gruik WHERE mark=" mark ")")) 1244 (n (query fetch-value 1245 (sql/transient db (conc "SELECT COUNT(id) FROM entry " wh))))) 1246 (if (zero? n) 1247 (pull-gruiks* (current-seconds) mark) 1248 (begin 1249 (write-line (conc vt100-alert "Conflicting gruiks:" vt100-reset)) 1250 (query 1251 (for-each-row* (lambda (id url notes) 1252 (write-line (conc id " - " vt100-entry-header url vt100-reset)) 1253 (write-line notes))) 1254 (sql db "SELECT id,url,notes FROM gruik 1255 WHERE mark=? AND url IN (SELECT url FROM entry);") 1256 mark) 1257 (write-line (conc vt100-alert "Conflicting entries:" vt100-reset)) 1258 (print-selection wh))))) 1259 1260 (defcmd (catchup-gruik) 1261 "" "skip all past unfetched gruiks" 1262 (let ((src-path (get-config "gruik-source"))) 1263 (write-line (conc "Before: " (get-config "gruik-seen"))) 1264 (when src-path 1265 (set-config "gruik-seen" (file-size src-path))) 1266 (write-line (conc "After " (get-config "gruik-seen"))))) 1267 1268 ;;;;;;;;;;;;;;;;;;;; 1269 ;; Feed Generation 1270 1271 (define (atom-content type descr notes) 1272 (cond ((null? descr) `(atom:content ,notes)) 1273 ((null? type) `(atom:content ,descr)) 1274 ((equal? type "markdown-li") 1275 (let ((acc (open-output-string)) 1276 (prev-output (current-output-port))) 1277 (current-output-port acc) 1278 (let ((result (markdown->html (substring descr 3)))) 1279 (current-output-port prev-output) 1280 (if result 1281 `(atom:content (@ (type "html")) ,(get-output-string acc)) 1282 `(atom:content ,descr))))) 1283 (else `(atom:content (@ (type ,type)) ,descr)))) 1284 1285 (define (feed->sxml id url type descr notes ptime ctime mtime) 1286 `(atom:entry 1287 (atom:id ,(string-append config-entry-id-prefix (number->string id))) 1288 (atom:title ,url) 1289 (atom:updated ,(rfc-3339 mtime)) 1290 (atom:published ,(rfc-3339 (if (null? ptime) ctime ptime))) 1291 (atom:link (@ (rel "related") (href ,url))) 1292 ,(atom-content type descr notes) 1293 ,@(query (map-rows (lambda (x) `(atom:category (@ (term ,(car x)))))) 1294 (sql db "SELECT tag.name FROM tagrel 1295 OUTER LEFT JOIN tag ON tagrel.tag_id=tag.id 1296 WHERE url_id=? ORDER BY tag.name;") 1297 id))) 1298 1299 (define (write-feed mtime title self rows) 1300 (write-string 1301 (serialize-sxml 1302 `(*TOP* (@ (*NAMESPACES* (atom "http://www.w3.org/2005/Atom"))) 1303 (*PI* xml "version='1.0' encoding='utf-8'") 1304 (atom:feed 1305 (atom:title ,title) 1306 (atom:author 1307 (atom:name ,(if config-author-name 1308 config-author-name 1309 "Unknown Author")) 1310 ,@(if config-author-email `((atom:email ,config-author-email)) '()) 1311 ,@(if config-author-uri `((atom:uri ,config-author-uri)) '())) 1312 (atom:id ,self) 1313 (atom:link (@ (rel "self") (href ,self))) 1314 (atom:updated ,(rfc-3339 mtime)) 1315 ,@(map (lambda (row) (apply feed->sxml row)) rows))) 1316 ns-prefixes: '((*default* . "http://www.w3.org/2005/Atom"))))) 1317 1318 (define (generate-feed forced feed-id filename url selector title mtime) 1319 (let* ((rows (query fetch-rows 1320 (sql db (string-append "SELECT id,url,type,description, 1321 notes,ptime,ctime,mtime 1322 FROM entry " selector ";")))) 1323 (generate? 1324 (cond ((null? rows) 1325 (when config-verbose 1326 (write-line (conc "Feed " feed-id " is empty"))) 1327 #f) 1328 ((any (cut = feed-id <>) dirty-feeds) 1329 (when config-verbose 1330 (write-line (conc "Generating feed " feed-id))) 1331 #t) 1332 (forced 1333 (when config-verbose 1334 (write-line (conc "Generating feed " feed-id 1335 " unconditionally"))) 1336 #t) 1337 (else 1338 (when config-verbose 1339 (write-line (conc "Feed " feed-id 1340 " is already up to date"))) 1341 #t)))) 1342 (when generate? 1343 (with-output-to-file filename 1344 (lambda () (write-feed (if (null? mtime) (list-ref (car rows) 7) mtime) 1345 title url rows))) 1346 (set! dirty-feeds (delete! feed-id dirty-feeds =)) 1347 (set! feed-cache 1348 (alist-update! feed-id 1349 (map (lambda (row) (list (car row) (list-ref row 7))) 1350 rows) 1351 feed-cache =))))) 1352 1353 (define (generate-feeds forced id-list) 1354 (for-each 1355 (lambda (row) (apply generate-feed forced row)) 1356 (if (null? id-list) 1357 (query fetch-rows 1358 (sql db "SELECT id,filename,url,selector,title,mtime 1359 FROM feed WHERE active=1;")) 1360 (map (lambda (id) 1361 (query fetch 1362 (sql db "SELECT id,filename,url,selector,title,mtime 1363 FROM feed WHERE id=?;") 1364 id)) 1365 id-list)))) 1366 1367 (defcmd (force-generate . args) 1368 "[feed-id ...]" 1369 "Generate unconditionally the given feeds, or all active feeds" 1370 (generate-feeds #t args)) 1371 1372 (defcmd (generate . args) 1373 "[feed-id ...]" "Generate if needed the given feeds, or all active feeds" 1374 (generate-feeds #f args)) 1375 1376 ;;;;;;;;;;;;; 1377 ;; Auto Add 1378 1379 (define (auto-add lines) 1380 (unless arg-replay 1381 (trace `(auto-add ,lines)) 1382 (let loop ((index 0) (urls '())) 1383 (let* ((start0 (substring-index-ci "https://" lines index)) 1384 (start (if start0 start0 1385 (substring-index-ci "http://" lines index))) 1386 (end (if start 1387 (apply min 1388 (filter identity 1389 (list 1390 (string-length lines) 1391 (substring-index " " lines start) 1392 (substring-index "\n" lines start)))) 1393 #f))) 1394 (cond (start 1395 (loop end (cons (substring lines start end) urls))) 1396 ((null? urls) 1397 (write-line (conc "Warning: no URL found"))) 1398 (else 1399 (for-each (lambda (url) (add-entry url lines)) urls))))))) 1400 1401 ;;;;;;;;;;;;;; 1402 ;; Main loop 1403 1404 (defcmd (replay filename) 1405 "filename" "Replay the given file" 1406 (let ((old-arg-replay arg-replay)) 1407 (set! arg-replay #t) 1408 (load filename) 1409 (set! arg-replay old-arg-replay))) 1410 1411 (define write-each-row 1412 (for-each-row 1413 (lambda (row) (if (= 1 (length row)) 1414 (write-line (->string (car row))) 1415 (begin (write row) (newline)))))) 1416 1417 (define (write-query text . args) 1418 (apply query write-each-row (sql/transient db text) args)) 1419 1420 (defcmd (help) 1421 "" "Display this help" 1422 (for-each 1423 (lambda (row) 1424 (write-line (conc 1425 "(" 1426 (car row) 1427 (if (zero? (string-length (cadr row))) "" " ") 1428 (cadr row) 1429 ")")) 1430 (write-line (conc " " (caddr row)))) 1431 cmd-list)) 1432 1433 (set! cmd-list (sort! cmd-list (lambda (r1 r2) (string<? (car r1) (car r2))))) 1434 1435 (define completion-ptr cmd-list) 1436 (define new-completion #t) 1437 (define (completer prefix state) 1438 (when (zero? state) 1439 (set! completion-ptr cmd-list) 1440 (set! new-completion #t)) 1441 (let ((buf (line-buffer))) 1442 (cond ((and (positive? (string-length buf)) 1443 (not (eqv? (string-ref buf 0) #\())) 1444 #f) 1445 ((substring-index " " buf) 1446 (let ((other-state (if new-completion 0 state))) 1447 (set! new-completion #f) 1448 (scheme-completer prefix other-state))) 1449 (else 1450 (let loop () 1451 (cond ((null? completion-ptr) 1452 #f) 1453 ((starts-with? prefix (caar completion-ptr)) 1454 (let ((result (caar completion-ptr))) 1455 (set! completion-ptr (cdr completion-ptr)) 1456 result)) 1457 (else 1458 (set! completion-ptr (cdr completion-ptr)) 1459 (loop)))))))) 1460 1461 (define state 'general) 1462 (define (prompt) 1463 (string-append 1464 (if (null? protection-overrides) 1465 "" 1466 (string-append "!" 1467 (string-intersperse (map ->string protection-overrides) ","))) 1468 (cond ((eqv? state 'general) "> ") 1469 ((eqv? state 'in-command) "… ") 1470 (else "? ")))) 1471 1472 (define (interactive-main) 1473 (basic-quote-characters-set! "\"|") 1474 (completer-word-break-characters-set! "\"\'`;|()[] ") 1475 (completer-set! completer) 1476 (variable-bind! "blink-matching-paren" "on") 1477 (paren-blink-timeout-set! 200000) 1478 1479 (let ((handler (signal-handler signal/int))) 1480 (set-signal-handler! signal/int (lambda (s) (cleanup-after-signal!) 1481 (reset-after-signal!) 1482 (handler s)))) 1483 (on-exit reset-terminal!) 1484 (current-input-port (make-readline-port prompt)) 1485 1486 (let main-loop () 1487 (let ((c (peek-char))) 1488 (cond ((eof-object? c)) 1489 ((eqv? c #\() 1490 (set! state 'in-command) 1491 (handle-exceptions 1492 exn 1493 (begin 1494 (print-error-message exn) 1495 (print-call-chain)) 1496 (eval (read))) 1497 (set! state 'general) 1498 (main-loop)) 1499 (else 1500 (let data-loop ((acc (list (read-line)))) 1501 (if (char-ready?) 1502 (data-loop (cons (read-line) acc)) 1503 (let ((lines (reverse-string-append 1504 (map terminate-line acc)))) 1505 (when (positive? (string-length lines)) 1506 (auto-add lines)) 1507 (main-loop))))))))) 1508 1509 (cond ((not arg-replay) 1510 (interactive-main)) 1511 ((eqv? (string-ref arg-replay 0) #\() 1512 (eval (read (open-input-string arg-replay)))) 1513 (else 1514 (load arg-replay)))