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