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