pax_global_header00006660000000000000000000000064151631240360014513gustar00rootroot0000000000000052 comment=8dad34d16abea52751221cc6ea5b9d11e7326978 newsraft/000077500000000000000000000000001516312403600127105ustar00rootroot00000000000000newsraft/.editorconfig000066400000000000000000000003071516312403600153650ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf indent_style = tab insert_final_newline = true [src/termbox2.h] indent_style = space indent_size = 4 [*.yaml] indent_style = space indent_size = 2 newsraft/.gitignore000066400000000000000000000002431516312403600146770ustar00rootroot00000000000000newsraft newsraft-test newsraft-test-log newsraft-test-feeds newsraft-test-database* libnewsraft.so flog vlog vgcore.* callgrind.out* compile_commands.json .cache newsraft/.woodpecker/000077500000000000000000000000001516312403600151305ustar00rootroot00000000000000newsraft/.woodpecker/alpine.yaml000066400000000000000000000004531516312403600172660ustar00rootroot00000000000000when: - event: manual - event: push branch: main steps: - name: alpine image: alpine:latest commands: - apk add --no-cache build-base curl-dev expat-dev gumbo-parser-dev sqlite-dev - make - ./newsraft -v - make check newsraft/.woodpecker/arch.yaml000066400000000000000000000004521516312403600167320ustar00rootroot00000000000000when: - event: manual - event: push branch: main steps: - name: arch image: archlinux:latest commands: - pacman --needed --noconfirm -Syu base-devel curl expat gumbo-parser sqlite - make - ./newsraft -v - make check newsraft/README.md000066400000000000000000000117351516312403600141760ustar00rootroot00000000000000### Description Newsraft is a [feed reader](https://en.wikipedia.org/wiki/News_aggregator) with text-based user interface. It's greatly inspired by [Newsboat](https://www.newsboat.org) and is its lightweight counterpart ![Newsraft in action](doc/newsraft.png) ### Features * Parallel downloads * Section-based feeds grouping * Open any link in any program * [Filter news with SQL conditions](https://newsraft.codeberg.page/#item-rule_(*)) * View news from all feeds with explore mode (tab) * [Automatic updates for feeds and sections](https://newsraft.codeberg.page/#reload-period_(*)) * Per-feed/per-section settings and key bindings * Assign multiple actions to key bindings * Text search by news titles and content * View news content interactively * Sort menus by your most desired parameters * Detailed error reports on failed updates * Scripted feeds (read from command output) * [Support for practically all feed formats](https://newsraft.codeberg.page/#FORMATS_SUPPORT) * Import/export OPML * Come try segfault me, baby ;) Check out [comparison of Newsraft and Newsboat](https://codeberg.org/newsraft/newsraft/src/branch/main/doc/comparison-newsboat.md) ### Dependencies * [curl](https://curl.se) >= `7.87.0` * [expat](https://github.com/libexpat/libexpat) >= `2.4.8` * [gumbo-parser](https://codeberg.org/gumbo-parser/gumbo-parser) >= `0.11.0` * [sqlite](https://www.sqlite.org) >= `3.38.0` Build-time: C99 compiler, POSIX make If you want to change sources: [gperf](https://www.gnu.org/software/gperf), [scdoc](https://git.sr.ht/~sircmpwn/scdoc), [mandoc](https://mandoc.bsd.lv) ### Install [![Packaging status from Repology](https://repology.org/badge/vertical-allrepos/newsraft.svg?columns=4)](https://repology.org/project/newsraft/versions) ### Build ``` make ``` ``` make install ``` More details: [doc/build-instructions.md](https://codeberg.org/newsraft/newsraft/src/branch/main/doc/build-instructions.md) ### Learn more * `man newsraft` * [newsraft.codeberg.page](https://newsraft.codeberg.page) * [doc/examples](https://codeberg.org/newsraft/newsraft/src/branch/main/doc/examples) * **#newsraft** at [libera.chat](https://libera.chat) ### Contribute * Reporting bugs: [doc/contributing-report.md](https://codeberg.org/newsraft/newsraft/src/branch/main/doc/contributing-report.md) * Making changes: [doc/contributing-change.md](https://codeberg.org/newsraft/newsraft/src/branch/main/doc/contributing-change.md) ### FAQ
Why it's called Newsraft? This is a rip-off of Newsboat, replacing "boat" with "raft", which emphasizes a smaller codebase.
Why a raccoon in Newsraft's logo? Because he's cute, dummy. His name is Malin.
How do I bind mpv to run in the background?
bind m exec setsid mpv --terminal=no "%l" &
How do I filter out things I don't want to see in my feed? See item-rule setting.
Can I alter feed's content before Newsraft processes it? Yes, you can do practically anything before Newsraft takes over. It's done via shell interlayer: any shell command in between of $( and ) will be executed on reload and its standard output will be taken for a feed content. Here are examples of such feeds:

$(gemget -sq gemini://example.org/feed.xml) "Simple blog"
$($HOME/bin/html2rss http://example.org/index.html) "Local news"
Why some of my feeds are lagging behind the upstream website even after updating? Some web servers ask Newsraft to withhold content to reduce network load. Newsraft fulfills these web server wishes by default. There are settings to disable Newsraft's respect for web servers and make it a bad boy, if you are that kind of person.
I get a lot of getaddrinfo() thread failed to start errors. What do I do? Usually it happens because your setup can't handle many concurrent DNS resolves. Try to reduce the value of download-max-connections setting.
My database is over 9000 GB now. What do I do?
Where I can find a change log? See doc/changes.md file.
How is Newsraft licensed? The license is ISC because its name is sweet.
newsraft/doc/000077500000000000000000000000001516312403600134555ustar00rootroot00000000000000newsraft/doc/build-instructions.md000066400000000000000000000040201516312403600176340ustar00rootroot00000000000000## Obtaining dependencies | Operating system | Command | |------------------|---------------------------------------------------------------------------------------------| | Alpine Linux | `apk add build-base curl-dev expat-dev gumbo-parser-dev sqlite-dev` | | Arch Linux | `pacman -S base-devel curl expat gumbo-parser sqlite` | | Source Mage | `cast curl expat gumbo-parser sqlite` | | Debian/Ubuntu | `apt install build-essential libcurl4-openssl-dev libexpat-dev libgumbo-dev libsqlite3-dev` | | Fedora Linux | `dnf install gcc make libcurl-devel expat-devel gumbo-parser-devel sqlite-devel` | | Void Linux | `xbps-install base-devel libcurl-devel expat-devel gumbo-parser-devel sqlite-devel` | | OpenBSD | `pkg_add curl gumbo sqlite` | | Haiku | `pkgman install curl_devel expat_devel gumbo_devel sqlite_devel` | | macOS | `brew install gumbo-parser` | ## Compilation | Operating system | Command | |-----------------------|-------------------------------------------------------------------------------------| | General Unix-like | `make` | | OpenBSD | `make CFLAGS="-I/usr/local/include" LDFLAGS="-L/usr/local/lib"` | | macOS (Apple Silicon) | `make CFLAGS="-I/opt/homebrew/include" LDFLAGS="-L/opt/homebrew/lib"` | | macOS (Intel) | `make CFLAGS="-I/usr/local/include" LDFLAGS="-L/usr/local/lib"` | ## Examination ``` ./newsraft ``` ## Installing (run as root) ``` make install ``` newsraft/doc/changes.md000066400000000000000000000247761516312403600154270ustar00rootroot00000000000000# newsraft 0.36 "flip a mood" (2026-04-01) * Stuart Henderson (@sthen) thanks for #249 * fiore (@fiore) thanks for #251 * Cobe Liu (@cobeml) thanks for #252 some more little fixes and nice-to-haves * fix year 2038 overflow in If-Modified-Since header (#249) * set window title with an escape sequence (#251) * update build instructions for macOS (#252) * cancel search input with ^C key (#253) * discard search query text on search input canceling (#253) * extend menu-responsiveness setting to feeds and sections (#254) * update items menu on mark-read-all regardless of menu-responsiveness (#260) * respect RFC 3986 when detecting links in pager what should `( ')>` say to package maintainers? `( ')<` thanks # newsraft 0.35 "physics jitter" (2026-01-01) * Dung Ngo (@nlqdung) thanks for #242 * jhhm (@jhhm) thanks for #245 * Kafva (@kafva) thanks for #246, #248 * schmijoe (@schmijoe) thanks for #247 * zhml (@zhml) thanks for #240, #241 oh, it's a new year already - time to throw in some minor improvements * add support for soft hyphens in pager (#247) * fallback to attachment url if there is no item link (#245) * fix nested parentheses parsing in `$(...)` feeds (#242, #243) * include categories when exporting/importing feeds to/from OPML (#221) * exit early on invalid locale settings (#241) * add hint about converting encodings to error message (#238) * add dependencies list for Haiku OS thank you to everyone involved in packaging Newsraft in repos!!! <3 # newsraft 0.34 "potolok ledyanoy" * Solt Budavári (@solt87) thanks for #236 fixes pretty serious oversight made by me in the previous release. thanks to Solt Budavári for reporting it so fast sorry to bother you with another release in such a short time, lads. promise this will not happen again (probably) # newsraft 0.33 "energizor" i'd like to start a tradition of mentioning contributors to the release, so here we go: * caveman (@caveman) thanks for #132, #227 * David Pedersen (@Limero) thanks for #216, #233, #234 * Tafnur (@tafnur) thanks for #224, #230, #231 * W4RH4WK (@W4RH4WK) thanks for #225, #226 this release is a little late because some good stuff has been brewing over the last few days * add `read-on-arrival` setting * add `scrollwrap` setting (#216) * add `color-list-item-selected` setting (#132) * add `color-list-feed-selected` setting (#132) * add `color-list-section-selected` setting (#132) * fix colorN value offset by 1 in color settings (#231) * gracefully handle zero size state of terminal emulator (#218) * apply search cumulatively instead of overwriting previous query * don't remove trailing slashes from feed urls (#224) * store feed urls without trailing slashes in the database (#224) * let go of terminal control while executing commands (#225) * add total items count specifier to `menu-feed-entry-format` (#234) * provide update error for generator feeds on failed command * use esc mode only when escape key is bound (#227) * add items sorting by download time (#123) * bind `?` to `exec man newsraft` by default > if you are used to wrapping behavior of list menu jumps (e.g. `next-unread`), > now you have to enable `scrollwrap` to make it wrapping just as in 0.32 _**aaaand, big thanks to everyone involved in Newsraft packaging <3**_ # newsraft 0.32 "conflagratio" * add `toggle-read` action (#213) * add `toggle-important` action (#213) * add `global-section-hide` setting (#144) * make `FEATURECFLAGS` in makefile universally correct for most platforms * fix termbox2 behavior to handle `TERMINFO` environment variable as in ncurses (#212, [github](https://github.com/termbox/termbox2/pull/104)) * provide more log information during termbox2 initialization (#212) * prefer a link to webpage instead of feed URL when converting relative item links to absolute notation thank you to all of you who keep ports updated, i see your work <3 <3 <3 # newsraft 0.31 "way fare" * dependency on `ncurses` is gone, it's not needed to build Newsraft anymore so currently only 4 libraries are needed to build Newsraft: libcurl, libexpat, libgumbo and libsqlite3. note that you don't need scdoc/mandoc during the build * add `download-max-connections` setting (#187) * add `ignore-no-color` setting (#204) * add `sort-by-time-update` action (#185) * add `sort-by-time-publication` action (#185) * fallback to `open` in `open-in-browser-command` setting on macOS (#203) * don't call `make clean` before running tests every time * prevent rebuild in some cases when calling `make install` and yet again, a low bow to every one of you who lead Newsraft ports! # newsraft 0.30 "one cabbage a day and the doc's never away" this is a big one. dear repository maintainers, here's an important heads up: * dependency on `yajl` is gone, it's not needed to build Newsraft anymore * requirements for `sqlite` are raised to 3.38.0. now we use its json facilities * new metadata file is available for packaging on Linux: `doc/newsraft.desktop` ok i hope these are visible enough. other substantial changes are: * add `edit` action (#31, #117, #133) * add `find` action (#31, #117, #133) * add `user_data` column to feeds and items database tables (#31, #117, #133) so now you have the ability to play with the database. one example use of this is a custom tagging functionality: ``` # mark item "toWatch" bind w edit UPDATE items SET user_data = json_set(IFNULL(user_data, '{}'), '$.toWatch', 1) WHERE @selected ``` ``` # unmark item "toWatch" bind W edit UPDATE items SET user_data = json_set(IFNULL(user_data, '{}'), '$.toWatch', 0) WHERE @selected ``` ``` # find all "toWatch" items in the current context bind f find json_extract(user_data, '$.toWatch') = 1 ``` more details on how it all works are in man page. but now we continue: * add `next-error` action * add `prev-error` action * add `convert-opml-to-feeds` scenario (argument for `-e`) * add `convert-feeds-to-opml` scenario (argument for `-e`) * add `database-batch-transactions` setting (#145) * add REGEXP operator to `item-rule` setting * report error when `item-rule` setting is invalid (#149) * make items counting respect applied `item-rule` setting (#149) * fallback to OSC 9 in `notification-command` setting (#153) * fallback to OSC 52 in `copy-to-clipboard-command` setting (#147) * rename `analyze-database-on-startup` setting to `database-analyze-on-startup` * rename `clean-database-on-startup` setting to `database-clean-on-startup` * clarify that only one specifier can be put per field in `item-content-format` (#184) * delete `yajl` dependency, use `json_tree()` from `sqlite` to parse json big shout out to package maintainers as always, you're the best guys ;) # newsraft 0.29 "san dian yi xian" from now on there's no global pager for status messages. status messages related to each feed will be saved for each feed individually. if feed has error messages, it will be painted in red, as will the sections containing it. to view errors of individual feed, you need to hover over it and press `view-errors` (`v` by default). if `view-errors` is invoked on section, it will show all errors of failed feeds in the given section. if some feed is failing too frequently and you don't want to see its errors, apply `suppress-errors` setting to it. default binding for `mark-read-all` action is changed to `A` key, because `^D` is now occupied by a very neat `select-next-page-half` action just like in vim. * add `suppress-errors` setting (#141) * add `menu-section-sorting` setting (#138) * add `menu-responsiveness` setting (#135) * add `color-list-feed-failed` setting * add `color-list-section-failed` setting * add `view-errors` action (`v` key) * add `sort-by-initial` action (`z` key) * add `select-next-page-half` action (`^D` key) * add `select-prev-page-half` action (`^U` key) * delete `status-history-menu` action * delete `status-messages-count-limit` setting * make `mark-item-read-on-hover` setting scalable glory to package maintainers! <3 # newsraft 0.28 "creeping train" this one brings a long awaited feature: feeds filtering via `item-rule` setting. i will go as far and just dump the setting description from the man page here: > Item search condition when accessing database. This can be very useful in > managing feeds with a heavy spam flow: you set a condition based on some > parameters and only those entries that meet this condition will be shown in > the feed. It's specified in SQL format. It probably only makes sense to set > this setting for individual feeds, and not globally (see *FEEDS FILE* section > to understand how). Available parameters: _guid_, _title_, _link_, _content_, > _attachments_, _persons_, _publication_date_, _update_date_. also color settings and notification command can be set for individual feeds now happy new year to everyone and especially to package maintainers :^) * add `item-rule` setting (#104) * add `download-max-host-connections` setting (#120) * add `sort-by-rowid` action (#123) * make `notification-command` setting scalable (#130) * make color settings scalable (#122) # newsraft 0.27 "confusing query" fixed erroneous logic of `item-limit` setting and split its special behavior into new `item-limit-unread` and `item-limit-important` settings. also now `[X]` and `{Y}` counters are no longer supported in the feeds file. `< reload-period X` should be used instead of `[X]` and `< item-limit Y` should be used instead of `{Y}` sorry to bother you with the second update in such a short time! * add `item-limit-unread` setting * add `item-limit-important` setting * drop support for bracketed [update timers] and {item limits} in feeds file (#94) # newsraft 0.26 "delicious tvorozhok" apart from other nice things, threading logic is changed completely to make use of caching for dns, connections, tls sessions, ca certs. now there's just 4 threads in the process and `update-threads-count` is gone. let the maintainers cook <3 * add tab characters rendering in plain text content (#109) * add -e option to execute certain actions without getting into the menus (#45) * add support for rdf-namespaced rss 1.0 feeds (#112) * add support for relative links in feed elements (#113) * add support for yyyy-mm-dd and yyyy/mm/dd dates in feeds * add support for style attributes within cells of html tables * make items menu regenerate upon returning from items menu obtained via `goto-feed` action * make date parsing less strict * report exit status of failed shell commands * fix storing http headers behavior according to rfc9111 (4.3.4) * fix difference in compiler flags between primary executable and test programs (#114) * delete `update-threads-count` setting * provide 2 woodpecker ci jobs for alpine and arch linuxes * provide a change log newsraft/doc/comparison-newsboat.md000066400000000000000000000131051516312403600177710ustar00rootroot00000000000000# Comparison of Newsraft and Newsboat Due to Newsraft's endeavor to be simpler than Newsboat, some design choices were made differently than in Newsboat. The main differences are listed below, so if you're considering switching from Newsboat to Newsraft, it's advised to examine them. ## TL;DR | Criterion | Newsraft | Newsboat | |:---------------------------------------------------|:---------------------|:-------------------------------------| | Parallel downloads | + | + | | Multiple actions key bindings | + | + | | Interactive content pager | + | + | | Built-in HTML renderer | + | + | | Sorting | + | + | | Automatic updates | + | + | | Item limits | + | + | | [Per-feed settings](#individual-feed-settings) | + | - | | Download manager | - | `podboat` | | Integration with third-party services | - | FreshRSS, Miniflux, Tiny Tiny RSS... | | [Feeds grouping](#sections-instead-of-query-feeds) | Section based | Query feed based | | Command feeds | `$(cmd arg1 arg2)` | `"exec:cmd arg1 arg2"` | | Scripting capabilities | `newsraft -e ACTION` | `newsboat -x ACTION` | | Programming languages used | C99 | C++17, Rust | | User interface libraries used | termbox2 | NCURSES, STFL | | [Source lines of code](#source-lines-of-code) | 9k + 3k (termbox2) | 45k | Feel free to submit an issue if you think that table above contains outdated information. ## Sections instead of query feeds Sections are needed to organize feeds in groups to be able to process them in bulk. They are like directories, but for feeds. You can update, explore and set auto updates for sections - this will all be applied to belonging feeds. This makes Newsraft very different from Newsboat as latter uses query feeds for that purpose which are based on the comprehensive filter language - it brings many possibilities, but also introduces some significant limitations (for example, query feeds [can't be reloaded](https://github.com/newsboat/newsboat/issues/978) and they have to be constantly populated which may be pretty slow for hundreds of feeds). ## Simpler command bindings Two lines below add the ability to open links in `mpv` to the feed reader (first line corresponds to Newsraft, second line corresponds to Newsboat). Newsboat requires you to first press the macro prefix key (`,` by default) to execute bound command, while Newsraft doesn't. ``` bind m exec mpv "%l" ``` ``` macro m set browser mpv; open-in-browser; set browser elinks ``` ## Individual feed settings Newsboat [doesn't support individual configuration for feeds](https://github.com/newsboat/newsboat/issues/83). Newsraft, on the other hand, supports many settings to be set on individual feeds and sections. For example, you can do something like this: ``` http://example.org/feed1.xml "Phonk" < reload-period 120 http://example.org/feed2.xml "Weather" < proxy socks5h://127.0.0.1:9050 @ News < reload-period 60 http://example.org/feed3.xml "World news" < reload-period 0; item-limit 50 http://example.org/feed4.xml "Tech news" < suppress-errors ``` ## Vim-like bindings by default You don't need to configure anything related to bindings if you are familiar with the Vim text editor. Also Newsraft borrows from Vim the ability to specify index for an entry on which you want to perform an action. So, for example, to open the 9th link in the browser, you need to press `9` followed by the key of the command to open the browser (`o` by default). ## Faster feed updates Newsraft uses streaming parsers to process feeds and parses feed elements with O(1) time complexity by using perfect hashing, while Newsboat uses DOM parsers and parses feed elements with O(n) time complexity. Newsboat takes object-oriented approach to processing feeds which can sometimes result in [a huge memory footprint](https://github.com/newsboat/newsboat/issues/977), while Newsraft represents feeds as simple structures of strings and numbers. ## Source lines of code This is how SLOC is calculated. As you can see, Newsraft is more than 3 times smaller than Newsboat: ``` ~/src/newsraft > git show -s --pretty=format:"%H %ad" 9df65bb858dbaf72e3a5e947ae1b40cbc27d42d4 Fri Jan 16 23:29:33 2026 +0300 ~/src/newsraft > find src -regex ".*\.\(c\|h\)" -exec awk NF {} + | wc -l 12596 ``` ``` ~/src/newsboat > git show -s --pretty=format:"%H %ad" 90fa5bc13bc43751a8e1463126bef7fc9649bfeb Thu Jan 15 23:32:30 2026 +0100 ~/src/newsboat > find src rust rss filter include newsboat.cpp podboat.cpp config.h -regex ".*\.\(cpp\|h\|rs\)" -exec awk NF {} + | wc -l 45132 ``` newsraft/doc/contributing-change.md000066400000000000000000000061331516312403600177340ustar00rootroot00000000000000# Introduction First of all, thank you for your desire to contribute some code! But before you start, there are a few things that need to be clarified. Since this project is designed to be very stable and maintainable over a long time, every decision will be annoyingly thought out and meticulously tested. So in order to not waste our precious time, make sure that the proposed functionality doesn't conflict with the goals of the project (see [doc/comparison-newsboat.md](https://codeberg.org/newsraft/newsraft/src/branch/main/doc/comparison-newsboat.md)) and its source code conforms to C99 language standard and project's code guidelines (see below). In terms of dependencies, Newsraft is very unpretentious - it uses [termbox2](https://github.com/termbox/termbox2) to draw user interface, [SQLite](https://www.sqlite.org) to store data and parse JSON, [curl](https://curl.se) to download feeds, [Expat](https://github.com/libexpat/libexpat) to parse XML and [Gumbo](https://github.com/google/gumbo-parser) to parse HTML. To build the project you will also need any C compiler that supports the C99 standard, any POSIX-compliant Make and, in case you want to generate a man page (of course you do), [scdoc](https://git.sr.ht/~sircmpwn/scdoc). If your contribution involves the introduction of a new dependency in the project, then there must be a very very very good reason for this. # Code guidelines ## Indentation characters One tab per one level of indentation. ## Naming convention ### Variables, functions, structures, etc Stick to the snake case (use underscores to separate words). ### Files Stick to the kebab case (use dashes to separate words). ### Directories Every directory in the `src` directory is named after the function that the files in that directory implement. ## Control statements Every control expression in the control statement must be followed by a curly brace to describe compound statement even if it has only one command. The first curly brace must be on the same line with the control expression, and the closing curly brace must be on a separate line. The exception is when the condition in the control expression is longer than 120 characters, then it needs to be continued in the next line and the first curly brace placed on its own line. For example: ``` for (size_t i = 0; i < items_count; ++i) { mark_item_read(i); } ``` ``` for (size_t i = 0, j = 1; (j < enormously_long_variable) \ && ((i < very_long_variable) || (i > another_very_long_variable)); ++i) { j += count_important_stuff(i); } ``` Note that it is forbidden to use assignment operator within conditions of `if`, `while` and `switch` statements. ## Function definitions Every function name in a function definition must start on a new line, and the curly braces that enclose the body of the function must be on their own lines. By the way, this makes it very easy to search for functions throughout the codebase with `grep -oR '^[a-z].*(.*)'` command. For example: ``` static void character_data_handler(void *userData, const XML_Char *s, int len) { struct stream_callback_data *data = userData; catas(data->value, s, len); } ``` newsraft/doc/contributing-report.md000066400000000000000000000016301516312403600200170ustar00rootroot00000000000000Before submitting your report, check if a similar issue has already been discussed [here](https://codeberg.org/newsraft/newsraft/issues). If a similar issue has already been reported, you do not need to create a new one. In order to make a good thorough report, please follow these simple steps: 1. Run the program with `-l mylog.txt` in the arguments and reproduce the bug you found. Please try not to do things that are not related to the bug so the log will be more readable and compact. 2. In your report describe what happened in detail, what you think should have happened and provide name and version of the operating system you are using. 3. If you are using some kind of network authentication, replace your passwords in the `mylog.txt` with fake ones. When you're done, [create issue](https://codeberg.org/newsraft/newsraft/issues/new) for your report with your explanation and attached `mylog.txt` file. newsraft/doc/examples/000077500000000000000000000000001516312403600152735ustar00rootroot00000000000000newsraft/doc/examples/config000066400000000000000000000012451516312403600164650ustar00rootroot00000000000000# This is an example of valid config file for Newsraft. # Note that config file is not required for Newsraft to work! # It's used only for overriding default settings and expanding functionality. # Lines with # character at the beginning are comments - they are ignored. # Here we set some settings. scrolloff 5 feeds-menu-paramount-explore true color-list-item-important bold red default # That's how you bind a regular action. bind ^P mark-unread-all # Command binding for opening links with feh image viewer. bind f exec feh "%l" # This will launch mpv in the background. bind m exec mpv --no-terminal "%l" & # This will launch mpv interactively. bind M exec mpv "%l" newsraft/doc/examples/feeds000066400000000000000000000035371516312403600163140ustar00rootroot00000000000000# This is an example of valid feeds file for Newsraft. # Lines with # character at the beginning are comments - they are ignored. # To subscribe to a feed, you have to add a line with its URL: http://example.org/feed1.xml # If you want to assign a custom name to the feed, # put a double quoted string after its URL: http://example.org/feed2.xml "Faculty notifications" # Lines with @ character at the beginning are section declarations, # they are used to organize your feed entries and process them in groups. # All feed entries after this declaration will belong to this section. @ News Reports http://example.org/feed3.xml "Local news" # This feed entry belongs to the "News Reports" section: http://example.org/feed4.xml "Global news" # Another section declaration will cause next feed entries to be added to the # corresponding section. Thus, the following feed entries will not belong to # the "News Reports" section, but to the "Blog Posts" section. @ Blog Posts http://example.org/feed5.xml "John Johnson" http://example.org/feed6.xml "Peter Peterson" # As you might have noticed, feeds http://example.org/feed1.xml and # http://example.org/feed2.xml don't belong to any section; but actually they # do - all feed entries are de facto included in the "Global" section (even # those that have been assigned to section by a section declaration). # Therefore, to continue adding feed entries without being included in previous # section, you must declare the beginning of the "Global" section: @ Global http://example.org/feed7.xml "Weather warnings" # You can subscribe to any feed you can get through a command. For # example, if you can get a Gemini atom feed with the program # 'gemget', then you might do this (note the silencing flags, so you # don't spook the terminal while newsraft is running). $(gemget -sq gemini://example.org/feed8.xml) "Reasons to use Gemini" newsraft/doc/license-curl.txt000066400000000000000000000021001516312403600165740ustar00rootroot00000000000000COPYRIGHT AND PERMISSION NOTICE Copyright (c) 1996 - 2022, Daniel Stenberg, , and many contributors, see the THANKS file. All rights reserved. Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization of the copyright holder. newsraft/doc/license-expat.txt000066400000000000000000000021701516312403600167570ustar00rootroot00000000000000Copyright (c) 1998-2000 Thai Open Source Software Center Ltd and Clark Cooper Copyright (c) 2001-2019 Expat maintainers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. newsraft/doc/license-gumbo.txt000066400000000000000000000261351516312403600167560ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. newsraft/doc/license-scdoc.txt000066400000000000000000000020471516312403600167340ustar00rootroot00000000000000Copyright © 2017 Drew DeVault Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. newsraft/doc/license-sqlite.txt000066400000000000000000000004071516312403600171400ustar00rootroot00000000000000The author disclaims copyright to this source code. In place of a legal notice, here is a blessing: * May you do good and not evil. * May you find forgiveness for yourself and forgive others. * May you share freely, never taking more than you give. newsraft/doc/license-termbox2.txt000066400000000000000000000021661516312403600174050ustar00rootroot00000000000000MIT License Copyright (c) 2010-2020 nsf 2015-2025 Adam Saponara Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. newsraft/doc/license.txt000066400000000000000000000022251516312403600156410ustar00rootroot00000000000000Copyright 2021-2026 Grigory Kirillov Copyright 2023-2026 Newsraft contributors (see git log) Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. The project as it is right now would not be possible without the use of such wonderful pieces of software as curl, expat, gumbo, scdoc, sqlite and termbox2. The licenses under which these projects are distributed are provided in files license-curl.txt, license-expat.txt, license-gumbo.txt, license-scdoc.txt, license-sqlite.txt and license-termbox2.txt respectively. newsraft/doc/making-a-release.md000066400000000000000000000003111516312403600170740ustar00rootroot00000000000000Package maintainers heavily rely on the format of the names for a release, so releases must always comply with these rules: Release commit tag name = newsraft-VERSION Release title = newsraft-VERSION newsraft/doc/newsraft.1000066400000000000000000000737161516312403600154060ustar00rootroot00000000000000.\" Generated by scdoc 1.11.3 .\" Complete documentation for this program is not available as a GNU info page .ie \n(.g .ds Aq \(aq .el .ds Aq ' .nh .ad l .\" Begin generated content: .TH "NEWSRAFT" "1" "2025-10-04" .PP .SH NAME .PP newsraft - feed reader for terminal .PP .SH SYNOPSIS .PP \fBnewsraft\fR [\fB-f\fR \fIFILE1\fR] [\fB-c\fR \fIFILE2\fR] [\fB-d\fR \fIFILE3\fR] [\fB-l\fR \fIFILE4\fR] [\fB-e\fR \fIACTION\fR] [\fB-v\fR] [\fB-h\fR] .PP .SH DESCRIPTION .PP Newsraft is a small text based program for reading syndication feeds.\& It obtains content from a given set of sources and lets you browse it all via one streamlined user interface.\& .PP .SH OPTIONS .PP \fB-f\fR \fIFILE\fR .RS 4 Force \fBfeeds\fR file to \fIFILE\fR.\& .PP .RE \fB-c\fR \fIFILE\fR .RS 4 Force \fBconfig\fR file to \fIFILE\fR.\& .PP .RE \fB-d\fR \fIFILE\fR .RS 4 Force \fBdatabase\fR file to \fIFILE\fR.\& .PP .RE \fB-l\fR \fIFILE\fR .RS 4 Write logs to \fIFILE\fR.\& .PP .RE \fB-e\fR \fIACTION\fR .RS 4 Execute one-time \fIACTION\fR from the following list: .RS 4 \fBconvert-opml-to-feeds\fR (takes OPML from standard input) .br \fBconvert-feeds-to-opml\fR (takes feeds from \fBfeeds\fR file) .br \fBreload-all\fR .br \fBprint-unread-items-count\fR .br \fBpurge-abandoned\fR .PP .RE .RE \fB-v\fR .RS 4 Print version information.\& .PP .RE \fB-h\fR .RS 4 Print usage information.\& .PP .RE .SH STARTER GUIDE .PP To start using Newsraft you have to create a \fBfeeds\fR file with the list of links to feeds you want to receive news from.\& Check out \fBFEEDS FILE\fR section for file syntax and valid paths.\& .PP When \fBfeeds\fR file is ready, you can launch Newsraft.\& There are only 4 menus you will have to deal with: \fIsections\fR, \fIfeeds\fR, \fIitems\fR and \fIpager\fR.\& Default binds are listed in \fBACTIONS\fR section.\& .PP \fISections\fR menu consists of section entries which are needed to organize feeds in groups to be able to process them in bulk.\& They are kind of directories for feeds.\& If you didn'\&t specify any section declarations in your \fBfeeds\fR file then you will get to the \fIfeeds\fR menu straightaway.\& .PP \fIFeeds\fR menu consists of feed entries.\& Every feed entry contains news downloaded from one specific source which you have set in \fBfeeds\fR file.\& To update a single feed you have to select it and press \fBr\fR or \fBR\fR if you want to update all feeds.\& From \fIfeeds\fR menu you can get to the \fIitems\fR menu by entering some feed.\& .PP \fIItems\fR menu consists of feed item entries (i.\& e.\& single pieces of news) which you get when you update feeds in the previous menu.\& Every feed item entry has two switchable properties - read state and importance state.\& Keys to change read state: \fBd\fR to mark read, \fBD\fR to mark unread, \fB^D\fR to mark everything read.\& Keys to change importance state: \fBf\fR to flag important, \fBF\fR to flag unimportant.\& To view item'\&s content you have to go to \fIpager\fR menu by entering selected item.\& .PP \fIPager\fR menu will display some details about selected item and render its content if it was provided by feed.\& Usually feed item entries have a links section with one link pointing to a related web page and several links that were mentioned in the item'\&s content.\& You can copy these links into your clipboard with \fBy\fR key and open them in your web browser with \fBo\fR key.\& To target a key action to link with a specific index you have to prefix your key with this index.\& For example, \fB5y\fR will copy fifth link and \fB17o\fR will open seventeenth link in the web browser.\& You can also setup custom command bindings to execute any commands with these links.\& Consider this \fBconfig\fR file: .PP .RS 4 \fIbind m exec mpv "%l"\fR .br \fIbind f exec feh "%l"\fR .PP .RE With this you will be able to open any link in \fBmpv\fR(1) and \fBfeh\fR(1) directly from your terminal!\& Isn'\&t it awesome?\& It is freaking amazing!\& .PP For both \fIsections\fR menu and \fIfeeds\fR menu there is a special explore mode.\& You can toggle it by pressing the \fBtab\fR key.\& It'\&s truly miraculous: it reveals all the news in the current context (combines news from all feeds of the current menu into one list).\& This mode may come in handy when you want to quickly scroll through all the news without switching between sections and feeds back and forth.\& .PP And for a dessert, I'\&ll tell you about the search functionality.\& You can type \fB/\fR to begin search input - enter the desired query here and press \fBEnter\fR.\& This will open up an \fIitems\fR menu with all the items matching your query in the current context.\& .PP .SH CONFIGURATION .PP .SS FEEDS FILE .PP This file contains feed entries that Newsraft will display and process.\& There are 4 types of lines in \fBfeeds\fR file.\& .PP Comment lines start with \fI#\fR character.\& These lines are completely ignored.\& For example: .PP .RS 4 \fI# Look closely.\& The beautiful may be small.\&\fR .PP .RE Feed lines start with a URL.\& After at least one whitespace character, the name of the feed may be specified - it must be enclosed in double quotes.\& For example: .PP .RS 4 \fIhttps://example.\&org/feed.\&xml "Lorem Ipsum Blog"\fR .PP .RE Generator lines start with a command enclosed in \fB$()\fR.\& These act just like feed lines but instead of fetching resources from a remote server they use the output of the specified command to obtain the content.\& .PP .RS 4 \fI$(cat ~/local-feed.\&xml) "Lorem Ipsum Blog"\fR .PP .RE Section lines start with \fB@\fR character.\& After any number of whitespace characters, the name of the section must be specified.\& For example: .PP .RS 4 \fI@ Software Releases\fR .PP .RE Both feed and section lines allow you to set individual settings and binds for them.\& The syntax is as follows: .PP .RS 4 \fI@ Lorem Ipsum < reload-period 1440\fR .br \fIhttp://example.\&org/feed1.\&xml "Dolor Sit" < reload-period 60; item-limit 500\fR .br \fIhttp://example.\&org/feed2.\&xml "Id Est" < bind b mark-read; exec book.\&sh "%l"\fR .PP .RE Settings set for feeds take precedence over the settings specified for sections.\& Not every setting supports individual assignment - only settings with asterisk \fB(*)\fR on them do (see \fBSETTINGS\fR section).\& .PP Search precedence: .PD 0 .IP 1. 4 \fI$XDG_CONFIG_HOME\fR/newsraft/feeds .IP 2. 4 \fI$HOME\fR/.\&config/newsraft/feeds .IP 3. 4 \fI$HOME\fR/.\&newsraft/feeds .PD .PP .SS CONFIG FILE .PP This file is used to override default settings and bindings of Newsraft.\& Presence of \fBconfig\fR file is totally optional and Newsraft will work without it just fine.\& There are 3 types of lines in \fBconfig\fR file.\& .PP Comment lines start with \fI#\fR character.\& These lines are completely ignored.\& For example: .PP .RS 4 \fI# Good design is as little design as possible.\&\fR .PP .RE Setting lines start with a setting name and end with a setting value.\& Available settings are listed in the \fBSETTINGS\fR and \fBCOLOR SETTINGS\fR sections.\& Here are a couple of examples: .PP .RS 4 \fBscrolloff\fR \fI5000\fR .br \fBlist-entry-date-format\fR \fI"%D"\fR .br \fBfeeds-menu-paramount-explore\fR \fItrue\fR .PP .RE Binding lines start with the \fBbind\fR word.\& They define actions that are performed when certain keys are pressed.\& Complete list of available actions can be found in the \fBACTIONS\fR section.\& Here'\&s an example: .PP .RS 4 \fBbind\fR r \fIreload-all\fR .PP .RE The special \fIexec\fR action makes it possible to run shell commands when the bound key is pressed.\& Specifiers within the command are replaced with values corresponding to the currently selected entry as per \fBmenu-item-entry-format\fR: .PP .RS 4 \fBbind\fR m \fIexec\fR setsid mpv --terminal=no "%l" & .PP .RE The \fIedit\fR action runs a specified SQL query on the \fBdatabase\fR, so please be careful!\& \fI@selected\fR specifier is replaced with a condition which identifies the currently selected entry - make sure to include it if you want to target individual item/feed rather than the whole database: .PP .RS 4 \fBbind\fR w \fIedit\fR UPDATE items SET user_data = json_set(IFNULL(user_data, '\&{}'\&), '\&$.\&toWatch'\&, 1) WHERE \fI@selected\fR .br \fBbind\fR W \fIedit\fR UPDATE items SET user_data = json_set(IFNULL(user_data, '\&{}'\&), '\&$.\&toWatch'\&, 0) WHERE \fI@selected\fR .PP .RE Use the \fIfind\fR action to retrieve items based on a specified SQL condition in the current context.\& For instance, to search for items marked as toWatch (as shown in the previous example), one can use the bindings like the following: .PP .RS 4 \fBbind\fR f \fIfind\fR user_data LIKE '\&%"toWatch":1%'\& .br \fBbind\fR f \fIfind\fR json_extract(user_data, '\&$.\&toWatch'\&) = 1 .PP .RE Binding lines can fit multiple actions in a single key.\& These actions will be executed in the order in which they are specified.\& Assigned actions must be separated with semicolon (;) characters, for example: .PP .RS 4 \fBbind\fR key \fIaction\fR; \fIexec\fR command; \fIaction\fR; \fIedit\fR query; \fIaction\fR .PP .RE In case you want to disable some binding which was set in Newsraft by default, you can use a line according to this format: .PP .RS 4 \fBunbind\fR key .PP .RE Search precedence: .PD 0 .IP 1. 4 \fI$XDG_CONFIG_HOME\fR/newsraft/config .IP 2. 4 \fI$HOME\fR/.\&config/newsraft/config .IP 3. 4 \fI$HOME\fR/.\&newsraft/config .PD .PP .SS DATABASE FILE .PP This file stores everything you download from feeds in \fBsqlite3\fR(1) format.\& Although you now know the format in which the data is stored, it is highly recommended to avoid modifying the database manually - things will break and it will be very sad.\& .PP Search precedence: .PD 0 .IP 1. 4 \fI$XDG_DATA_HOME\fR/newsraft/newsraft.\&sqlite3 .IP 2. 4 \fI$HOME\fR/.\&local/share/newsraft/newsraft.\&sqlite3 .IP 3. 4 \fI$HOME\fR/.\&newsraft/newsraft.\&sqlite3 .PD .PP .SH SETTINGS .PP Settings with asterisk \fB(*)\fR on them can be set for individual feeds and sections.\& .PP .SS reload-period (*) Default: \fI0\fR.\& Feed auto reload period in minutes.\& If set to \fI0\fR, no auto reloads will be run.\& .PP .SS suppress-errors (*) Default: \fIfalse\fR.\& If \fItrue\fR, feed update error indication in the menu will be disabled.\& It'\&s recommended to set this setting only for specific feeds that are expected to fail frequently and you are tired of seeing their errors.\& .PP .SS item-rule (*) Default: \fI""\fR.\& Item search condition when accessing database.\& This can be very useful in managing feeds with a heavy spam flow: you set a condition based on some parameters and only those entries that meet this condition will be shown in the feed.\& It'\&s specified in SQL format.\& It probably only makes sense to set this setting for individual feeds, and not globally (see \fBFEEDS FILE\fR section to understand how).\& .PP Available parameters: .TS l l lx l l l l l l l l l l l l l l l l l l l l l l l l l l l. T{ \fIguid\fR T} T{ (string) T} T{ Globally unique identifier of the article which is expected to be unique within the originating feed T} T{ \fItitle\fR T} T{ (string) T} T{ Title of the article T} T{ \fIlink\fR T} T{ (string) T} T{ URL of the article T} T{ \fIcontent\fR T} T{ (string) T} T{ Content of the article which is stored exactly as it appears in the feed, with all original HTML preserved T} T{ \fIattachments\fR T} T{ (string) T} T{ Serialized string of all attachments with URL links, MIME types and byte sizes T} T{ \fIpersons\fR T} T{ (string) T} T{ Serialized string of all people related to the article with names and email addresses T} T{ \fIdownload_date\fR T} T{ (integer) T} T{ Timestamp of download date in seconds since 1970 T} T{ \fIpublication_date\fR T} T{ (integer) T} T{ Timestamp of publication date in seconds since 1970 T} T{ \fIupdate_date\fR T} T{ (integer) T} T{ Timestamp of update date in seconds since 1970 T} T{ \fIuser_data\fR T} T{ (string) T} T{ User-defined data for the item.\& It can be modified using the \fBedit\fR action (see \fBCONFIG FILE\fR section) T} .TE .sp 1 Here are some examples of correct setting values: .PP .RS 4 \fItitle NOT LIKE '\&%Rust%'\&\fR .br \fIpersons LIKE '\&%PHARMACIST%'\& OR persons LIKE '\&%OFFL1NX%'\&\fR .br \fIattachments LIKE '\&%audio/mp3%'\& OR attachments LIKE '\&%video/mp4%'\&\fR .br \fICAST(strftime('\&%Y'\&, publication_date, '\&unixepoch'\&) AS INTEGER) >= 2024\fR .br \fINOT (title REGEXP '\&.\&+:.\&+\e".\&+\e".\&+by'\& OR title LIKE '\&%(ultra slowed)%'\& OR title LIKE '\&%(1 hour version)%'\&)\fR .PP .RE REGEXP operator uses POSIX Extended Regular Expression syntax, see \fBregex\fR(7).\& .PP .SS item-limit (*) Default: \fI0\fR.\& Maximum number of items stored in a feed.\& If set to \fI0\fR, no limit will be set.\& .PP .SS item-limit-unread (*) Default: \fItrue\fR.\& If \fItrue\fR, \fBitem-limit\fR setting will also cap unread items.\& .PP .SS item-limit-important (*) Default: \fIfalse\fR.\& If \fItrue\fR, \fBitem-limit\fR setting will also cap important items.\& .PP .SS scrolloff Default: \fI0\fR.\& Minimal number of list menu entries to keep above and below the selected entry.\& If you set it to a very large value the selected entry will always be in the middle of the list menu (except for start and end of the list menu).\& .PP .SS scrollwrap Default: \fIfalse\fR.\& If \fItrue\fR, moving down while on the last item in a list will wrap around to the top and vice versa.\& .PP .SS pager-width (*) Default: \fI100\fR.\& Pager width in characters.\& If set to \fI0\fR, the pager will take up all available space.\& .PP .SS pager-centering (*) Default: \fItrue\fR.\& If \fItrue\fR and \fBpager-width\fR is not \fI0\fR, pager will center its content horizontally.\& .PP .SS menu-item-sorting Default: \fItime-desc\fR.\& Sorting order for the items menu.\& Available values: \fItime-desc\fR, \fItime-asc\fR, \fItime-download-desc\fR, \fItime-download-asc\fR, \fItime-publication-desc\fR, \fItime-publication-asc\fR, \fItime-update-desc\fR, \fItime-update-asc\fR, \fIrowid-desc\fR, \fIrowid-asc\fR, \fIunread-desc\fR, \fIunread-asc\fR, \fIimportant-desc\fR, \fIimportant-asc\fR, \fIalphabet-desc\fR, \fIalphabet-asc\fR.\& .PP .SS menu-feed-sorting Default: \fInone\fR.\& Sorting order for the feeds menu.\& Available values: \fIunread-desc\fR, \fIunread-asc\fR, \fIalphabet-desc\fR, \fIalphabet-asc\fR.\& .PP .SS menu-section-sorting Default: \fInone\fR.\& Sorting order for the sections menu.\& Available values: \fIunread-desc\fR, \fIunread-asc\fR, \fIalphabet-desc\fR, \fIalphabet-asc\fR.\& .PP .SS menu-responsiveness Default: \fItrue\fR.\& If \fItrue\fR, update menu contents as soon as possible.\& If \fIfalse\fR, the menu will be updated only when you re-open it.\& .PP .SS open-in-browser-command (*) Default: \fIauto\fR.\& Shell command for \fBopen-in-browser\fR action.\& If set to \fIauto\fR, most operating systems will get \fI${BROWSER:-xdg-open} "%l"\fR while macOS users have it set to \fIopen "%l"\fR.\& .PP .SS copy-to-clipboard-command Default: \fIauto\fR.\& Shell command for copying text to clipboard.\& All copied data is piped into the standard input of the specified command.\& If set to \fIauto\fR, Newsraft will determine the appropriate command based on the user environment: .PP .PD 0 .IP \(bu 4 \fI"wl-copy"\fR if environment variable WAYLAND_DISPLAY is set .IP \(bu 4 \fI"xclip -selection clipboard"\fR if environment variable DISPLAY is set .IP \(bu 4 \fI"pbcopy"\fR if Newsraft was built for macOS operating system .IP \(bu 4 \fI"newsraft-osc-52"\fR otherwise .PD .PP \fI"newsraft-osc-52"\fR is a special value that doesn'\&t run an external command but instead triggers the OSC 52 escape sequence, instructing the terminal to copy data directly to the system clipboard.\& This behavior is especially useful when running Newsraft over \fBssh\fR(1), as it allows clipboard operations to affect the local system rather than the remote one.\& .PP .SS notification-command (*) Default: \fIauto\fR.\& Shell command for invoking system notifications about new news received.\& If set to \fIauto\fR, Newsraft will determine the appropriate command based on the user environment: .PP .PD 0 .IP \(bu 4 \fI"notify-send '\&Newsraft brought %q news!\&'\&"\fR if WAYLAND_DISPLAY or DISPLAY is set in environment .IP \(bu 4 \fI"osascript -e '\&display notification "Newsraft brought %q news!\&"'\&"\fR if Newsraft was built for macOS operating system .IP \(bu 4 \fI"printf '\&\ee]9;Newsraft brought %q news!\&\ea'\&"\fR (OSC 9 escape sequence) otherwise .PD .PP .SS proxy (*) Default: \fI""\fR.\& Sets the proxy to use for the network requests.\& It must be either a hostname or dotted numerical IPv4 address.\& To specify IPv6 address you have to enclose it within square brackets.\& Port number can be set by appending :PORT to the end of setting value.\& By default proxy protocol is considered HTTP, but you can set a different one by prepending SCHEME:// to the setting value.\& .PP .SS proxy-user (*) Default: \fI""\fR.\& User for authentication with the proxy server.\& .PP .SS proxy-password (*) Default: \fI""\fR.\& Password for authentication with the proxy server.\& .PP .SS global-section-name Default: \fIGlobal\fR.\& Name of the section that contains all feeds.\& .PP .SS global-section-hide Default: \fIfalse\fR.\& If \fItrue\fR, global section will not be shown.\& .PP .SS status-show-menu-path Default: \fItrue\fR.\& If \fItrue\fR, print menu path in the status bar.\& .PP .SS status-placeholder Default: \fIr:reload R:reload-all tab:explore d:read D:unread f:important F:unimportant n:next-unread N:prev-unread p:next-important P:prev-important\fR.\& .PP Placeholder which is put in the status bar if it'\&s empty.\& .PP .SS item-content-format (*) Default: \fIFeed:  %f
|Title: %t
|Date:  %d
|
%c
|

%L\fR.\& .PP Sets the HTML format according to which the item'\&s content will be generated.\& Fields are separated by \fI|\fR character and ONLY one specifier can be placed in each field.\& If an item doesn'\&t have a value corresponding to the specifier in the field, then the entire field will not be shown.\& Specifiers are as follows: .PP .RS 4 \fIf\fR feed title if set, feed link otherwise; .br \fIt\fR item title; .br \fIl\fR item link; .br \fId\fR item date; .br \fIa\fR item authors; .br \fIc\fR item content; .br \fIL\fR item links list.\& .PP .RE .SS item-content-date-format (*) Default: \fI%a, %d %b %Y %H:%M:%S %z\fR.\& Date format in the item'\&s content.\& Specifier values correspond to the \fBstrftime\fR(3) format.\& .PP .SS item-content-link-format (*) Default: \fI[%i]: %l
\fR.\& Link format in the links list of item'\&s content.\& \fI%i\fR and \fI%l\fR will be replaced by link index and link address respectively.\& .PP .SS list-entry-date-format Default: \fI%b %d\fR.\& Date format of the list entries.\& Specifier values correspond to the \fBstrftime\fR(3) format.\& .PP .SS menu-section-entry-format Default: \fI%5.\&0u @ %t\fR.\& Format of the section list entries.\& Specifiers are as follows: .PP .RS 4 \fIi\fR index number; .br \fIu\fR unread items count; .br \fIt\fR section title.\& .PP .RE .SS menu-feed-entry-format Default: \fI%5.\&0u │ %t\fR.\& Format of the feed list entries.\& Specifiers are as follows: .PP .RS 4 \fIi\fR index number; .br \fIu\fR unread items count; .br \fIn\fR total items count; .br \fIl\fR feed link; .br \fIt\fR feed name if set, feed link otherwise.\& .PP .RE .SS menu-item-entry-format Default: \fI" %u │ %d │ %o"\fR.\& Format of the item list entries.\& Specifiers are as follows: .PP .RS 4 \fIi\fR index number; .br \fIu\fR "N" if item is unread, " " otherwise; .br \fId\fR update date formatted according to \fBlist-entry-date-format\fR; .br \fID\fR publication date formatted according to \fBlist-entry-date-format\fR; .br \fIl\fR item link; .br \fIt\fR item title; .br \fIo\fR item title if set, item link otherwise; .br \fIL\fR feed link; .br \fIT\fR feed title; .br \fIO\fR feed title if set, feed link otherwise.\& .PP .RE .SS menu-explore-item-entry-format Default: \fI" %u │ %d │ %-28O │ %o"\fR.\& Format of the item list entries in explore mode.\& Specifiers are the same as in \fBmenu-item-entry-format\fR.\& .PP .SS sections-menu-paramount-explore Default: \fIfalse\fR.\& Enables explore mode in sections menu by default.\& .PP .SS feeds-menu-paramount-explore Default: \fIfalse\fR.\& Enables explore mode in feeds menu by default.\& .PP .SS read-on-arrival (*) Default: \fIfalse\fR.\& Mark new items as read automatically.\& .PP .SS mark-item-unread-on-change (*) Default: \fIfalse\fR.\& Mark every item that changes on a feed update as unread.\& .PP .SS mark-item-read-on-hover (*) Default: \fIfalse\fR.\& Mark every item that gets selected as read.\& .PP .SS database-batch-transactions Default: \fItrue\fR.\& Apply all changes to the \fBdatabase\fR file in one big transaction after all feed updates have finished instead of using a separate transaction for each feed update.\& This improves update performance a lot with the downside that fetched content will not be saved if you quit Newsraft before update finishes.\& .PP .SS database-analyze-on-startup Default: \fItrue\fR.\& Run "ANALYZE" SQLite command on the database every time you start Newsraft.\& It gathers statistics about database and uses it to optimize some queries making runtime faster.\& .PP .SS database-clean-on-startup Default: \fIfalse\fR.\& Run "VACUUM" SQLite command on the database every time you start Newsraft.\& It rebuilds the database file by packing it into a minimal amount of disk space.\& This can significantly increase startup time.\& .PP .SS download-timeout (*) Default: \fI20\fR.\& Maximum time in seconds that you allow Newsraft to download one feed.\& Setting to \fI0\fR disables the timeout.\& .PP .SS download-speed-limit (*) Default: \fI0\fR.\& Maximum download speed in kilobytes per second (kB/s).\& Setting to \fI0\fR disables the limit.\& .PP .SS download-max-connections Default: \fI500\fR.\& Maximum amount of simultaneously open connections Newsraft may hold in total.\& If set to \fI0\fR, there is no limit.\& You can try to increase this value or even set it to \fI0\fR if you want to squeeze out all performance to the last drop but be aware that things can start to break at high setting values.\& One obvious example is getaddrinfo() starts to choke with a large number of simultaneous requests trying to resolve domain names.\& .PP .SS download-max-host-connections Default: \fI0\fR.\& Maximum amount of simultaneously open connections Newsraft may hold a single host.\& If set to \fI0\fR, there is no limit.\& .PP .SS user-agent (*) Default: \fIauto\fR.\& User-Agent header sent with download requests.\& If set to \fIauto\fR, Newsraft will generate it according to the following format: .PP .RS 4 \fI"newsraft/"\fR + NEWSRAFT_VERSION + \fI" ("\fR + OS_NAME + \fI")"\fR .PP .RE OS_NAME shouldn'\&t be a matter of privacy concern, because on most systems it contains nothing more like \fI"Linux"\fR or \fI"Darwin"\fR.\& If you want to be sure of this, check Newsraft log to see how \fBuser-agent\fR is set at startup.\& .PP If set to \fI""\fR, User-Agent header will not be sent.\& .PP .SS respect-ttl-element (*) Default: \fItrue\fR.\& Prevents too frequent updates for some feeds.\& The limit is set by the creators of the feeds in order to save traffic and resources for a very rarely updated feeds.\& Disabling it is strongly discouraged.\& .PP .SS respect-expires-header (*) Default: \fItrue\fR.\& Prevents feed updates until the expiration date of the previously downloaded information in order to save traffic and resources.\& Disabling it is strongly discouraged.\& .PP .SS send-if-none-match-header (*) Default: \fItrue\fR.\& Sends an entity tag corresponding to the previously downloaded information.\& If the server from which the feed is downloaded contains information with the same tag, then in order to save traffic and resources, it will reject the download request.\& Disabling it is strongly discouraged.\& .PP .SS send-if-modified-since-header (*) Default: \fItrue\fR.\& Sends a date corresponding to the last modification of previously downloaded information.\& If the server from which the feed is downloaded contains information with the same modification date, then in order to save traffic and resources, it will reject the download request.\& Disabling it is strongly discouraged.\& .PP .SS ignore-no-color Default: \fIfalse\fR.\& If \fItrue\fR, Newsraft will use colors regardless of whether \fBNO_COLOR\fR environment variable is present or not.\& .PP .SH COLOR SETTINGS .PP Color settings are the same settings as above, but they take two color words (foreground and background) and optional attribute words.\& Available colors are \fIdefault\fR, \fIblack\fR, \fIred\fR, \fIgreen\fR, \fIyellow\fR, \fIblue\fR, \fImagenta\fR, \fIcyan\fR, \fIwhite\fR and \fIcolorN\fR (\fIN\fR can be a number from \fI0\fR to \fI255\fR).\& Available attributes are \fIbold\fR, \fIitalic\fR and \fIunderlined\fR.\& .PP .TS l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l. T{ Color setting T} T{ Default value T} T{ \fBcolor-status\fR T} T{ \fIgreen default bold\fR T} T{ \fBcolor-status-info\fR T} T{ \fIcyan default bold\fR T} T{ \fBcolor-status-fail\fR T} T{ \fIred default bold\fR T} T{ \fBcolor-list-item\fR T} T{ \fIdefault default\fR T} T{ \fBcolor-list-item-selected\fR T} T{ (not set) T} T{ \fBcolor-list-item-unread\fR T} T{ \fIyellow default\fR T} T{ \fBcolor-list-item-important\fR T} T{ \fImagenta default\fR T} T{ \fBcolor-list-feed\fR T} T{ \fIdefault default\fR T} T{ \fBcolor-list-feed-selected\fR T} T{ (not set) T} T{ \fBcolor-list-feed-unread\fR T} T{ \fIyellow default\fR T} T{ \fBcolor-list-feed-failed\fR T} T{ \fIred default\fR T} T{ \fBcolor-list-section\fR T} T{ \fIdefault default\fR T} T{ \fBcolor-list-section-selected\fR T} T{ (not set) T} T{ \fBcolor-list-section-unread\fR T} T{ \fIyellow default\fR T} T{ \fBcolor-list-section-failed\fR T} T{ \fIred default\fR T} .TE .sp 1 .SH ACTIONS .PP .TS l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l. T{ Actions T} T{ Keys T} T{ \fBselect-next\fR T} T{ \fIj\fR, \fIKEY_DOWN\fR, \fI^E\fR T} T{ \fBselect-prev\fR T} T{ \fIk\fR, \fIKEY_UP\fR, \fI^Y\fR T} T{ \fBselect-next-page\fR T} T{ \fIspace\fR, \fI^F\fR, \fIKEY_NPAGE\fR T} T{ \fBselect-next-page-half\fR T} T{ \fI^D\fR T} T{ \fBselect-prev-page\fR T} T{ \fI^B\fR, \fIKEY_PPAGE\fR T} T{ \fBselect-prev-page-half\fR T} T{ \fI^U\fR T} T{ \fBselect-first\fR T} T{ \fIg\fR, \fIKEY_HOME\fR T} T{ \fBselect-last\fR T} T{ \fIG\fR, \fIKEY_END\fR T} T{ \fBjump-to-next\fR T} T{ \fIJ\fR T} T{ \fBjump-to-prev\fR T} T{ \fIK\fR T} T{ \fBjump-to-next-unread\fR T} T{ \fIn\fR T} T{ \fBjump-to-prev-unread\fR T} T{ \fIN\fR T} T{ \fBjump-to-next-important\fR T} T{ \fIp\fR T} T{ \fBjump-to-prev-important\fR T} T{ \fIP\fR T} T{ \fBnext-error\fR T} T{ \fIe\fR T} T{ \fBprev-error\fR T} T{ \fIE\fR T} T{ \fBgoto-feed\fR T} T{ \fI*\fR T} T{ \fBshift-west\fR T} T{ \fI,\fR T} T{ \fBshift-east\fR T} T{ \fI.\&\fR T} T{ \fBshift-reset\fR T} T{ \fI<\fR T} T{ \fBsort-by-time\fR T} T{ \fIt\fR T} T{ \fBsort-by-time-download\fR T} T{ (not set) T} T{ \fBsort-by-time-publication\fR T} T{ (not set) T} T{ \fBsort-by-time-update\fR T} T{ (not set) T} T{ \fBsort-by-rowid\fR T} T{ \fIw\fR T} T{ \fBsort-by-unread\fR T} T{ \fIu\fR T} T{ \fBsort-by-initial\fR T} T{ \fIz\fR T} T{ \fBsort-by-alphabet\fR T} T{ \fIa\fR T} T{ \fBsort-by-important\fR T} T{ \fIi\fR T} T{ \fBenter\fR T} T{ \fIenter\fR, \fIl\fR, \fIKEY_ENTER\fR, \fIKEY_RIGHT\fR T} T{ \fBreload\fR T} T{ \fIr\fR T} T{ \fBreload-all\fR T} T{ \fIR\fR, \fI^R\fR T} T{ \fBmark-read\fR; \fBjump-to-next\fR T} T{ \fId\fR T} T{ \fBmark-unread\fR; \fBjump-to-next\fR T} T{ \fID\fR T} T{ \fBmark-read-all\fR T} T{ \fIA\fR T} T{ \fBmark-unread-all\fR T} T{ (not set) T} T{ \fBmark-important\fR T} T{ \fIf\fR T} T{ \fBmark-unimportant\fR T} T{ \fIF\fR T} T{ \fBtoggle-read\fR T} T{ (not set) T} T{ \fBtoggle-important\fR T} T{ (not set) T} T{ \fBtoggle-explore-mode\fR T} T{ \fItab\fR T} T{ \fBview-errors\fR T} T{ \fIv\fR T} T{ \fBopen-in-browser\fR T} T{ \fIo\fR T} T{ \fBcopy-to-clipboard\fR T} T{ \fIy\fR, \fIc\fR T} T{ \fBstart-search-input\fR T} T{ \fI/\fR T} T{ \fBclean-status\fR T} T{ \fI`\fR T} T{ \fBnavigate-back\fR T} T{ \fIh\fR, \fIbackspace\fR, \fIKEY_LEFT\fR, \fIKEY_BACKSPACE\fR T} T{ \fBquit\fR T} T{ \fIq\fR T} T{ \fBquit-hard\fR T} T{ \fIQ\fR T} T{ \fBexec man newsraft\fR T} T{ \fI?\&\fR T} .TE .sp 1 You can also bind \fIescape\fR key, but this can potentially lead to incomplete processing of escape sequences in the terminal on a very fast input.\& This is due to the special role of the \fIescape\fR character under the hood of terminals.\& Your mileage may vary.\& .PP .SH FORMATS SUPPORT .PP Data formats of feeds which Newsraft recognizes.\& Not the whole functionality of these formats is implemented, but only the functionality that is most likely to carry the most essential information.\& .PP \fIRSS 2.\&0\fR, \fI1.\&1\fR, \fI1.\&0\fR, \fI0.\&94\fR, \fI0.\&93\fR, \fI0.\&92\fR, \fI0.\&91\fR, \fI0.\&9\fR .br \fIAtom 1.\&0\fR .br \fIRSS Content Module\fR .br \fIMedia RSS\fR .br \fIDublinCore 1.\&1 Elements\fR .br \fIJSON Feed\fR .PP .SH ENVIRONMENT .PP Newsraft'\&s behavior depends on the environment variables set, however not all environment variables affect Newsraft directly - many environment variables affect libraries that Newsraft is built upon.\& For example, \fBlibcurl\fR(3) recognizes a large number of different environment variables which you can learn more about on \fBlibcurl-env\fR(3).\& .PP .TS l lx l l l l l l l l l l l l. T{ \fBXDG_CONFIG_HOME\fR T} T{ Directory in which user-specific configuration files are stored.\& T} T{ \fBXDG_DATA_HOME\fR T} T{ Directory in which user-specific data files are stored.\& T} T{ \fBHOME\fR T} T{ User home directory.\& T} T{ \fBBROWSER\fR T} T{ User web browser.\& T} T{ \fBWAYLAND_DISPLAY\fR T} T{ Identifier of the Wayland graphics display.\& T} T{ \fBDISPLAY\fR T} T{ Identifier of the X graphics display.\& T} T{ \fBNO_COLOR\fR T} T{ Makes the interface monochrome when present.\& T} .TE .sp 1 .SH SEE ALSO .PP \fBmpv\fR(1), \fBfeh\fR(1), \fBsqlite3\fR(1), \fBregex\fR(7), \fBssh\fR(1), \fBstrftime\fR(3), \fBlibcurl\fR(3), \fBlibcurl-env\fR(3) .PP .SH BUGS .PP Don'\&t be ridiculous.\&.\&.\& .PP .SH AUTHORS .PP Grigory Kirillov and contributors newsraft/doc/newsraft.desktop000066400000000000000000000003401516312403600166760ustar00rootroot00000000000000[Desktop Entry] Name=Newsraft GenericName=Feed reader Comment=Feed reader for terminal Type=Application Exec=newsraft Icon=newsraft Terminal=true Categories=Feed;News;Network;ConsoleOnly; Keywords=feed;news;network;console; newsraft/doc/newsraft.html000066400000000000000000001323221516312403600161770ustar00rootroot00000000000000 NEWSRAFT(1)
NEWSRAFT(1) General Commands Manual NEWSRAFT(1)

newsraft - feed reader for terminal

newsraft [-f FILE1] [-c FILE2] [-d FILE3] [-l FILE4] [-e ACTION] [-v] [-h]

Newsraft is a small text based program for reading syndication feeds. It obtains content from a given set of sources and lets you browse it all via one streamlined user interface.

-f FILE

Force feeds file to FILE.

-c FILE

Force config file to FILE.

-d FILE

Force database file to FILE.

-l FILE

Write logs to FILE.

-e ACTION

Execute one-time ACTION from the following list:
convert-opml-to-feeds (takes OPML from standard input)
convert-feeds-to-opml (takes feeds from feeds file)
reload-all
print-unread-items-count
purge-abandoned

-v

Print version information.

-h

Print usage information.

To start using Newsraft you have to create a feeds file with the list of links to feeds you want to receive news from. Check out FEEDS FILE section for file syntax and valid paths.

When feeds file is ready, you can launch Newsraft. There are only 4 menus you will have to deal with: sections, feeds, items and pager. Default binds are listed in ACTIONS section.

Sections menu consists of section entries which are needed to organize feeds in groups to be able to process them in bulk. They are kind of directories for feeds. If you didn't specify any section declarations in your feeds file then you will get to the feeds menu straightaway.

Feeds menu consists of feed entries. Every feed entry contains news downloaded from one specific source which you have set in feeds file. To update a single feed you have to select it and press r or R if you want to update all feeds. From feeds menu you can get to the items menu by entering some feed.

Items menu consists of feed item entries (i. e. single pieces of news) which you get when you update feeds in the previous menu. Every feed item entry has two switchable properties - read state and importance state. Keys to change read state: d to mark read, D to mark unread, ^D to mark everything read. Keys to change importance state: f to flag important, F to flag unimportant. To view item's content you have to go to pager menu by entering selected item.

Pager menu will display some details about selected item and render its content if it was provided by feed. Usually feed item entries have a links section with one link pointing to a related web page and several links that were mentioned in the item's content. You can copy these links into your clipboard with y key and open them in your web browser with o key. To target a key action to link with a specific index you have to prefix your key with this index. For example, 5y will copy fifth link and 17o will open seventeenth link in the web browser. You can also setup custom command bindings to execute any commands with these links. Consider this config file:

bind m exec mpv "%l"
bind f exec feh "%l"

With this you will be able to open any link in mpv(1) and feh(1) directly from your terminal! Isn't it awesome? It is freaking amazing!

For both sections menu and feeds menu there is a special explore mode. You can toggle it by pressing the tab key. It's truly miraculous: it reveals all the news in the current context (combines news from all feeds of the current menu into one list). This mode may come in handy when you want to quickly scroll through all the news without switching between sections and feeds back and forth.

And for a dessert, I'll tell you about the search functionality. You can type / to begin search input - enter the desired query here and press Enter. This will open up an items menu with all the items matching your query in the current context.

This file contains feed entries that Newsraft will display and process. There are 4 types of lines in feeds file.

Comment lines start with # character. These lines are completely ignored. For example:

# Look closely. The beautiful may be small.

Feed lines start with a URL. After at least one whitespace character, the name of the feed may be specified - it must be enclosed in double quotes. For example:

https://example.org/feed.xml "Lorem Ipsum Blog"

Generator lines start with a command enclosed in $(). These act just like feed lines but instead of fetching resources from a remote server they use the output of the specified command to obtain the content.

$(cat ~/local-feed.xml) "Lorem Ipsum Blog"

Section lines start with @ character. After any number of whitespace characters, the name of the section must be specified. For example:

@ Software Releases

Both feed and section lines allow you to set individual settings and binds for them. The syntax is as follows:

@ Lorem Ipsum < reload-period 1440
http://example.org/feed1.xml "Dolor Sit" < reload-period 60; item-limit 500
http://example.org/feed2.xml "Id Est" < bind b mark-read; exec book.sh "%l"

Settings set for feeds take precedence over the settings specified for sections. Not every setting supports individual assignment - only settings with asterisk (*) on them do (see SETTINGS section).

Search precedence:

1.
$XDG_CONFIG_HOME/newsraft/feeds
2.
$HOME/.config/newsraft/feeds
3.
$HOME/.newsraft/feeds

This file is used to override default settings and bindings of Newsraft. Presence of config file is totally optional and Newsraft will work without it just fine. There are 3 types of lines in config file.

Comment lines start with # character. These lines are completely ignored. For example:

# Good design is as little design as possible.

Setting lines start with a setting name and end with a setting value. Available settings are listed in the SETTINGS and COLOR SETTINGS sections. Here are a couple of examples:

scrolloff 5000
list-entry-date-format "%D"
feeds-menu-paramount-explore true

Binding lines start with the bind word. They define actions that are performed when certain keys are pressed. Complete list of available actions can be found in the ACTIONS section. Here's an example:

bind r reload-all

The special exec action makes it possible to run shell commands when the bound key is pressed. Specifiers within the command are replaced with values corresponding to the currently selected entry as per menu-item-entry-format:

bind m exec setsid mpv --terminal=no "%l" &

The edit action runs a specified SQL query on the database, so please be careful! @selected specifier is replaced with a condition which identifies the currently selected entry - make sure to include it if you want to target individual item/feed rather than the whole database:

bind w edit UPDATE items SET user_data = json_set(IFNULL(user_data, '{}'), '$.toWatch', 1) WHERE @selected
bind W edit UPDATE items SET user_data = json_set(IFNULL(user_data, '{}'), '$.toWatch', 0) WHERE @selected

Use the find action to retrieve items based on a specified SQL condition in the current context. For instance, to search for items marked as toWatch (as shown in the previous example), one can use the bindings like the following:

bind f find user_data LIKE '%"toWatch":1%'
bind f find json_extract(user_data, '$.toWatch') = 1

Binding lines can fit multiple actions in a single key. These actions will be executed in the order in which they are specified. Assigned actions must be separated with semicolon (;) characters, for example:

bind key action; exec command; action; edit query; action

In case you want to disable some binding which was set in Newsraft by default, you can use a line according to this format:

unbind key

Search precedence:

1.
$XDG_CONFIG_HOME/newsraft/config
2.
$HOME/.config/newsraft/config
3.
$HOME/.newsraft/config

This file stores everything you download from feeds in sqlite3(1) format. Although you now know the format in which the data is stored, it is highly recommended to avoid modifying the database manually - things will break and it will be very sad.

Search precedence:

1.
$XDG_DATA_HOME/newsraft/newsraft.sqlite3
2.
$HOME/.local/share/newsraft/newsraft.sqlite3
3.
$HOME/.newsraft/newsraft.sqlite3

Settings with asterisk (*) on them can be set for individual feeds and sections.

Default: 0. Feed auto reload period in minutes. If set to 0, no auto reloads will be run.

Default: false. If true, feed update error indication in the menu will be disabled. It's recommended to set this setting only for specific feeds that are expected to fail frequently and you are tired of seeing their errors.

Default: "". Item search condition when accessing database. This can be very useful in managing feeds with a heavy spam flow: you set a condition based on some parameters and only those entries that meet this condition will be shown in the feed. It's specified in SQL format. It probably only makes sense to set this setting for individual feeds, and not globally (see FEEDS FILE section to understand how).

Available parameters:

guid (string) Globally unique identifier of the article which is expected to be unique within the originating feed
title (string) Title of the article
link (string) URL of the article
content (string) Content of the article which is stored exactly as it appears in the feed, with all original HTML preserved
attachments (string) Serialized string of all attachments with URL links, MIME types and byte sizes
persons (string) Serialized string of all people related to the article with names and email addresses
download_date (integer) Timestamp of download date in seconds since 1970
publication_date (integer) Timestamp of publication date in seconds since 1970
update_date (integer) Timestamp of update date in seconds since 1970
user_data (string) User-defined data for the item. It can be modified using the edit action (see CONFIG FILE section)

Here are some examples of correct setting values:

title NOT LIKE '%Rust%'
persons LIKE '%PHARMACIST%' OR persons LIKE '%OFFL1NX%'
attachments LIKE '%audio/mp3%' OR attachments LIKE '%video/mp4%'
CAST(strftime('%Y', publication_date, 'unixepoch') AS INTEGER) >= 2024
NOT (title REGEXP '.+:.+\".+\".+by' OR title LIKE '%(ultra slowed)%' OR title LIKE '%(1 hour version)%')

REGEXP operator uses POSIX Extended Regular Expression syntax, see regex(7).

Default: 0. Maximum number of items stored in a feed. If set to 0, no limit will be set.

Default: true. If true, item-limit setting will also cap unread items.

Default: false. If true, item-limit setting will also cap important items.

Default: 0. Minimal number of list menu entries to keep above and below the selected entry. If you set it to a very large value the selected entry will always be in the middle of the list menu (except for start and end of the list menu).

Default: false. If true, moving down while on the last item in a list will wrap around to the top and vice versa.

Default: 100. Pager width in characters. If set to 0, the pager will take up all available space.

Default: true. If true and pager-width is not 0, pager will center its content horizontally.

Default: time-desc. Sorting order for the items menu. Available values: time-desc, time-asc, time-download-desc, time-download-asc, time-publication-desc, time-publication-asc, time-update-desc, time-update-asc, rowid-desc, rowid-asc, unread-desc, unread-asc, important-desc, important-asc, alphabet-desc, alphabet-asc.

Default: none. Sorting order for the feeds menu. Available values: unread-desc, unread-asc, alphabet-desc, alphabet-asc.

Default: none. Sorting order for the sections menu. Available values: unread-desc, unread-asc, alphabet-desc, alphabet-asc.

Default: true. If true, update menu contents as soon as possible. If false, the menu will be updated only when you re-open it.

Default: auto. Shell command for open-in-browser action. If set to auto, most operating systems will get ${BROWSER:-xdg-open} "%l" while macOS users have it set to open "%l".

Default: auto. Shell command for copying text to clipboard. All copied data is piped into the standard input of the specified command. If set to auto, Newsraft will determine the appropriate command based on the user environment:

  • "wl-copy" if environment variable WAYLAND_DISPLAY is set
  • "xclip -selection clipboard" if environment variable DISPLAY is set
  • "pbcopy" if Newsraft was built for macOS operating system
  • "newsraft-osc-52" otherwise

"newsraft-osc-52" is a special value that doesn't run an external command but instead triggers the OSC 52 escape sequence, instructing the terminal to copy data directly to the system clipboard. This behavior is especially useful when running Newsraft over ssh(1), as it allows clipboard operations to affect the local system rather than the remote one.

Default: auto. Shell command for invoking system notifications about new news received. If set to auto, Newsraft will determine the appropriate command based on the user environment:

  • "notify-send 'Newsraft brought %q news!'" if WAYLAND_DISPLAY or DISPLAY is set in environment
  • "osascript -e 'display notification "Newsraft brought %q news!"'" if Newsraft was built for macOS operating system
  • "printf '\e]9;Newsraft brought %q news!\a'" (OSC 9 escape sequence) otherwise

Default: "". Sets the proxy to use for the network requests. It must be either a hostname or dotted numerical IPv4 address. To specify IPv6 address you have to enclose it within square brackets. Port number can be set by appending :PORT to the end of setting value. By default proxy protocol is considered HTTP, but you can set a different one by prepending SCHEME:// to the setting value.

Default: "". User for authentication with the proxy server.

Default: "". Password for authentication with the proxy server.

Default: Global. Name of the section that contains all feeds.

Default: false. If true, global section will not be shown.

Default: true. If true, print menu path in the status bar.

Default: r:reload R:reload-all tab:explore d:read D:unread f:important F:unimportant n:next-unread N:prev-unread p:next-important P:prev-important.

Placeholder which is put in the status bar if it's empty.

Default: <b>Feed</b>:&nbsp;&nbsp;%f<br>|<b>Title</b>:&nbsp;%t<br>|<b>Date</b>:&nbsp;&nbsp;%d<br>|<br>%c<br>|<br><hr>%L.

Sets the HTML format according to which the item's content will be generated. Fields are separated by | character and ONLY one specifier can be placed in each field. If an item doesn't have a value corresponding to the specifier in the field, then the entire field will not be shown. Specifiers are as follows:

f feed title if set, feed link otherwise;
t item title;
l item link;
d item date;
a item authors;
c item content;
L item links list.

Default: %a, %d %b %Y %H:%M:%S %z. Date format in the item's content. Specifier values correspond to the strftime(3) format.

Default: <b>[%i]</b>:&nbsp;%l<br>. Link format in the links list of item's content. %i and %l will be replaced by link index and link address respectively.

Default: %b %d. Date format of the list entries. Specifier values correspond to the strftime(3) format.

Default: %5.0u @ %t. Format of the section list entries. Specifiers are as follows:

i index number;
u unread items count;
t section title.

Default: %5.0u │ %t. Format of the feed list entries. Specifiers are as follows:

i index number;
u unread items count;
n total items count;
l feed link;
t feed name if set, feed link otherwise.

Default: " %u │ %d │ %o". Format of the item list entries. Specifiers are as follows:

i index number;
u "N" if item is unread, " " otherwise;
d update date formatted according to list-entry-date-format;
D publication date formatted according to list-entry-date-format;
l item link;
t item title;
o item title if set, item link otherwise;
L feed link;
T feed title;
O feed title if set, feed link otherwise.

Default: " %u │ %d │ %-28O │ %o". Format of the item list entries in explore mode. Specifiers are the same as in menu-item-entry-format.

Default: false. Enables explore mode in sections menu by default.

Default: false. Enables explore mode in feeds menu by default.

Default: false. Mark new items as read automatically.

Default: false. Mark every item that changes on a feed update as unread.

Default: false. Mark every item that gets selected as read.

Default: true. Apply all changes to the database file in one big transaction after all feed updates have finished instead of using a separate transaction for each feed update. This improves update performance a lot with the downside that fetched content will not be saved if you quit Newsraft before update finishes.

Default: true. Run "ANALYZE" SQLite command on the database every time you start Newsraft. It gathers statistics about database and uses it to optimize some queries making runtime faster.

Default: false. Run "VACUUM" SQLite command on the database every time you start Newsraft. It rebuilds the database file by packing it into a minimal amount of disk space. This can significantly increase startup time.

Default: 20. Maximum time in seconds that you allow Newsraft to download one feed. Setting to 0 disables the timeout.

Default: 0. Maximum download speed in kilobytes per second (kB/s). Setting to 0 disables the limit.

Default: 500. Maximum amount of simultaneously open connections Newsraft may hold in total. If set to 0, there is no limit. You can try to increase this value or even set it to 0 if you want to squeeze out all performance to the last drop but be aware that things can start to break at high setting values. One obvious example is getaddrinfo() starts to choke with a large number of simultaneous requests trying to resolve domain names.

Default: 0. Maximum amount of simultaneously open connections Newsraft may hold a single host. If set to 0, there is no limit.

Default: auto. User-Agent header sent with download requests. If set to auto, Newsraft will generate it according to the following format:

"newsraft/" + NEWSRAFT_VERSION + " (" + OS_NAME + ")"

OS_NAME shouldn't be a matter of privacy concern, because on most systems it contains nothing more like "Linux" or "Darwin". If you want to be sure of this, check Newsraft log to see how user-agent is set at startup.

If set to "", User-Agent header will not be sent.

Default: true. Prevents too frequent updates for some feeds. The limit is set by the creators of the feeds in order to save traffic and resources for a very rarely updated feeds. Disabling it is strongly discouraged.

Default: true. Prevents feed updates until the expiration date of the previously downloaded information in order to save traffic and resources. Disabling it is strongly discouraged.

Default: true. Sends an entity tag corresponding to the previously downloaded information. If the server from which the feed is downloaded contains information with the same tag, then in order to save traffic and resources, it will reject the download request. Disabling it is strongly discouraged.

Default: true. Sends a date corresponding to the last modification of previously downloaded information. If the server from which the feed is downloaded contains information with the same modification date, then in order to save traffic and resources, it will reject the download request. Disabling it is strongly discouraged.

Default: false. If true, Newsraft will use colors regardless of whether NO_COLOR environment variable is present or not.

Color settings are the same settings as above, but they take two color words (foreground and background) and optional attribute words. Available colors are default, black, red, green, yellow, blue, magenta, cyan, white and colorN (N can be a number from 0 to 255). Available attributes are bold, italic and underlined.

Color setting Default value
color-status green default bold
color-status-info cyan default bold
color-status-fail red default bold
color-list-item default default
color-list-item-selected (not set)
color-list-item-unread yellow default
color-list-item-important magenta default
color-list-feed default default
color-list-feed-selected (not set)
color-list-feed-unread yellow default
color-list-feed-failed red default
color-list-section default default
color-list-section-selected (not set)
color-list-section-unread yellow default
color-list-section-failed red default

Actions Keys
select-next j, KEY_DOWN, ^E
select-prev k, KEY_UP, ^Y
select-next-page space, ^F, KEY_NPAGE
select-next-page-half ^D
select-prev-page ^B, KEY_PPAGE
select-prev-page-half ^U
select-first g, KEY_HOME
select-last G, KEY_END
jump-to-next J
jump-to-prev K
jump-to-next-unread n
jump-to-prev-unread N
jump-to-next-important p
jump-to-prev-important P
next-error e
prev-error E
goto-feed *
shift-west ,
shift-east .
shift-reset <
sort-by-time t
sort-by-time-download (not set)
sort-by-time-publication (not set)
sort-by-time-update (not set)
sort-by-rowid w
sort-by-unread u
sort-by-initial z
sort-by-alphabet a
sort-by-important i
enter enter, l, KEY_ENTER, KEY_RIGHT
reload r
reload-all R, ^R
mark-read; jump-to-next d
mark-unread; jump-to-next D
mark-read-all A
mark-unread-all (not set)
mark-important f
mark-unimportant F
toggle-read (not set)
toggle-important (not set)
toggle-explore-mode tab
view-errors v
open-in-browser o
copy-to-clipboard y, c
start-search-input /
clean-status `
navigate-back h, backspace, KEY_LEFT, KEY_BACKSPACE
quit q
quit-hard Q
exec man newsraft ?

You can also bind escape key, but this can potentially lead to incomplete processing of escape sequences in the terminal on a very fast input. This is due to the special role of the escape character under the hood of terminals. Your mileage may vary.

Data formats of feeds which Newsraft recognizes. Not the whole functionality of these formats is implemented, but only the functionality that is most likely to carry the most essential information.

RSS 2.0, 1.1, 1.0, 0.94, 0.93, 0.92, 0.91, 0.9
Atom 1.0
RSS Content Module
Media RSS
DublinCore 1.1 Elements
JSON Feed

Newsraft's behavior depends on the environment variables set, however not all environment variables affect Newsraft directly - many environment variables affect libraries that Newsraft is built upon. For example, libcurl(3) recognizes a large number of different environment variables which you can learn more about on libcurl-env(3).

XDG_CONFIG_HOME Directory in which user-specific configuration files are stored.
XDG_DATA_HOME Directory in which user-specific data files are stored.
HOME User home directory.
BROWSER User web browser.
WAYLAND_DISPLAY Identifier of the Wayland graphics display.
DISPLAY Identifier of the X graphics display.
NO_COLOR Makes the interface monochrome when present.

mpv(1), feh(1), sqlite3(1), regex(7), ssh(1), strftime(3), libcurl(3), libcurl-env(3)

Don't be ridiculous...

Grigory Kirillov and contributors

2025-10-04
newsraft/doc/newsraft.png000066400000000000000000001315621516312403600160240ustar00rootroot00000000000000PNG  IHDR8pPLTE֘ `wwwjVPK61+WIDATxA BA`VrnO`T\0ƨ"0FE7[0ƨؿ{g6}q@$o10ocS:oaSXvso_*N:G'dYbx O<_G?u > ǤAx'1OE4q]PvT: N]Xc?YdLc*x‚&v7hUtJ@ @C|`U`& 5SgJ( bΨ–f3 M8Јdn֑Wx@kW5vcM+7X^W4w @ *'c`Yl *Mi?&kϷs8E4AXv2VhMva%|$eXcZΈ1m}-,"Bly 7 ;iVi2!MeƅUOӿ)xoc[ qՄkX{ܙ䲻0UE sǭiNղUrh Oy^)4*8oABugViJpqayЋDӄlz9<*[:nOWike& T}9}T$W%kL9m'J1&xrӸ4?˱츕\!'C Ї:?9<p=q6 BkզkMr]"ecMrw>u -p#'*8gA&? cL^#٧2+E;4e xhNpI.xk$!їk+X^W_[p( .j<*|4o^ů7" f +q}. `N[ݱZ ,ej>F%= Vi2xS)2=#a1 J>`GөX5vcMN:ihB3d5 BB<*M> o`墹\'"ݱe p=+X^Xd![pނM~X}`";pZoÙ>pm)xkdwy`[`10`LEyUfp{+^AKu hOυN,9uJX&?ViK8YSCDScPWhU]0?XT,c׷C`~yT_{:gc,&Ո5]$f&=-'ИAAPM#c}Û6GvpEx9 pbk-xh^.y)X~0_#4 J\=X{ZBXa҅]}o?S). 6 #4}굵\8 J2V=ɦtcVPb<5ڬAB-xrEETSr]"%0M'ѩB̈ct%=1L%+V9f}POcK^%S_#gֲEm l;vM,eSHLJ. 8Ph'v35pT96ՃQ>R%+B`[&VRi2Q+ %[ЌG:0 Z\ͪʦdˈvUJ]"u85lV3.heʬ @ $c| {ZʦtNɳ +rt+p62Fj1ȯ WI%Y [T5aj]m淝\+1\.!і`Kڗ:-\kb) i5,؝MS xxR8a ٔUEf6`P8oAfku*M>DXjЍj@/c9/`$Ї:x3Ǜ']\ÿoh)}M;*B,x4! p8iNZa_`'c9;v)( mD7ٺ5ipMBa% xhx] ps5Y\HbL6t5fۀ^8qֲ{`wbdj4%/qW{kWB͙U2}]DL ,cezy$e<[\>yN<`"X 5Rf2^8 -N{`yUԝzxu) y횷=T 0'Rj&Vggbz9 ɹZ맱?YDfv\hQ8v`C4XT{&L_ghښ3Z 5 JF 2 pi]WvM,=ѻ.p }:mBx`5D5R<pH*l>W7e `>갈0h 54CgLlºFf 쪔]ЩgT8&<`"d`,\r`xDZ*/gUJiӲ9xxa]c̦hM࠰v&V|`װ20*/gj]iU,9FVL"*BN(iXJ{<5Ỳ\8:ղ ƪ X8$ &fiMrNN?jM,dZv^硛ye y9ǭD ܝPTm-:wz(Р D3.`|`mIx&?pИJcvM[p9 Fmh}XG hMr]`Ġ=įC9ڮqZfbCSi]ܞ9cadaVۡa؛䲻3I4/ 2V9 zˑ]%P6]֭=qy{.p z<(X7?hc$Vms1Fh&"˽ͦ" 0eu="Ň k[7i/Gvf,`LCo8 pv®c$]h2#J806cݝ;o&#ʦ"-`Ոcw2)ORJlEF؃YqX)r O-0˜A, xjx6`"u',lیS5jH Gָ3TD2c|*FIuCcW SǟwI4o^ů7" f B}vXvn{|{Zկ 4a@oxy'F9mC8)uCcW M쩔_ruxMxQP ;3䲻DuG ,|,M6[+',6 nTO%]@٤*5^0r\hghCאU5L.X7oN\MlXv2Va; `r.`鯄='\V[U6ܽh >\v nX6&<|~U=ȮACX:u 3wW7Q:Qfn~,߿_fl 6ƘRX@ٖ &Ԯ`%(|< м\)lU#S.`|oz`jv |fSa{%.Ue[>zQ_ adze086(JeX7<vst@{)FS.f5ͦ"[`;Rr5zrGps/Z=AY3Ȭtk(ߑ'l@.);Z|7Q-pԚô;NljlMew-pw[E3EY7F&VVQr]8f>:䴨ZMEul3nݐU?`}>vwjB9H~6ZZ-xrEa ).`z`M_5ZLy+̳1"Z\h[7d.GvW Hkb+b\m l;v$eS7P%ݥhBx9c r*L6YKƪH]n+f,pT=u9Ʈ,`ikh#Z@vgLfBe3hVݥ oT=e))"+}+cd`@ӕ)[6`UOs9ɗZ9hMXZ-HSBF͒%r r<5Iq `|x%QS#JӪDx!^:n \k~euӐZ`]: b`95XUĔq0 mHTȮ,r`ۂr`;.`;.^`rv9.wvS]|9.Nu97.r]Tr"9.Nu9.w/NzT<_(>+[fB?HZxURZ*xs 7e V !h_y -XV6,`B tv]vG th̅ՕV}aG?"hKK ``wf@oN=;w&;Kt`eʊe+8mQv)FR)h0o=gaL!5:V؝%8|ytc ++yG?K@ ܝpU;[S:L> 3)Р ؝!xFi`$ju_TM;_" o_s Cfz5'n֑Wx@k/Xaq_{M/١az. `nm4C3jK7In쾿`wXXI_ j™ 8 USL|Co:m& w$ebK(X)Zg'5<p4`RX.X)I+6tx6G@A{ܷ4x,ٱUM BQdiM!cx &E\[`u؜k]X㬪*MguK֥rt^.%~Q.g4z^)B@Oxew=Fؚ_`U6,іfʂ\.]SVWf,^/=tGc1g`KtT`LCo8 pv®cd0D\vC#}r:Ug'4稌Xni۠\.]SVWk1 } -}&%?GE#8+#ǛNIJ^a_pH_xVLr]z#gͩC=;5^tV+,K53 VZ63yG,x_#Qo^Ec)>VWvq`5~BoN UjJ3ge>SX.X)HkWԮd{PX e%?Tʯ#!N&rL(c(]_;WvZMH٫#`V%P4,䥰\.]Sr$XGMPR2zǏ8vEsџB;^cRew0t=;5p7`YKapU pAt|&`K%DF?aw*X]L8|@[ -]Fn0G emyT Jr8_OPP҆b҇5k>'m_ޔB' 4V7OB^ \u9xf `_ &Ԯ `%(< м\)l!( z[6aZ`YKa .z+morIÅ4x)40B_p޴%B^ lu9̡/ ,ZvEXp};9k:`P)Pv١8 \!}re!/e~ |[5p#`=-~y e<7jg}`̚x(׈ )PvXOX&:uoy'`e >`T ` pSa)M-:)Pv&0㙶:d~W%XRX^ pFPPswXPgbWh5 = `k]w*c2F&zVr]r^YvcoEa1! ,eÒKPrlRcklwXݝnyj.Yg !o9䲻жoۏvey粹в K8˙ >/m ((XiYGw¿ =o5T HxYf{lA[C>`&G%݅dpۚ`lXT إٳ tX. >'_#uhLKF³\bAK5c `Q^9.Nu9r]Trr]˝r=n.\*ܩ.Nu9r]˝r]TqӧAr=nk, .uP$[*)bW3A0{J-6 6!uewq -`/[K* v:Z0Kvazg` G>S  @|&K` *FRYXfXsd"Pc]I@n U/O6.e;D ܝPT׵iwBl*Р V%DrfHZh ֻkJ8ThN[]xrۇudU$,G܂@ץ|`ջd C}1]EP'vo]XmIbob\nAR>XT6;.۝PhÀQ7F4m& w썭$݅,GUozZJvSCY>Z>.{TBچ gz#5JW|,0{[ .Q6^zY`Ze C\ !mMr]r0ZB|L_5ϝɵDA^1-5`>Z.;g4.`/^p1@2&ˁ!Lek^r- 4yz ţ]vP'p3Ǜ'TIi:N!!BY z 86cao5UHTia7?ڰb xh BCz<5i0ewQP؁ݠa jw&Y0#2 0z& |F,0_z*`&KаjjewQ%bD+,kv+[xz=5|8-nMbu]3k@.Q0A1 J>`Ǫd~ə\v p>:4Z(ZW˩|[0= xۆwq"X$u K+/zh.43WhgǦk*k[` 0ǑieA< 'u>tu J UjzX%oÙ>pkcήad^ewQ7*j`YZ_Faj(4T9an t!#b%,G k(X4C0)yasvÚlP2U4C pV"L@xx| ꬌ c_ a<娵yeb(XVEe; YSM@z]Y+D8<2_[zPfˬj(ˋE?蓑`Y03S.X؞4J9R#Wm~&xVгևU]}enL~䲻H@h+J"k(EKjE03z_trgcSM % <)u,@J"4`XH{#]+`YXf,sd"P# I7T-1+g$Bsv1];4{QwY;Ne)Р VbDJ.wk`- JDg JX%K"4 `XPڹnB3?pS F7>#$e2X'iirWY;t^fbC(3˅O  bo2n7XR)bqr% >UL|oØ6U;qg ݥƍ񐷙 F&I5=%i\̤ pQ;k YJ6L [Tm&g#s܊@=qq`/.B@OxewONm7o^MCwq_p%ffB.|xI۴v#kEׄ!䲻c3C8:ϵ JQ-0"kp]N+ͱ9e¾c i0ew-G4 ; u<7Ng j\7ؕD0ayS /~DxY0whp}M%Rd11H%5.JhCxMxQP ;l~\v 7A>DՒ>*7,Uo`J RN\'J.Qkr.9)Xjf]`k{G^ `9|]jI$4gD?Xါ&0N|6Z`}Қew--aZJ`[K¥_#qfO,(5rgbWh5 = `McX*. R836~Z5SȟB9H~6Z΁apOv1CB{d3]"P>_#ub4{[Gn֌)<Ձ m7`}0V)٬Jlj"5ewS)!1_{`yRĤ1vJ,iџ! Z9"Y١g,c c l;B `MV),f8\w~}:  (Vlat3<¡0S@w'`Ƨ,ౌ#ب"[Fz"`"`ɏ1*ƨL"`w+H&- -01Ϳg@btٖ+<*c*%},vm&;+⢞-QMҁPǷ)72xڤcIwOv`!p[f3캫5|}&{`$_lۤX_r?j-Rnvբj8Yq<&+pJ1`<`X;4wL FDSÁh^- pwBxLIx\>_U0UӮ<-sT4!TȈkQ7uYxRF8M q/Ex/265(0_c͗;8"8L5lyոa :EpQ;J'е'Iq܆0l`+zXCν.w`Y'N)'x'ş~A/f01ݫ˭I+Vy60Q:_v\?l59۠RT6x;ya,:#Uuq06_DҥRZ1mI=ƍ; 7/x-@7sO@ UioaԸ*o.|l\p ,$j܊$^` skpwxbwmU`kD)e`B ̸èq<}*7ݢWs7``A>&>_nX=?0[g ̸GW`;]j05KkQlC7.үW[,MT`㧺~r[Z+ g{J/V 7/Cof3an!twb ->Uiop ;9obB!tzzeD*/w~b㤻k܃̉oh865!}K?'J3Mx3qe|ƔP br%&'<+μ r֤*8U%QHp\}E ԍ:Wb_>&iL]**4! 8vBB{N[fKx ` tw{P+>> 9p l|R6f:cU`BfC*% bkܓukNY L3|[MhkqYF7qt\6x&>W`gɗNXMڡ= ~nT /( `gum Ċ92'n,ǚd&h,U|ުڛR}U/mՓ<(z'rZ'rHtOcͯW5>p#k߫ڛ|- ٣[]"b7lXn% D}&kEk#^Jp5$ߍ9&n4NXl(Zͫ\7oէRT`[bR*λXY{$uz"&`G ck3Mh\@ p$)?YmK7 υ=`<2:T,oQ}\ׁԾ]UL{w+vvB]J$LVBX lj|ϕ D,|T%ֱX'zKo~مPDh'D@ [@/KwG'`;j%7?&lHJ pQ;6*ƨx;#`]7gFEx0FE/"`Q0FE{0ƨuAhعYqKK︔M.Hݷ\Zzt̎ŒGжd=ٮTHqA}[P 3~Te^wW9H-V!䃽Vx.~ ?8?a,#:H@ hK,/ 6"+c_4\'-&n G2C +mx K;:!d10岤3p1fo LwN;J1eIvl7dA4J=p')mPW&B?|7u|]#<ɪfQ,d^ |aRy~84jN9bwזnV <><XOb]`>YwvچS/Jjc[UAFc=WީNm]h>4zV.m_cvwEiÏ؆0:4Oc['05'5 8n(B@4_k{ֈ,KT=܏P3 a'uMbk~pXwNkL0;‚BxV 1DU7D ,K5X"[my'0ҙ!K w#Q]HJ{&(~_qF]%WGgf>^ZZ)d`f `9d d̩&f2mt*LTlq3 t&dN56&s6SMd`f `9d[L'$A9zRَNd;EoPMb{Nݪ{Ily|m϶iD(Vc\-^̦6ӵS `& `^/6ȤhI&1`7^J;\%{NfEWRd 7ڎsН@|he}Sa̶[՝Z"ZH] X+x`L;RD9$?Gn 0l+xD3@9+XaS`ۭvߖ"7ӵSBTa%p w}py%L[Rs NNeN}2]` ,H#D  -`\i?6NNeN[:Vj09Ak[UU-gtTk`0T݂_`[Īh֛c:$UAȵ:l EZߩiY_ }arf fB*VwÔk*tӗl0vaܤTA~WL_c 95{`({f.iVH+}h {(f [_*9˵\!&°̶LTTɶs'mqpVpjvQ?J:q6f5 `^co3m[mM)6N N00lqVwlcWpWGm&۾ږR{Y.{`l[ L:^%`ˬq7V郅iuV #0l_ \h90t1rSXpw*'AR-ntU `Rv6l6ӵSCG-n, +<ֲ]tʹSC3́lC|1;8l 8PL̵-|D`(iꮉ߉u9ơ||w6Ym)TE\1E&[2N~[LN  nǘmMRx鯴i|;\ԉܙ+L,c?9S0 Gf'Ofx+Lq- l#j5/X!& 'JNƠ&8d#/G[?9=.@rxd*xCdKs"xP/A|G>니tܟ]d'yZ89lq(68d;~΁dx= }ʊ'Ǎ.2el"J~Rp2&ڡ!<4RlG`M/3W[Ă:i 7'Ç*x`.r_򧘗<0ZZ.d˞ rzYHJz3| :a{9}j6`)BpA#:2[ZFm~QZ)ɞ=?JFki_pHөr#c$a`ZEHlz]  dPE)#+l ЧPˮ¿f+eÖ.[`XK.X M ɔ+NfW0~ 7GdP`όlBR2}H1)n 0Re6px̥݉|Y#^\q`lW%)GJ2p}1k;Bӎ8l#c)?W~|q9n>%e;/z;w*񑧑 =]d`+̶qmO,rqI.hcb(7J 2).YĿIY+e2.荑+̶lەFD ɍdь& Cf.϶Kf G `ެ)8l6sbiLqcBkkyy]~E?t+̶!ءeʿ1/Lqczo_PKҫ0S"mLW|211)VDM5oMUnd_ U3- `lf `9d!dN5I6*/JU̩&fD\3Mɜj2-n&lq3Y"X&dN5+6&s,OzI`{s.-vouڶG[+TȨ@8}ZʙS?'x:u"_.}`j2hP1#j,/D .$(` -6xS9"sNCXp`b| ?<>Zy7hD5 NDsvRh[;U9%0`%hG>Y…E;#p;Tz/?ɜ6W[=8w94.\̩Y…:63mhoU v 0>#S!Φn Io3~Վ>+9@~ B3Gε9x$\(ąo`E ge:izPayLX+>,dz{8^Hh>OЄ3ÿ\XJ8XI#0rPaaCnz GpNx]_ܢnQd[0TsrRhG>YBaUu$)T'^\h M/'EF$ɸ-saq>)XihG>YBto?ub q! o4=>FGsF`#j,Vj)P26Ќ~?X#0+iF8*,Io?~gЍT/<~<Pwss[:fx|z070#p;;}9ϙZ=8G|#{"a8VE oqBVX/WYB;_pvTo`t=;r~\oP;&bt!ipɶH8aO,*zPaqtUp>v^bkʎ`r*V-pH#0mȇZ9Kߏ|mgy*LT `l2Md2/'d̩&6Y"X&dN57Nl2MTl2 t&dN5&sNl2MTl2 6&swTm`sB&l_hi{}@ꐛ\B܀UUuDeZ"n^ew2$>[~ѽJ~i{`Fl vz) VS:85L=h[k -ܽ\wxVmR;7$O6j.<{է5-e'ݕĭu'BܬN m$N `%y_'* &7A*6-Z !=\F]AȧZCbw+\j#{[>0,j_ HwT.w7_"}&^2F3p0/~0[tкAV wvu%q u]>nMZ6՟1,:a]\ 8Nw0!A>f.ߙ}éA8IBSvR][ Vl 22:KA<} @ZB,_v!AQ;.\F]ԧ>eδ݉}Xȍb3'- j5ٗԆ^q":ֶ-k2*+՛[Ur`E% >8n0.#VϢC `zT' 5j\E.̻['nF&6xRatNfxή7ex)znK:{OL3u^AE9S_E,o:%6!C)ҩ o8ZƓ 52L730JFAӗA"V0.K_Ozѡ~xdŝϵ'M(1^ ;|`|%62*F !BǓ 3+$)nZqj¶t ܲ`=pWxC,*80HxVFrfXg F/XPRa0%Dm@ `XRatƒTPprYig28nV50.KW7_MֿNN'8K45ρfG`H?V%Բz [g77H.K7<;m~נ|5ρ9+Pz)nrBe4 O.$ 8q24js-q2СsuAרv1ŗxq-CxRatNqwQD{,3ƥ;eѮK:S20=;ZgR St=p5Ӗ''ҙS`R H0MzȐ[6<0]6~|GeoFᝏy*!4HzAl^}*?:` 5l9Ta6Ķ=Iݑft~x! .8SX8g#ph0(M\F] |s8T L>xo3o)Ta }C/´frmy;1])&sjb2 k/dWB+1 _ `*i#&RW]ɜj2-n&P6SMd_ 9d[L&8]f&dN57Nl2MTl2 `J2&s]Ѫ8-L_A_1;o!/y3fۋwP׷$r|Rekr H6}5]k d[oiƠ~kw#k6'uHuuaAmY"G"n2.f !]튵 3iŚ尶ɃtaٔF{ . \ݲeSwY$kWKcm* IP*K`XskmX3D H Elvs?fMɌblE6RuFnM[g5N.t o1cU l1=EnNXC3ݍ!`GP[QgMjYnnݲ ؔ6x$DX$Mqm bim;M޵ A2#blE35.Jc$03)1p :CO[1|1jsT,^mN{18Xjfc+ Iզ$ &=F: R%FG3-1&hA;#qM6c+ I]ϸÖ lJYYVNdo[~@!pST,#-~"X;yX؊?gRI#.j-eQ1T?*tEp#C9*]?"1OUCq\cV.ά )KUT!fKnnb!G'Q-"vd6OTo,]Xg$) ϛ4vI5Z!ddL5PcliKvaC4DBs@y{Y]x҅f%u`ł!*jvdk(&5 $O=wFf,W|K7Wv#;(/adGffc{{Z{O#$r}h+va4DB`AQ` )p;Мű6܍ޞ0!i%pR@-Y :kSUB<⥂ڎszW`- /h#31=R]_:ɵ]x^؅ufؖrd{^RH٢?a^-`ڠߎalE6K:l֙%0K CWlV8!]0؎XclE2si7OӅqF׶45~6HHվCnm1~֤V> 5]XgVYZ"psH~/)ڗ"r';25VYZairz7b,YZfX0?=>uYcI6c+ I=>O,.3AQ`ws'_nb"ّd3Ϛ$uO: lKk~oKR`aS/vGBG7YVZ#OR'L\`0,MiCpX!!nyIHL,oR0rdk(&qcGo,Y=d )j6׫?,vW_P/92"c+ Im~HLmil#B`(060 ;rp__W`vA`M?mR`ɉC1]Xg,v8Xs|Խ/*K2ر"ҌZ. I?z]Й˛O(V&X!p[[BBbk +.y+VXBZ!pBbeR=ߊ+ZXBZ!pBbeR+Z8n I-V\LjBbk I-VV?X!Z!p H'M{ ?,!Vjc#pSӼ/KۀlKf*E@_)7B3 [roKLZlНE<  0[곛n"ЉOd<c'0`3~"k${8 L?4<&q& , (8ۀC`\,MĦ+9=qnWJ`X]C|<۰K:0ɑ!q6;C[Flw*ld F ӌ/\\A`/PDӾRuZXI癉1xx}Tt]Cqo|Dm@փ_f߸e/e3G^Fs+uz k̲d`R;e6o@`}q{D۲:OZ~ƴ 4s+xX˰]W׳(2H? &A_nkbr /h>&vJW0bX~Iڀ<-e:a3: \rmssfʓT*] W%`xLbL`5%pN>ހ%2g+z \AdTptѲK9WPຓ0}WlShJil@Sa]fZN^F#CkHɓex~ x#su!N#.vd@=<qB˾]:!0A-X'MКWBLKekVM졵+.%Al~-`)?| m's~+B`$@k *:YMYoK L|58hS=X 6 )lOU`>dZȸ~1hitY`b2 m5,е_uBSRNv fWuf (0$#IN~޼8 |-fM~ڎ>`ƥɗV'@s@yY]xnӶ!pA |ESh5+  ~Vx` l榐̟`̔d+F׬3ceHd[)x'^T y?H[֯yĀJ+0Wh-^ .6Iaqb{WWM.O,h# ЉX$<+BŽ d<waJPm?9;>QN b0s 8k`5\^0=BO; 6+'֯ Eq6-luj-5w}.f9br\s^RH٢ 3^kY~_.L3J |O6F> l l<ؒ,һmV \!;;G~ЍXf >.Z6ciK+pzSdD\IL pAnEEXmƿCd.Lk)00 xm4.Ik hCV%$u?xxR8=|${)s*:Go2  L R`Zָ'5btFYlxA'!;bY[L^K{N}(JU΂<̄   ی>L@ISb3KMy`0[%&[)f;X" y1,\D/$[{삉d=|]xAn#[cIlsKa xXbr 7!p)3NJ^݋|?g& z*]f|r yE{b)f^{bͬNĀY?lb]xxW𥟀_e4( *@ܗ9t k\yT`sA^#` ~<2GO0p#c(L]gF/3}m\ 'uׄ`O+ ;v|n-ΦyK ىU/<~ `õ_4|_Z}ȍ`vilcVYt+%f6eШ? Z ޙu FvE!l\DJ?u츴qo0Rh +nrL UЯ&0 0ۀnoGh|] "`RdPHpdijM4U4l*~4߱N6 .\\_^9,x3}ꋶHn#-!p 6 z;-m wq.yAo8;1\  @#p|>y2h_VA E\p49[;-jnYu=p[ l%-cNYWs#pd+j*|׷ 5bྜ7v`(? tLJp JVAպ[Њ&^BvB714?] 6_+e5Fo#M|W*Wjc `)OA 7V :fbଢ଼Nu%,nA IdfXVw piB- Y\#X+i|# ۔Om`9c+׿#P_MAu 3gk@[li&i&ZDH7F`}+w_m9adrpH&۴_u`T֤֔j`r`e*`#$tsƚly[ _`T7f{76$Vkvy6o|ڠyX&MFP.XZ K*`<ė()SO-oYFmf#|y Jm.#HϾ̰`Ș ΩE}mؐ|ަ 4 uFuS#p3xbC]\Xصm$ "b ƛKb*oF`Tz?*~Pb?ARNgY:NkORe>oPZ?4,4Vs4`Eķ ټ%m3VAvfpȊgG(> -yUim`XBur%Yi'њhۤAkfЍXUG؎N٣RXrwCG࢈sRf|U0_oDe>oVd5rgәm!*~f:"VBAUn`8uE*6ҍ5e Fg~p`x!& .8vd5̠(B>1Agn&o39Cߍl:"+~C pyP]mr9v`r]r`wkvyP]-`{\.x^vyP]˃rv9.vyP]rW$fpX|gGt,}ZmbA6DYLvzG'G`~QL?rpa@Wviux*0@P4+*ouD7X 9 bx"L_(p <Ś?]0>0imlL4w!mx&x }20q~M,9w vj#Y/&ȠB'52VXt Y(?y=|6$F` 9p:w_%Slp:S 0OwVHv|>,Os#o,j<9p$jGWg2x * 0Oorˆo&F~FP'*RpkXٍ%'bSk7 N3#0\KY 0Z!^tKM:鬞bBQL:[>[~?Ѳd+Qy-_:,,la >G`S|'Z6 ~xLm!%?ue4o b$78)xlx)hmi4*PLXmcCG`㣔70e q `4,4W \._-yyY-]7/ybY/3. 3|z` 9\}_kư46R0u `~ |``boq )E<5ur:!B];#e qҟ[<V`㚒Yh+vh*P/>B5/:lŊ,~!@wF\QjUe ͡)>W ? pXm8x^76B fƧ@ѶpmK~ģ.@e .wfXS?LKatM,pFvqms3RSpo8aQO6bR0qgD}` 9TzAe]7Z)v-v= "]?w+<.}v9.\``_+<.څߩ+~o(r }tRkgTk9?'[<ߩuЅEz2o/3p".\>V9j?40Y,`Uņ DzؙT8vŨ¬ {sKz$C SXSTӋںWb6_]TEZתH;`i|nލ@ۋC/Fކ QnuqW2fE 5uY3+_TEj3d PW=c(~A#@^|1j\օZLUvPGa OlMak5n^qw."k )ֺp.l?dI`tNS *7wPA*/c/ҰeMkϲ]5C?c 5ء u.C_; oڸ~_iP@k ^|1J=OUz hO\Jgall: Jqw鯭NWN3 Y# k,a0TƟ~_mP CbP7eJuZhv,t:(Yp1ILx90V^a~f F7/ *8[G勥sK)ȏ41Yu#S|]/}MimNf^lagR>`?v\?7@A`xg$L97pa ](jIꏘo*׆Ǥmp'nnLiZ O ^|1Jmm|6{hV\x8eAq[7עw_ة xҚM\?7ۛrŦnL9>Ll+} Tx}J!`_S~Ƈy몰P7? i_ͺ ~vZ?4|@ۋCbJq=]XNl]Tmk k[bhMs0~55ukSR(`{qHYlQ 5KY/&ȠZ 0V߁I_-bhRpJt~|M[`Vv6͹L^]YYœ TL(: p@5V(X/[*ZY|!,)r0P6>WT $!C+fs KReUXSTaSΞ JZ^P <$b9~#koI/Ai}xԹ6ǃziYhȱ`Vps+u8m.:va{q.2+r<%BKHѼZsZӒWF 5*?x~M })Tcl/TZ,Uk`xߩ5 +s3kb/z]ExQg)J`VF#r1@}vg?X ~A^kkW ;*TTWܼU)&._ .Uae^,WV.؆\EI}X?5@ۛvͯ M~pbPkrk{(yzlE|IkX R7;`v0QyB^?4@"ڛr,63ܙP>Wvfmp]TWo OW;7;z;;k~Au97wk7wkT`ϛ;5<.r9r]˃r]T|``ϛ_ <.rM/ЅEz.tU?%Y`K^/69Y~ז78(0fX_ǰ8~W 0⠳Bl``ž&mS7rL`X]@ۏϋ;+EZ/6go#ֵcY/*l})OɆ^!ay60l9hQH/aF 2oJ6x#fLb{;;p|>nRIlO@+q gw؂9ˆ(֦^2ZpȼƖLK¹7BVcxg@"f/po]A,m Qo,]1 < p70@ ·Ә6b8G8D Ԧ?;y'yf0y=1,xg:Ǡ,qY!Ay%ur/ :!ek96~87kBQ>b9pkK>MwV-nӖC9`a?!$1Rb-Mao 0Ya._ARo#rѥG|3& I ~Aׇp6]1~20$1ϦVAEn`%,4!Ů(v>&'#x85M/$%o*i)C5Zl9`Ob1$1BŷųϨ0 0`X7LR~Rj:eļxcS/ycଢ଼~RV `~;`]7Q9` a`Akt 2o*vo`a9R/xkG<w#x!ay6m9h j`M0!<0Wm0xxwudP] !5px |f)CI]+;b}Cl*&BoX 0i`~v_%SЂl9|0 p{[: j=yF(%Cl*ھL&_fes)mVCM-[!U\r ,Mܗ# G`sw#ClNedq `M֝"`0:.~i~ח|<3BlV堩7 _ٴ*Y7}q64,4V`a")O*έȁ𻾼1Mҳyі[&`R)Ts? 2oD\S>!-yUim`XJ@ۚ!b5\2Cml^sEy7~ksT×t9`]͊-zY R^0$TfJbyڨÈ wuy@2$1feewv`np*xȼ5S9,y.LȁEzZCM/Br%a,Xb,4ClVźЭ@˖aM,*xHKZvfWx!& XJզ{C-ؙq?G`]Elo `P!-Hh UySMrϋݬ {\`v9.{\.vyGv9.#x_s]˃r]T|``ϛv9.vyP]r]˃r=o.]XBqQZ.*O -ׅf^;X05Њ*@-brixl=ET!estxIVih[˗ M`urgdщlX˟}ͺ>AM4K[%#&*7FX08?dgmo]4r(CD/$%o*i)>ќ~ 080K[-LZrigҽ>4~ځ\ˣ#H>5X%{9&GFcX */T,`ā [ݙi~sesv;e7_Ű`;ΚvC~a&GzȋO;YpaѺtw/]T|r l?^YPRo U4q 8 0`|a;EgFp|y~ќE6[QG fu,aYh<~6M~ 8>!k=)|[Kf<]Kʅ#q`bagBZ[2&֭`~6kVgfŽf@ `6,GBa2#pbk8`ۿͫ6NXۢ*c]>[ 0b}iMY/-ډJvW X*'1lك5B {s )q~9;^phmz6ڡ쫰9L> F0O|1+&N5 ^po'dٟFqS~ku4O$K4E]M w){o4H¼pn_:d_;:[z-qC{ !X~0>1%$Tb>PKnn$}2~ЍI9L^?vu YN1 'Vm ]FtOxrn$ Em.4 pSW?Yzq[=F7+ ܼbvtG,m$IM+E7o/J`̎a>% 'ļ5wZMV$g"$Һ v9xlk}f/[`9.oo 1h!g10ݓG`b ~]#FKiYhq=XZC#a90`׎/dntO{nd),uk)CwВWF 5q 'X p Tk~|HR-tOy.Cx!H{{9[<1V`,)vxgRG60bUJ]]Ur ==a>pe,ލ+)`XzL82.4ֺ `~blj=t:81˟Ċ_ӯ Z /dĂY?wf`1l=w?hCdXOgUnT&v{ەexr9pVg]EoR.ߴ`CyGtN@>u"ޝ{ <*ƣ"`A0kFxTt]:(mUHo0wܹ@fty99NfҝI;6&z^zſ [Q(EjVT< lx!mm;y8tHDY n5KCDP|AWہO,%ML'*p~|Ý#V HUD(`6|ځD$5~]p)I:e ~x/xيs_oMvrQg6.T\ {8E^hq-hB߁OQq|1Pz q.Agb sy(Fah;꙯V k.ᎄ :WV *5`h f40`aCtxRg3 MM"`2VdbvvCf $[i [ŭ+Ma/$f3Ѩ!)piY1J0r$`0$٠f`z7 *W8|.LvUQQ,[ `;sU/Q f3/0jHkY^^EQE:0jWV7RbW1lбɌY Mޤ> l%e2bA/J۲nβUSg Y0f^Ձ[x98"x#`~B/ .YG&3QM4ҁw*: LƊuXyxKꤣϱ"pUڿ KfފK8pӣp֢!2zKa mBBY6ÎɌh1/v;} %6,K䫉s:$f36'.|ZT8ee'`ڼ@|[r+&:!a&36}fHr'eY#`Ff:Opu`š;y3;) X bOdfIh}j3a+f2 }ˁW$YS) ?ށ+?נo<Hq`] 1}ہ{vK쑀]\ܧts`9]ܘa(iʣ6Ćo΁]l9" ILv`E.2 v_z'XSx>r=*/#4}90Y#^o/w Kkl&/,3`${lmt`oXl@ho"0[/!t|^Tm$p<̼?o(yLM~ߑ/Q2;0;:wdN?ρ-u Vi0|^T^}4/gX^ 5y9?4P2Q2iuQ hY1n)o0{u?\fާF?M, X hY};67/R+ aĐtt+0VdZ%#o}Bn|JU\uփ"np@#t_U> :™@?~׮`BA!d09g\8)퐲$_bq zIF=l#{TIENDB`newsraft/doc/newsraft.scd000066400000000000000000000622651516312403600160140ustar00rootroot00000000000000NEWSRAFT(1) # NAME newsraft - feed reader for terminal # SYNOPSIS *newsraft* [*-f* _FILE1_] [*-c* _FILE2_] [*-d* _FILE3_] [*-l* _FILE4_] [*-e* _ACTION_] [*-v*] [*-h*] # DESCRIPTION Newsraft is a small text based program for reading syndication feeds. It obtains content from a given set of sources and lets you browse it all via one streamlined user interface. # OPTIONS *-f* _FILE_ Force *feeds* file to _FILE_. *-c* _FILE_ Force *config* file to _FILE_. *-d* _FILE_ Force *database* file to _FILE_. *-l* _FILE_ Write logs to _FILE_. *-e* _ACTION_ Execute one-time _ACTION_ from the following list: *convert-opml-to-feeds* (takes OPML from standard input)++ *convert-feeds-to-opml* (takes feeds from *feeds* file)++ *reload-all*++ *print-unread-items-count*++ *purge-abandoned* *-v* Print version information. *-h* Print usage information. # STARTER GUIDE To start using Newsraft you have to create a *feeds* file with the list of links to feeds you want to receive news from. Check out *FEEDS FILE* section for file syntax and valid paths. When *feeds* file is ready, you can launch Newsraft. There are only 4 menus you will have to deal with: _sections_, _feeds_, _items_ and _pager_. Default binds are listed in *ACTIONS* section. _Sections_ menu consists of section entries which are needed to organize feeds in groups to be able to process them in bulk. They are kind of directories for feeds. If you didn't specify any section declarations in your *feeds* file then you will get to the _feeds_ menu straightaway. _Feeds_ menu consists of feed entries. Every feed entry contains news downloaded from one specific source which you have set in *feeds* file. To update a single feed you have to select it and press *r* or *R* if you want to update all feeds. From _feeds_ menu you can get to the _items_ menu by entering some feed. _Items_ menu consists of feed item entries (i. e. single pieces of news) which you get when you update feeds in the previous menu. Every feed item entry has two switchable properties - read state and importance state. Keys to change read state: *d* to mark read, *D* to mark unread, *^D* to mark everything read. Keys to change importance state: *f* to flag important, *F* to flag unimportant. To view item's content you have to go to _pager_ menu by entering selected item. _Pager_ menu will display some details about selected item and render its content if it was provided by feed. Usually feed item entries have a links section with one link pointing to a related web page and several links that were mentioned in the item's content. You can copy these links into your clipboard with *y* key and open them in your web browser with *o* key. To target a key action to link with a specific index you have to prefix your key with this index. For example, *5y* will copy fifth link and *17o* will open seventeenth link in the web browser. You can also setup custom command bindings to execute any commands with these links. Consider this *config* file: _bind m exec mpv "%l"_++ _bind f exec feh "%l"_ With this you will be able to open any link in *mpv*(1) and *feh*(1) directly from your terminal! Isn't it awesome? It is freaking amazing! For both _sections_ menu and _feeds_ menu there is a special explore mode. You can toggle it by pressing the *tab* key. It's truly miraculous: it reveals all the news in the current context (combines news from all feeds of the current menu into one list). This mode may come in handy when you want to quickly scroll through all the news without switching between sections and feeds back and forth. And for a dessert, I'll tell you about the search functionality. You can type */* to begin search input - enter the desired query here and press *Enter*. This will open up an _items_ menu with all the items matching your query in the current context. # CONFIGURATION ## FEEDS FILE This file contains feed entries that Newsraft will display and process. There are 4 types of lines in *feeds* file. Comment lines start with _#_ character. These lines are completely ignored. For example: _# Look closely. The beautiful may be small._ Feed lines start with a URL. After at least one whitespace character, the name of the feed may be specified - it must be enclosed in double quotes. For example: _https://example.org/feed.xml "Lorem Ipsum Blog"_ Generator lines start with a command enclosed in *$()*. These act just like feed lines but instead of fetching resources from a remote server they use the output of the specified command to obtain the content. _$(cat ~/local-feed.xml) "Lorem Ipsum Blog"_ Section lines start with *@* character. After any number of whitespace characters, the name of the section must be specified. For example: _@ Software Releases_ Both feed and section lines allow you to set individual settings and binds for them. The syntax is as follows: _@ Lorem Ipsum < reload-period 1440_++ _http://example.org/feed1.xml "Dolor Sit" < reload-period 60; item-limit 500_++ _http://example.org/feed2.xml "Id Est" < bind b mark-read; exec book.sh "%l"_ Settings set for feeds take precedence over the settings specified for sections. Not every setting supports individual assignment - only settings with asterisk *(\*)* on them do (see *SETTINGS* section). Search precedence: . _$XDG_CONFIG_HOME_/newsraft/feeds . _$HOME_/.config/newsraft/feeds . _$HOME_/.newsraft/feeds ## CONFIG FILE This file is used to override default settings and bindings of Newsraft. Presence of *config* file is totally optional and Newsraft will work without it just fine. There are 3 types of lines in *config* file. Comment lines start with _#_ character. These lines are completely ignored. For example: _# Good design is as little design as possible._ Setting lines start with a setting name and end with a setting value. Available settings are listed in the *SETTINGS* and *COLOR SETTINGS* sections. Here are a couple of examples: *scrolloff* _5000_++ *list-entry-date-format* _"%D"_++ *feeds-menu-paramount-explore* _true_ Binding lines start with the *bind* word. They define actions that are performed when certain keys are pressed. Complete list of available actions can be found in the *ACTIONS* section. Here's an example: *bind* r _reload-all_ The special _exec_ action makes it possible to run shell commands when the bound key is pressed. Specifiers within the command are replaced with values corresponding to the currently selected entry as per *menu-item-entry-format*: *bind* m _exec_ setsid mpv --terminal=no "%l" & The _edit_ action runs a specified SQL query on the *database*, so please be careful! _@selected_ specifier is replaced with a condition which identifies the currently selected entry - make sure to include it if you want to target individual item/feed rather than the whole database: *bind* w _edit_ UPDATE items SET user_data = json_set(IFNULL(user_data, '{}'), '$.toWatch', 1) WHERE _@selected_++ *bind* W _edit_ UPDATE items SET user_data = json_set(IFNULL(user_data, '{}'), '$.toWatch', 0) WHERE _@selected_ Use the _find_ action to retrieve items based on a specified SQL condition in the current context. For instance, to search for items marked as toWatch (as shown in the previous example), one can use the bindings like the following: *bind* f _find_ user_data LIKE '%"toWatch":1%'++ *bind* f _find_ json_extract(user_data, '$.toWatch') = 1 Binding lines can fit multiple actions in a single key. These actions will be executed in the order in which they are specified. Assigned actions must be separated with semicolon (;) characters, for example: *bind* key _action_; _exec_ command; _action_; _edit_ query; _action_ In case you want to disable some binding which was set in Newsraft by default, you can use a line according to this format: *unbind* key Search precedence: . _$XDG_CONFIG_HOME_/newsraft/config . _$HOME_/.config/newsraft/config . _$HOME_/.newsraft/config ## DATABASE FILE This file stores everything you download from feeds in *sqlite3*(1) format. Although you now know the format in which the data is stored, it is highly recommended to avoid modifying the database manually - things will break and it will be very sad. Search precedence: . _$XDG_DATA_HOME_/newsraft/newsraft.sqlite3 . _$HOME_/.local/share/newsraft/newsraft.sqlite3 . _$HOME_/.newsraft/newsraft.sqlite3 # SETTINGS Settings with asterisk *(\*)* on them can be set for individual feeds and sections. ## reload-period (*) Default: _0_. Feed auto reload period in minutes. If set to _0_, no auto reloads will be run. ## suppress-errors (*) Default: _false_. If _true_, feed update error indication in the menu will be disabled. It's recommended to set this setting only for specific feeds that are expected to fail frequently and you are tired of seeing their errors. ## item-rule (*) Default: _""_. Item search condition when accessing database. This can be very useful in managing feeds with a heavy spam flow: you set a condition based on some parameters and only those entries that meet this condition will be shown in the feed. It's specified in SQL format. It probably only makes sense to set this setting for individual feeds, and not globally (see *FEEDS FILE* section to understand how). Available parameters: |[ _guid_ :[ (string) :< Globally unique identifier of the article which is expected to be unique within the originating feed |[ _title_ :[ (string) :[ Title of the article |[ _link_ :[ (string) :[ URL of the article |[ _content_ :[ (string) :[ Content of the article which is stored exactly as it appears in the feed, with all original HTML preserved |[ _attachments_ :[ (string) :[ Serialized string of all attachments with URL links, MIME types and byte sizes |[ _persons_ :[ (string) :[ Serialized string of all people related to the article with names and email addresses |[ _download_date_ :[ (integer) :[ Timestamp of download date in seconds since 1970 |[ _publication_date_ :[ (integer) :[ Timestamp of publication date in seconds since 1970 |[ _update_date_ :[ (integer) :[ Timestamp of update date in seconds since 1970 |[ _user_data_ :[ (string) :[ User-defined data for the item. It can be modified using the *edit* action (see *CONFIG FILE* section) Here are some examples of correct setting values: _title NOT LIKE '%Rust%'_++ _persons LIKE '%PHARMACIST%' OR persons LIKE '%OFFL1NX%'_++ _attachments LIKE '%audio/mp3%' OR attachments LIKE '%video/mp4%'_++ _CAST(strftime('%Y', publication_date, 'unixepoch') AS INTEGER) >= 2024_++ _NOT (title REGEXP '.+:.+\\".+\\".+by' OR title LIKE '%(ultra slowed)%' OR title LIKE '%(1 hour version)%')_ REGEXP operator uses POSIX Extended Regular Expression syntax, see *regex*(7). ## item-limit (*) Default: _0_. Maximum number of items stored in a feed. If set to _0_, no limit will be set. ## item-limit-unread (*) Default: _true_. If _true_, *item-limit* setting will also cap unread items. ## item-limit-important (*) Default: _false_. If _true_, *item-limit* setting will also cap important items. ## scrolloff Default: _0_. Minimal number of list menu entries to keep above and below the selected entry. If you set it to a very large value the selected entry will always be in the middle of the list menu (except for start and end of the list menu). ## scrollwrap Default: _false_. If _true_, moving down while on the last item in a list will wrap around to the top and vice versa. ## pager-width (*) Default: _100_. Pager width in characters. If set to _0_, the pager will take up all available space. ## pager-centering (*) Default: _true_. If _true_ and *pager-width* is not _0_, pager will center its content horizontally. ## menu-item-sorting Default: _time-desc_. Sorting order for the items menu. Available values: _time-desc_, _time-asc_, _time-download-desc_, _time-download-asc_, _time-publication-desc_, _time-publication-asc_, _time-update-desc_, _time-update-asc_, _rowid-desc_, _rowid-asc_, _unread-desc_, _unread-asc_, _important-desc_, _important-asc_, _alphabet-desc_, _alphabet-asc_. ## menu-feed-sorting Default: _none_. Sorting order for the feeds menu. Available values: _unread-desc_, _unread-asc_, _alphabet-desc_, _alphabet-asc_. ## menu-section-sorting Default: _none_. Sorting order for the sections menu. Available values: _unread-desc_, _unread-asc_, _alphabet-desc_, _alphabet-asc_. ## menu-responsiveness Default: _true_. If _true_, update menu contents as soon as possible. If _false_, the menu will be updated only when you re-open it. ## open-in-browser-command (*) Default: _auto_. Shell command for *open-in-browser* action. If set to _auto_, most operating systems will get _${BROWSER:-xdg-open} "%l"_ while macOS users have it set to _open "%l"_. ## copy-to-clipboard-command Default: _auto_. Shell command for copying text to clipboard. All copied data is piped into the standard input of the specified command. If set to _auto_, Newsraft will determine the appropriate command based on the user environment: - _"wl-copy"_ if environment variable WAYLAND_DISPLAY is set - _"xclip -selection clipboard"_ if environment variable DISPLAY is set - _"pbcopy"_ if Newsraft was built for macOS operating system - _"newsraft-osc-52"_ otherwise _"newsraft-osc-52"_ is a special value that doesn't run an external command but instead triggers the OSC 52 escape sequence, instructing the terminal to copy data directly to the system clipboard. This behavior is especially useful when running Newsraft over *ssh*(1), as it allows clipboard operations to affect the local system rather than the remote one. ## notification-command (*) Default: _auto_. Shell command for invoking system notifications about new news received. If set to _auto_, Newsraft will determine the appropriate command based on the user environment: - _"notify-send 'Newsraft brought %q news!'"_ if WAYLAND_DISPLAY or DISPLAY is set in environment - _"osascript -e 'display notification "Newsraft brought %q news!"'"_ if Newsraft was built for macOS operating system - _"printf '\\e]9;Newsraft brought %q news!\\a'"_ (OSC 9 escape sequence) otherwise ## proxy (*) Default: _""_. Sets the proxy to use for the network requests. It must be either a hostname or dotted numerical IPv4 address. To specify IPv6 address you have to enclose it within square brackets. Port number can be set by appending :PORT to the end of setting value. By default proxy protocol is considered HTTP, but you can set a different one by prepending SCHEME:// to the setting value. ## proxy-user (*) Default: _""_. User for authentication with the proxy server. ## proxy-password (*) Default: _""_. Password for authentication with the proxy server. ## global-section-name Default: _Global_. Name of the section that contains all feeds. ## global-section-hide Default: _false_. If _true_, global section will not be shown. ## status-show-menu-path Default: _true_. If _true_, print menu path in the status bar. ## status-placeholder Default: _r:reload R:reload-all tab:explore d:read D:unread f:important F:unimportant n:next-unread N:prev-unread p:next-important P:prev-important_. Placeholder which is put in the status bar if it's empty. ## item-content-format (*) Default: _Feed:  %f
|Title: %t
|Date:  %d
|
%c
|

%L_. Sets the HTML format according to which the item's content will be generated. Fields are separated by _|_ character and ONLY one specifier can be placed in each field. If an item doesn't have a value corresponding to the specifier in the field, then the entire field will not be shown. Specifiers are as follows: _f_ feed title if set, feed link otherwise;++ _t_ item title;++ _l_ item link;++ _d_ item date;++ _a_ item authors;++ _c_ item content;++ _L_ item links list. ## item-content-date-format (*) Default: _%a, %d %b %Y %H:%M:%S %z_. Date format in the item's content. Specifier values correspond to the *strftime*(3) format. ## item-content-link-format (*) Default: _[%i]: %l
_. Link format in the links list of item's content. _%i_ and _%l_ will be replaced by link index and link address respectively. ## list-entry-date-format Default: _%b %d_. Date format of the list entries. Specifier values correspond to the *strftime*(3) format. ## menu-section-entry-format Default: _%5.0u @ %t_. Format of the section list entries. Specifiers are as follows: _i_ index number;++ _u_ unread items count;++ _t_ section title. ## menu-feed-entry-format Default: _%5.0u │ %t_. Format of the feed list entries. Specifiers are as follows: _i_ index number;++ _u_ unread items count;++ _n_ total items count;++ _l_ feed link;++ _t_ feed name if set, feed link otherwise. ## menu-item-entry-format Default: _" %u │ %d │ %o"_. Format of the item list entries. Specifiers are as follows: _i_ index number;++ _u_ "N" if item is unread, " " otherwise;++ _d_ update date formatted according to *list-entry-date-format*;++ _D_ publication date formatted according to *list-entry-date-format*;++ _l_ item link;++ _t_ item title;++ _o_ item title if set, item link otherwise;++ _L_ feed link;++ _T_ feed title;++ _O_ feed title if set, feed link otherwise. ## menu-explore-item-entry-format Default: _" %u │ %d │ %-28O │ %o"_. Format of the item list entries in explore mode. Specifiers are the same as in *menu-item-entry-format*. ## sections-menu-paramount-explore Default: _false_. Enables explore mode in sections menu by default. ## feeds-menu-paramount-explore Default: _false_. Enables explore mode in feeds menu by default. ## read-on-arrival (*) Default: _false_. Mark new items as read automatically. ## mark-item-unread-on-change (*) Default: _false_. Mark every item that changes on a feed update as unread. ## mark-item-read-on-hover (*) Default: _false_. Mark every item that gets selected as read. ## database-batch-transactions Default: _true_. Apply all changes to the *database* file in one big transaction after all feed updates have finished instead of using a separate transaction for each feed update. This improves update performance a lot with the downside that fetched content will not be saved if you quit Newsraft before update finishes. ## database-analyze-on-startup Default: _true_. Run "ANALYZE" SQLite command on the database every time you start Newsraft. It gathers statistics about database and uses it to optimize some queries making runtime faster. ## database-clean-on-startup Default: _false_. Run "VACUUM" SQLite command on the database every time you start Newsraft. It rebuilds the database file by packing it into a minimal amount of disk space. This can significantly increase startup time. ## download-timeout (*) Default: _20_. Maximum time in seconds that you allow Newsraft to download one feed. Setting to _0_ disables the timeout. ## download-speed-limit (*) Default: _0_. Maximum download speed in kilobytes per second (kB/s). Setting to _0_ disables the limit. ## download-max-connections Default: _500_. Maximum amount of simultaneously open connections Newsraft may hold in total. If set to _0_, there is no limit. You can try to increase this value or even set it to _0_ if you want to squeeze out all performance to the last drop but be aware that things can start to break at high setting values. One obvious example is getaddrinfo() starts to choke with a large number of simultaneous requests trying to resolve domain names. ## download-max-host-connections Default: _0_. Maximum amount of simultaneously open connections Newsraft may hold a single host. If set to _0_, there is no limit. ## user-agent (*) Default: _auto_. User-Agent header sent with download requests. If set to _auto_, Newsraft will generate it according to the following format: _"newsraft/"_ + NEWSRAFT_VERSION + _" ("_ + OS_NAME + _")"_ OS_NAME shouldn't be a matter of privacy concern, because on most systems it contains nothing more like _"Linux"_ or _"Darwin"_. If you want to be sure of this, check Newsraft log to see how *user-agent* is set at startup. If set to _""_, User-Agent header will not be sent. ## respect-ttl-element (*) Default: _true_. Prevents too frequent updates for some feeds. The limit is set by the creators of the feeds in order to save traffic and resources for a very rarely updated feeds. Disabling it is strongly discouraged. ## respect-expires-header (*) Default: _true_. Prevents feed updates until the expiration date of the previously downloaded information in order to save traffic and resources. Disabling it is strongly discouraged. ## send-if-none-match-header (*) Default: _true_. Sends an entity tag corresponding to the previously downloaded information. If the server from which the feed is downloaded contains information with the same tag, then in order to save traffic and resources, it will reject the download request. Disabling it is strongly discouraged. ## send-if-modified-since-header (*) Default: _true_. Sends a date corresponding to the last modification of previously downloaded information. If the server from which the feed is downloaded contains information with the same modification date, then in order to save traffic and resources, it will reject the download request. Disabling it is strongly discouraged. ## ignore-no-color Default: _false_. If _true_, Newsraft will use colors regardless of whether *NO_COLOR* environment variable is present or not. # COLOR SETTINGS Color settings are the same settings as above, but they take two color words (foreground and background) and optional attribute words. Available colors are _default_, _black_, _red_, _green_, _yellow_, _blue_, _magenta_, _cyan_, _white_ and _colorN_ (_N_ can be a number from _0_ to _255_). Available attributes are _bold_, _italic_ and _underlined_. |[ Color setting :[ Default value |[ *color-status* :[ _green default bold_ |[ *color-status-info* :[ _cyan default bold_ |[ *color-status-fail* :[ _red default bold_ |[ *color-list-item* :[ _default default_ |[ *color-list-item-selected* :[ (not set) |[ *color-list-item-unread* :[ _yellow default_ |[ *color-list-item-important* :[ _magenta default_ |[ *color-list-feed* :[ _default default_ |[ *color-list-feed-selected* :[ (not set) |[ *color-list-feed-unread* :[ _yellow default_ |[ *color-list-feed-failed* :[ _red default_ |[ *color-list-section* :[ _default default_ |[ *color-list-section-selected* :[ (not set) |[ *color-list-section-unread* :[ _yellow default_ |[ *color-list-section-failed* :[ _red default_ # ACTIONS |[ Actions :[ Keys |[ *select-next* :[ _j_, _KEY_DOWN_, _^E_ |[ *select-prev* :[ _k_, _KEY_UP_, _^Y_ |[ *select-next-page* :[ _space_, _^F_, _KEY_NPAGE_ |[ *select-next-page-half* :[ _^D_ |[ *select-prev-page* :[ _^B_, _KEY_PPAGE_ |[ *select-prev-page-half* :[ _^U_ |[ *select-first* :[ _g_, _KEY_HOME_ |[ *select-last* :[ _G_, _KEY_END_ |[ *jump-to-next* :[ _J_ |[ *jump-to-prev* :[ _K_ |[ *jump-to-next-unread* :[ _n_ |[ *jump-to-prev-unread* :[ _N_ |[ *jump-to-next-important* :[ _p_ |[ *jump-to-prev-important* :[ _P_ |[ *next-error* :[ _e_ |[ *prev-error* :[ _E_ |[ *goto-feed* :[ _\*_ |[ *shift-west* :[ _,_ |[ *shift-east* :[ _._ |[ *shift-reset* :[ _<_ |[ *sort-by-time* :[ _t_ |[ *sort-by-time-download* :[ (not set) |[ *sort-by-time-publication* :[ (not set) |[ *sort-by-time-update* :[ (not set) |[ *sort-by-rowid* :[ _w_ |[ *sort-by-unread* :[ _u_ |[ *sort-by-initial* :[ _z_ |[ *sort-by-alphabet* :[ _a_ |[ *sort-by-important* :[ _i_ |[ *enter* :[ _enter_, _l_, _KEY_ENTER_, _KEY_RIGHT_ |[ *reload* :[ _r_ |[ *reload-all* :[ _R_, _^R_ |[ *mark-read*; *jump-to-next* :[ _d_ |[ *mark-unread*; *jump-to-next* :[ _D_ |[ *mark-read-all* :[ _A_ |[ *mark-unread-all* :[ (not set) |[ *mark-important* :[ _f_ |[ *mark-unimportant* :[ _F_ |[ *toggle-read* :[ (not set) |[ *toggle-important* :[ (not set) |[ *toggle-explore-mode* :[ _tab_ |[ *view-errors* :[ _v_ |[ *open-in-browser* :[ _o_ |[ *copy-to-clipboard* :[ _y_, _c_ |[ *start-search-input* :[ _/_ |[ *clean-status* :[ _`_ |[ *navigate-back* :[ _h_, _backspace_, _KEY_LEFT_, _KEY_BACKSPACE_ |[ *quit* :[ _q_ |[ *quit-hard* :[ _Q_ |[ *exec man newsraft* :[ _?_ You can also bind _escape_ key, but this can potentially lead to incomplete processing of escape sequences in the terminal on a very fast input. This is due to the special role of the _escape_ character under the hood of terminals. Your mileage may vary. # FORMATS SUPPORT Data formats of feeds which Newsraft recognizes. Not the whole functionality of these formats is implemented, but only the functionality that is most likely to carry the most essential information. _RSS 2.0_, _1.1_, _1.0_, _0.94_, _0.93_, _0.92_, _0.91_, _0.9_++ _Atom 1.0_++ _RSS Content Module_++ _Media RSS_++ _DublinCore 1.1 Elements_++ _JSON Feed_ # ENVIRONMENT Newsraft's behavior depends on the environment variables set, however not all environment variables affect Newsraft directly - many environment variables affect libraries that Newsraft is built upon. For example, *libcurl*(3) recognizes a large number of different environment variables which you can learn more about on *libcurl-env*(3). |[ *XDG_CONFIG_HOME* :< Directory in which user-specific configuration files are stored. |[ *XDG_DATA_HOME* :[ Directory in which user-specific data files are stored. |[ *HOME* :[ User home directory. |[ *BROWSER* :[ User web browser. |[ *WAYLAND_DISPLAY* :[ Identifier of the Wayland graphics display. |[ *DISPLAY* :[ Identifier of the X graphics display. |[ *NO_COLOR* :[ Makes the interface monochrome when present. # SEE ALSO *mpv*(1), *feh*(1), *sqlite3*(1), *regex*(7), *ssh*(1), *strftime*(3), *libcurl*(3), *libcurl-env*(3) # BUGS Don't be ridiculous... # AUTHORS Grigory Kirillov and contributors newsraft/doc/newsraft.svg000066400000000000000000001365241516312403600160420ustar00rootroot00000000000000newsraft/makefile000066400000000000000000000062721516312403600144170ustar00rootroot00000000000000.POSIX: .PHONY: all install install-newsraft install-man install-icon install-desktop install-examples man html clean check gperf cppcheck clang-tidy CC = cc CFLAGS = -O3 LDFLAGS = CURL_CFLAGS = `pkg-config --cflags libcurl 2>/dev/null` CURL_LIBS = `pkg-config --libs libcurl 2>/dev/null || echo '-lcurl'` EXPAT_CFLAGS = `pkg-config --cflags expat 2>/dev/null` EXPAT_LIBS = `pkg-config --libs expat 2>/dev/null || echo '-lexpat'` GUMBO_CFLAGS = `pkg-config --cflags gumbo 2>/dev/null` GUMBO_LIBS = `pkg-config --libs gumbo 2>/dev/null || echo '-lgumbo'` SQLITE_CFLAGS = `pkg-config --cflags sqlite3 2>/dev/null` SQLITE_LIBS = `pkg-config --libs sqlite3 2>/dev/null || echo '-lsqlite3'` PTHREAD_LIBS = -lpthread # for static linking #LDFLAGS = -static #CURL_LIBS = -lcurl -lbrotlidec -lbrotlienc -lbrotlicommon -lssl -lcrypto -lnghttp2 -lz AUXCFLAGS = $(CURL_CFLAGS) $(EXPAT_CFLAGS) $(GUMBO_CFLAGS) $(SQLITE_CFLAGS) FEATURECFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -D_BSD_SOURCE -D_DARWIN_C_SOURCE LDLIBS = $(CURL_LIBS) $(EXPAT_LIBS) $(GUMBO_LIBS) $(SQLITE_LIBS) $(PTHREAD_LIBS) DESTDIR = PREFIX = /usr/local BINDIR = $(PREFIX)/bin MANDIR = $(PREFIX)/share/man ICONSDIR = $(PREFIX)/share/icons/hicolor/scalable/apps DESKTOPDIR = $(PREFIX)/share/applications EXAMPLES_DIR = $(PREFIX)/share/newsraft/examples all: newsraft install: install-newsraft install-man install-icon install-examples install-newsraft: mkdir -p $(DESTDIR)$(BINDIR) install -m755 newsraft $(DESTDIR)$(BINDIR)/. install-man: mkdir -p $(DESTDIR)$(MANDIR)/man1 install -m644 doc/newsraft.1 $(DESTDIR)$(MANDIR)/man1/. install-icon: mkdir -p $(DESTDIR)$(ICONSDIR) install -m644 doc/newsraft.svg $(DESTDIR)$(ICONSDIR)/. install-desktop: mkdir -p $(DESTDIR)$(DESKTOPDIR) install -m644 doc/newsraft.desktop $(DESTDIR)$(DESKTOPDIR)/. install-examples: mkdir -p $(DESTDIR)$(EXAMPLES_DIR) install -m644 doc/examples/feeds $(DESTDIR)$(EXAMPLES_DIR)/. install -m644 doc/examples/config $(DESTDIR)$(EXAMPLES_DIR)/. newsraft: $(CC) -std=c99 $(CFLAGS) $(AUXCFLAGS) $(FEATURECFLAGS) -Isrc $(LDFLAGS) -o $@ src/newsraft.c $(LDLIBS) libnewsraft.so: $(CC) -std=c99 -shared $(CFLAGS) $(AUXCFLAGS) $(FEATURECFLAGS) -Isrc $(LDFLAGS) -o $@ src/newsraft.c $(LDLIBS) test-program: $(CC) -std=c99 $(CFLAGS) $(AUXCFLAGS) $(FEATURECFLAGS) -Isrc -o newsraft-test $(TEST_FILE) -L. -lnewsraft gperf: gperf -m 1000 -I -t -F ,0,NULL,NULL < src/parse_xml/gperf-data.in > src/parse_xml/gperf-data.c man: scdoc < doc/newsraft.scd > doc/newsraft.1 html: mandoc -T html ./doc/newsraft.1 > doc/newsraft.html sed -i 's///' doc/newsraft.html check: ./tests/run-check.sh clean: rm -rf newsraft newsraft-test newsraft-test-log newsraft-test-feeds newsraft-test-database* libnewsraft.so flog vlog cppcheck: find src -name "*.c" -exec cppcheck -q --enable=warning,performance,portability '{}' ';' clang-tidy: clang-tidy --checks='-clang-analyzer-security.insecureAPI.*' $$(find src -name '*.c') -- $(FEATURECFLAGS) -Isrc compile_commands.json: clean bear -- $(MAKE) newsraft/src/000077500000000000000000000000001516312403600134775ustar00rootroot00000000000000newsraft/src/alloc.c000066400000000000000000000007351516312403600147420ustar00rootroot00000000000000#include #include "newsraft.h" void * newsraft_malloc(size_t size) { void *ptr = malloc(size); if (ptr == NULL) { abort(); } return ptr; } void * newsraft_calloc(size_t n, size_t size) { void *ptr = calloc(n, size); if (ptr == NULL) { abort(); } return ptr; } void * newsraft_realloc(void *ptr, size_t size) { void *new_ptr = realloc(ptr, size); if (new_ptr == NULL) { abort(); } return new_ptr; } void newsraft_free(void *ptr) { free(ptr); } newsraft/src/binds.c000066400000000000000000000075031516312403600147470ustar00rootroot00000000000000#include #include #include #include "newsraft.h" #define INPUT_ARRAY #include "input.h" #undef INPUT_ARRAY static struct input_binding *binds = NULL; static bool was_escape_key_ever_bound = false; input_id get_action_of_bind(struct input_binding *ctx, const char *key, size_t action_index, const struct wstring **p_arg) { if (key != NULL) { struct input_binding *pool[] = {ctx, binds}; for (int p = 0; p < 2; ++p) { for (struct input_binding *i = pool[p]; i != NULL; i = i->next) { if (strcmp(key, i->key->ptr) == 0) { if (action_index < i->actions_count) { *p_arg = i->actions[action_index].arg; return i->actions[action_index].cmd; } return INPUT_ERROR; } } } } return INPUT_ERROR; } struct input_binding * create_or_clean_bind(struct input_binding **target, const char *key) { if (target == NULL) { target = &binds; } for (struct input_binding *i = *target; i != NULL; i = i->next) { if (strcmp(key, i->key->ptr) == 0) { for (size_t j = 0; j < i->actions_count; ++j) { free_wstring(i->actions[j].arg); } free(i->actions); i->actions = NULL; i->actions_count = 0; return i; } } struct input_binding *new = newsraft_calloc(1, sizeof(struct input_binding)); new->key = crtas(key, strlen(key)); new->next = *target; *target = new; if (strcmp(key, "escape") == 0) { WARN("Escape key is used for key binding!"); was_escape_key_ever_bound = true; } return *target; } bool attach_action_to_bind(struct input_binding *bind, input_id cmd, const char *arg, size_t arg_len) { bind->actions = newsraft_realloc(bind->actions, sizeof(struct binding_action) * (bind->actions_count + 1)); bind->actions[bind->actions_count].cmd = cmd; bind->actions[bind->actions_count].arg = NULL; if (arg && arg_len > 0) { bind->actions[bind->actions_count].arg = convert_array_to_wstring(arg, arg_len); if (bind->actions[bind->actions_count].arg == NULL) { return false; } } bind->actions_count += 1; INFO("Attached action: %14s, %zu, %2u, %s", bind->key->ptr, bind->actions_count, cmd, arg ? arg : "(none)"); return true; } static bool bind_exec(const char *key, const char *cmd) { struct input_binding *bind = create_or_clean_bind(NULL, key); return attach_action_to_bind(bind, INPUT_SYSTEM_COMMAND, cmd, strlen(cmd)); } static bool bind_two_actions(const char *key, input_id action1, input_id action2) { struct input_binding *bind = create_or_clean_bind(NULL, key); return attach_action_to_bind(bind, action1, NULL, 0) && attach_action_to_bind(bind, action2, NULL, 0); } bool assign_default_binds(void) { for (size_t i = 0; inputs[i].names[0] != NULL; ++i) { for (size_t j = 0; inputs[i].default_binds[j] != NULL; ++j) { struct input_binding *bind = create_or_clean_bind(NULL, inputs[i].default_binds[j]); if (!attach_action_to_bind(bind, i, NULL, 0)) { return false; } } } if (!bind_exec("?", "man newsraft")) { return false; } if (!bind_two_actions("d", INPUT_MARK_READ, INPUT_JUMP_TO_NEXT)) { return false; } if (!bind_two_actions("D", INPUT_MARK_UNREAD, INPUT_JUMP_TO_NEXT)) { return false; } return true; } input_id get_input_id_by_name(const char *name) { for (size_t i = 0; inputs[i].names[0] != NULL; ++i) { for (size_t j = 0; inputs[i].names[j] != NULL; ++j) { if (strcmp(name, inputs[i].names[j]) == 0) { return i; } } } write_error("Action \"%s\" doesn't exist!\n", name); return INPUT_ERROR; } bool is_escape_key_used(void) { return was_escape_key_ever_bound; } void free_binds(struct input_binding *target) { for (struct input_binding *i = target, *tmp = target; tmp != NULL; i = tmp) { free_string(i->key); for (size_t j = 0; j < i->actions_count; ++j) { free_wstring(i->actions[j].arg); } free(i->actions); tmp = i->next; free(i); } } void free_default_binds(void) { free_binds(binds); } newsraft/src/commands.c000066400000000000000000000034501516312403600154460ustar00rootroot00000000000000#include #include #include "newsraft.h" static inline void execute_system_command(const char *cmd) { info_status("Executing %s", cmd); pthread_mutex_lock(&interface_lock); NEWSRAFT_UI(ui_term()); int status = system(cmd); fflush(stdout); fflush(stderr); NEWSRAFT_UI(ui_init()); pthread_mutex_unlock(&interface_lock); // Resizing could be handled by the program running on top, so we have to catch up. if (ui_is_running() && call_resize_handler_if_current_list_menu_size_is_different_from_actual() == false) { pthread_mutex_lock(&interface_lock); tb_clear(); tb_present(); status_recreate_unprotected(); redraw_list_menu_unprotected(); pthread_mutex_unlock(&interface_lock); } if (status == 0) { status_clean(); } else { fail_status("Failed with status %d to run %s", status, cmd); } } void copy_string_to_clipboard(const struct string *src) { if (src != NULL && src->len > 0) { const struct string *cmd = get_cfg_string(NULL, CFG_COPY_TO_CLIPBOARD_COMMAND); if (strcmp(cmd->ptr, "newsraft-osc-52") == 0) { struct string *encoded_src = newsraft_base64_encode((uint8_t *)src->ptr, src->len); printf("\x1b]52;c;%s\x07", encoded_src->ptr); fflush(stdout); free_string(encoded_src); } else { FILE *p = popen(cmd->ptr, "w"); if (p == NULL) { fail_status("Failed to execute clipboard command!"); return; } fwrite(src->ptr, sizeof(char), src->len, p); pclose(p); } info_status("Copied %s", src->ptr); } } void run_formatted_command(const struct wstring *wcmd_fmt, const struct format_arg *args) { struct wstring *fmtout = wcrtes(200); do_format(fmtout, wcmd_fmt->ptr, args); struct string *cmd = convert_wstring_to_string(fmtout); if (cmd != NULL) { execute_system_command(cmd->ptr); free_string(cmd); } free_wstring(fmtout); } newsraft/src/config.h000066400000000000000000000210721516312403600151170ustar00rootroot00000000000000#ifndef CONFIG_H #define CONFIG_H typedef uint8_t config_type_id; enum config_type { CFG_BOOL, CFG_UINT, CFG_COLOR, CFG_STRING, }; struct config_color { uintattr_t fg; uintattr_t bg; uintattr_t attributes; }; struct config_string { const char *const base; struct string *actual; struct wstring *wactual; bool (*auto_set)(struct config_context **, config_type_id); }; union config_value { bool b; size_t u; struct config_color c; struct config_string s; }; struct config_entry { const char *name; config_type_id type; union config_value value; }; struct config_context { config_entry_id id; struct config_entry cfg; struct config_context *next; }; #define COLOR_TO_BIT(X) (1 << (X)) #define CFG(NAME, ...) NAME, enum { #endif // CONFIG_H #ifdef CONFIG_ARRAY #define CFG(NAME, ...) [NAME] = {__VA_ARGS__}, static struct config_entry config[] = { #endif // CONFIG_ARRAY CFG(CFG_COLOR_STATUS, "color-status", CFG_COLOR, {.c = {TB_GREEN, TB_DEFAULT, TB_BOLD}}) CFG(CFG_COLOR_STATUS_INFO, "color-status-info", CFG_COLOR, {.c = {TB_CYAN, TB_DEFAULT, TB_BOLD}}) CFG(CFG_COLOR_STATUS_FAIL, "color-status-fail", CFG_COLOR, {.c = {TB_RED, TB_DEFAULT, TB_BOLD}}) CFG(CFG_COLOR_LIST_ITEM, "color-list-item", CFG_COLOR, {.c = {TB_DEFAULT, TB_DEFAULT, TB_DEFAULT}}) CFG(CFG_COLOR_LIST_ITEM_SELECTED, "color-list-item-selected", CFG_COLOR, {.c = {~0, ~0, ~0}}) CFG(CFG_COLOR_LIST_ITEM_UNREAD, "color-list-item-unread", CFG_COLOR, {.c = {TB_YELLOW, TB_DEFAULT, TB_DEFAULT}}) CFG(CFG_COLOR_LIST_ITEM_IMPORTANT, "color-list-item-important", CFG_COLOR, {.c = {TB_MAGENTA, TB_DEFAULT, TB_DEFAULT}}) CFG(CFG_COLOR_LIST_FEED, "color-list-feed", CFG_COLOR, {.c = {TB_DEFAULT, TB_DEFAULT, TB_DEFAULT}}) CFG(CFG_COLOR_LIST_FEED_SELECTED, "color-list-feed-selected", CFG_COLOR, {.c = {~0, ~0, ~0}}) CFG(CFG_COLOR_LIST_FEED_UNREAD, "color-list-feed-unread", CFG_COLOR, {.c = {TB_YELLOW, TB_DEFAULT, TB_DEFAULT}}) CFG(CFG_COLOR_LIST_FEED_FAILED, "color-list-feed-failed", CFG_COLOR, {.c = {TB_RED, TB_DEFAULT, TB_DEFAULT}}) CFG(CFG_COLOR_LIST_SECTION, "color-list-section", CFG_COLOR, {.c = {TB_DEFAULT, TB_DEFAULT, TB_DEFAULT}}) CFG(CFG_COLOR_LIST_SECTION_SELECTED, "color-list-section-selected", CFG_COLOR, {.c = {~0, ~0, ~0}}) CFG(CFG_COLOR_LIST_SECTION_UNREAD, "color-list-section-unread", CFG_COLOR, {.c = {TB_YELLOW, TB_DEFAULT, TB_DEFAULT}}) CFG(CFG_COLOR_LIST_SECTION_FAILED, "color-list-section-failed", CFG_COLOR, {.c = {TB_RED, TB_DEFAULT, TB_DEFAULT}}) CFG(CFG_RELOAD_PERIOD, "reload-period", CFG_UINT, {.u = 0 }) CFG(CFG_ITEM_LIMIT, "item-limit", CFG_UINT, {.u = 0 }) CFG(CFG_SCROLLOFF, "scrolloff", CFG_UINT, {.u = 0 }) CFG(CFG_PAGER_WIDTH, "pager-width", CFG_UINT, {.u = 100 }) CFG(CFG_DOWNLOAD_TIMEOUT, "download-timeout", CFG_UINT, {.u = 20 }) CFG(CFG_DOWNLOAD_SPEED_LIMIT, "download-speed-limit", CFG_UINT, {.u = 0 }) CFG(CFG_DOWNLOAD_MAX_CONNECTIONS, "download-max-connections", CFG_UINT, {.u = 500 }) CFG(CFG_DOWNLOAD_MAX_HOST_CONNECTIONS, "download-max-host-connections", CFG_UINT, {.u = 0 }) CFG(CFG_STATUS_PLACEHOLDER, "status-placeholder", CFG_STRING, {.s = {.base = "r:reload R:reload-all tab:explore d:read D:unread f:important F:unimportant n:next-unread N:prev-unread p:next-important P:prev-important"}}) CFG(CFG_COPY_TO_CLIPBOARD_COMMAND, "copy-to-clipboard-command", CFG_STRING, {.s = {.base = "auto", .auto_set = &obtain_clipboard_command}}) CFG(CFG_PROXY, "proxy", CFG_STRING, {.s = {.base = ""}}) CFG(CFG_PROXY_USER, "proxy-user", CFG_STRING, {.s = {.base = ""}}) CFG(CFG_PROXY_PASSWORD, "proxy-password", CFG_STRING, {.s = {.base = ""}}) CFG(CFG_USER_AGENT, "user-agent", CFG_STRING, {.s = {.base = "auto", .auto_set = &obtain_useragent_string}}) CFG(CFG_ITEM_RULE, "item-rule", CFG_STRING, {.s = {.base = ""}}) CFG(CFG_ITEM_CONTENT_FORMAT, "item-content-format", CFG_STRING, {.s = {.base = "Feed:  %f
|Title: %t
|Date:  %d
|
%c
|

%L"}}) CFG(CFG_ITEM_CONTENT_DATE_FORMAT, "item-content-date-format", CFG_STRING, {.s = {.base = "%a, %d %b %Y %H:%M:%S %z"}}) CFG(CFG_ITEM_CONTENT_LINK_FORMAT, "item-content-link-format", CFG_STRING, {.s = {.base = "[%i]: %l
"}}) CFG(CFG_LIST_ENTRY_DATE_FORMAT, "list-entry-date-format", CFG_STRING, {.s = {.base = "%b %d"}}) CFG(CFG_OPEN_IN_BROWSER_COMMAND, "open-in-browser-command", CFG_STRING, {.s = {.base = "auto", .auto_set = &obtain_browser_command}}) CFG(CFG_NOTIFICATION_COMMAND, "notification-command", CFG_STRING, {.s = {.base = "auto", .auto_set = &obtain_notification_command}}) CFG(CFG_MENU_SECTION_ENTRY_FORMAT, "menu-section-entry-format", CFG_STRING, {.s = {.base = "%5.0u @ %t"}}) CFG(CFG_MENU_FEED_ENTRY_FORMAT, "menu-feed-entry-format", CFG_STRING, {.s = {.base = "%5.0u │ %t"}}) CFG(CFG_MENU_ITEM_ENTRY_FORMAT, "menu-item-entry-format", CFG_STRING, {.s = {.base = " %u │ %d │ %o"}}) CFG(CFG_MENU_EXPLORE_ITEM_ENTRY_FORMAT, "menu-explore-item-entry-format", CFG_STRING, {.s = {.base = " %u │ %d │ %-28O │ %o"}}) CFG(CFG_MENU_SECTION_SORTING, "menu-section-sorting", CFG_STRING, {.s = {.base = "none"}}) CFG(CFG_MENU_FEED_SORTING, "menu-feed-sorting", CFG_STRING, {.s = {.base = "none"}}) CFG(CFG_MENU_ITEM_SORTING, "menu-item-sorting", CFG_STRING, {.s = {.base = "time-desc"}}) CFG(CFG_GLOBAL_SECTION_NAME, "global-section-name", CFG_STRING, {.s = {.base = "Global"}}) CFG(CFG_GLOBAL_SECTION_HIDE, "global-section-hide", CFG_BOOL, {.b = false}) CFG(CFG_SUPPRESS_ERRORS, "suppress-errors", CFG_BOOL, {.b = false}) CFG(CFG_MENU_RESPONSIVENESS, "menu-responsiveness", CFG_BOOL, {.b = true }) CFG(CFG_ITEM_LIMIT_UNREAD, "item-limit-unread", CFG_BOOL, {.b = true }) CFG(CFG_ITEM_LIMIT_IMPORTANT, "item-limit-important", CFG_BOOL, {.b = false}) CFG(CFG_STATUS_SHOW_MENU_PATH, "status-show-menu-path", CFG_BOOL, {.b = true }) CFG(CFG_SECTIONS_MENU_PARAMOUNT_EXPLORE, "sections-menu-paramount-explore", CFG_BOOL, {.b = false}) CFG(CFG_FEEDS_MENU_PARAMOUNT_EXPLORE, "feeds-menu-paramount-explore", CFG_BOOL, {.b = false}) CFG(CFG_READ_ON_ARRIVAL, "read-on-arrival", /* ha, funny */ CFG_BOOL, {.b = false}) CFG(CFG_MARK_ITEM_UNREAD_ON_CHANGE, "mark-item-unread-on-change", CFG_BOOL, {.b = false}) CFG(CFG_MARK_ITEM_READ_ON_HOVER, "mark-item-read-on-hover", CFG_BOOL, {.b = false}) CFG(CFG_DATABASE_BATCH_TRANSACTIONS, "database-batch-transactions", CFG_BOOL, {.b = true }) CFG(CFG_DATABASE_ANALYZE_ON_STARTUP, "database-analyze-on-startup", CFG_BOOL, {.b = true }) CFG(CFG_DATABASE_CLEAN_ON_STARTUP, "database-clean-on-startup", CFG_BOOL, {.b = false}) CFG(CFG_RESPECT_TTL_ELEMENT, "respect-ttl-element", CFG_BOOL, {.b = true }) CFG(CFG_RESPECT_EXPIRES_HEADER, "respect-expires-header", CFG_BOOL, {.b = true }) CFG(CFG_SEND_IF_NONE_MATCH_HEADER, "send-if-none-match-header", CFG_BOOL, {.b = true }) CFG(CFG_SEND_IF_MODIFIED_SINCE_HEADER, "send-if-modified-since-header", CFG_BOOL, {.b = true }) CFG(CFG_PAGER_CENTERING, "pager-centering", CFG_BOOL, {.b = true }) CFG(CFG_IGNORE_NO_COLOR, "ignore-no-color", CFG_BOOL, {.b = false}) CFG(CFG_SCROLLWRAP, "scrollwrap", CFG_BOOL, {.b = false}) CFG(CFG_ENTRIES_COUNT, NULL, CFG_BOOL, {.b = false}) #ifdef CFG }; #endif #undef CFG newsraft/src/curses.c000066400000000000000000000120421516312403600151460ustar00rootroot00000000000000#include "newsraft.h" #define TB_IMPL #include "termbox2.h" struct WINDOW { int pos_y; size_t offset; struct wstring *content; uintmax_t attrs; }; WINDOW * newwin(int pos_y) { WINDOW *win = newsraft_calloc(1, sizeof(*win)); win->pos_y = pos_y; win->content = wcrtes(list_menu_width + 10); return win; } void delwin(WINDOW *win) { if (win) { free_wstring(win->content); newsraft_free(win); } } void wmove(WINDOW *win, size_t offset_x) { win->offset = offset_x; } void werase(WINDOW *win) { empty_wstring(win->content); for (int i = 0; i < tb_width(); ++i) { tb_set_cell(i, win->pos_y, ' ', TB_DEFAULT, TB_DEFAULT); } } uintattr_t real_color(uintattr_t color) { if (!arent_we_colorful()) { return TB_DEFAULT; } int mode = tb_set_output_mode(TB_OUTPUT_CURRENT); if (mode == TB_OUTPUT_256) { if (color == TB_DEFAULT) { return TB_DEFAULT; } if (color == TB_BLACK) { return TB_HI_BLACK; } return color - 1; } return color; } void wbkgd(WINDOW *win, struct config_color color) { int shift = 0; for (int i = 0; i < tb_width(); ++i) { if ((size_t)i < win->content->len) { tb_set_cell(shift, win->pos_y, win->content->ptr[i], real_color(color.fg) | color.attributes | win->attrs, real_color(color.bg)); shift += wcwidth(win->content->ptr[i]); } else { tb_set_cell(shift, win->pos_y, ' ', real_color(color.fg) | color.attributes | win->attrs, real_color(color.bg)); shift += 1; } } } void wattrset(WINDOW *win, int attrs) { win->attrs = attrs; } void waddnwstr(WINDOW *win, const wchar_t *wstr, size_t lim) { size_t wstr_len = wcslen(wstr); if (wstr_len > lim) { wstr_len = lim; } for (size_t i = 0; i < wstr_len; ++i) { int width = wcwidth(wstr[i]); if (width < 1) { WARN("Invalid wide character: %d", (int)wstr[i]); continue; } if (tb_set_cell(win->offset, win->pos_y, wstr[i], win->attrs, TB_DEFAULT) != TB_OK) { break; } win->offset += width; wcatcs(win->content, wstr[i]); } } void waddwstr(WINDOW *win, const wchar_t *wstr) { waddnwstr(win, wstr, SIZE_MAX); } void waddstr(WINDOW *win, const char *str) { struct wstring *new = convert_array_to_wstring(str, strlen(str)); waddnwstr(win, new->ptr, SIZE_MAX); free_wstring(new); } int get_wch(char *keyname) { static const char *escape_keys[] = { [TB_KEY_CTRL_A] = "^A", // ok [TB_KEY_CTRL_B] = "^B", // ok [TB_KEY_CTRL_C] = "^C", // ok [TB_KEY_CTRL_D] = "^D", // ok [TB_KEY_CTRL_E] = "^E", // ok [TB_KEY_CTRL_F] = "^F", // ok [TB_KEY_CTRL_G] = "^G", // ok [TB_KEY_BACKSPACE] = "backspace", [TB_KEY_TAB] = "tab", // ok [TB_KEY_CTRL_J] = "^J", // ok [TB_KEY_CTRL_K] = "^K", // ok [TB_KEY_CTRL_L] = "^L", // ok [TB_KEY_ENTER] = "enter", // ok [TB_KEY_CTRL_N] = "^N", // ok [TB_KEY_CTRL_O] = "^O", // ok [TB_KEY_CTRL_P] = "^P", // ok [TB_KEY_CTRL_Q] = "^Q", // ok [TB_KEY_CTRL_R] = "^R", // ok [TB_KEY_CTRL_S] = "^S", // ok [TB_KEY_CTRL_T] = "^T", // ok [TB_KEY_CTRL_U] = "^U", // ok [TB_KEY_CTRL_V] = "^V", // ok [TB_KEY_CTRL_W] = "^W", // ok [TB_KEY_CTRL_X] = "^X", // ok [TB_KEY_CTRL_Y] = "^Y", // ok [TB_KEY_CTRL_Z] = "^Z", [TB_KEY_ESC] = "escape", // ok [TB_KEY_CTRL_BACKSLASH] = "^\\", // ok [TB_KEY_CTRL_RSQ_BRACKET] = "^]", // ok [TB_KEY_CTRL_6] = "^6", // ok [TB_KEY_CTRL_SLASH] = "^/", // ok [TB_KEY_SPACE] = "space", // ok }; static const char *function_keys[] = { "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "KEY_INSERT", "KEY_DELETE", "KEY_HOME", "KEY_END", "KEY_PPAGE", "KEY_NPAGE", "KEY_UP", "KEY_DOWN", "KEY_LEFT", "KEY_RIGHT", }; struct tb_event ev; int ret = tb_peek_event(&ev, 0); if (ret != TB_OK) { if (ret == TB_ERR_POLL && tb_last_errno() == EINTR) { INFO("Caught resize interrupt"); return TB_EVENT_RESIZE; } switch (ret) { case TB_ERR_NEED_MORE: WARN("Ignoring incomplete input event!"); break; case TB_ERR_NO_EVENT: break; default: FAIL("Invalid input event, ret = %d", ret); break; } return TB_ERR; } if (ev.type == TB_EVENT_RESIZE) { INFO("Caught resize event"); return TB_EVENT_RESIZE; } if (ev.type == TB_EVENT_KEY) { if (ev.key == 0) { int len = tb_utf8_unicode_to_char(keyname, ev.ch); if (len > 0 && len < 7) { return TB_EVENT_KEY; } } else if (ev.key < 32) { strcpy(keyname, escape_keys[ev.key]); return TB_EVENT_KEY; } else if (ev.key >= TB_KEY_ARROW_RIGHT) { strcpy(keyname, function_keys[0xFFFF - ev.key]); return TB_EVENT_KEY; } else if (ev.key == TB_KEY_CTRL_8 || ev.key == TB_KEY_BACKSPACE2) { strcpy(keyname, "backspace"); return TB_EVENT_KEY; } } WARN("Unknown input event, key = %u, char = %u", ev.key, ev.ch); return TB_ERR; } newsraft/src/dates.c000066400000000000000000000060251516312403600147460ustar00rootroot00000000000000#include #include #include "newsraft.h" static int64_t get_local_offset_relative_to_utc(void) { time_t utc_seconds = time(NULL); if (utc_seconds == ((time_t)-1)) return 0; struct tm *utc_time = gmtime(&utc_seconds); if (utc_time == NULL) return 0; time_t local_seconds = mktime(utc_time); if (local_seconds == ((time_t)-1)) return 0; return utc_seconds - local_seconds; } static int64_t parse_date_rfc3339(const char *src) { struct tm t = {0}; const char *rem = strptime(src, "%Y-%m-%dT%H:%M:%S", &t); if (rem == NULL) { return 0; } // Function timegm does the conversion without bias from local timezone, // but it's not in the standard, unfortunately... //int64_t time = (int64_t)timegm(&t); int64_t time = (int64_t)mktime(&t) + get_local_offset_relative_to_utc(); if ((rem[0] == '+' || rem[0] == '-') && ISDIGIT(rem[1]) && ISDIGIT(rem[2]) && rem[3] == ':' && ISDIGIT(rem[4]) && ISDIGIT(rem[5])) { const long hours = strtol(rem + 1, NULL, 10); const long minutes = strtol(rem + 4, NULL, 10); const long offset = hours * 3600 + minutes * 60; time = rem[0] == '+' ? time - offset : time + offset; } return time > 0 ? time : 0; } int64_t parse_date(const char *str, bool rfc3339_first) { int64_t date = 0; if (rfc3339_first == true) { date = parse_date_rfc3339(str); } if (date <= 0) { date = (int64_t)curl_getdate(str, NULL); if (date < 0) { date = 0; } } if (date <= 0 && rfc3339_first == false) { date = parse_date_rfc3339(str); } static const char *formats[] = { "%Y-%m-%d", "%Y/%m/%d", }; for (size_t i = 0; i < LENGTH(formats) && date <= 0; ++i) { struct tm t = {0}; if (strptime(str, formats[i], &t)) { date = (int64_t)mktime(&t) + get_local_offset_relative_to_utc(); if (date < 0) { date = 0; } } } return date; } struct string * get_cfg_date(struct config_context **ctx, config_entry_id format_id, int64_t date) { const struct string *format = get_cfg_string(ctx, format_id); struct string *str = crtes(format->len + 1000); if (str == NULL) { return NULL; } struct tm timedata; str->len = strftime(str->ptr, str->lim, format->ptr, localtime_r((time_t *)&date, &timedata)); str->ptr[str->len] = '\0'; if (str->len == 0) { WARN("Failed to format date string!"); } return str; } struct timespec newsraft_get_monotonic_time(void) { struct timespec t = {}; clock_gettime(CLOCK_MONOTONIC, &t); return t; } struct string * newsraft_get_pretty_time_diff(struct timespec *start, struct timespec *stop) { struct string *s = crtes(100); int64_t diff_s = stop->tv_sec - start->tv_sec; int64_t diff_ns = stop->tv_nsec - start->tv_nsec; if (diff_ns < 0 && diff_s > 0) { diff_ns += INT64_C(1000000000); diff_s -= 1; } if (diff_s > 0) { str_appendf(s, "%.2f s", (double)diff_s + (double)diff_ns / 1000000000.0); } else if (diff_ns > 1000000) { str_appendf(s, "%.2f ms", (double)diff_ns / 1000000.0); } else if (diff_ns > 1000) { str_appendf(s, "%.2f us", (double)diff_ns / 1000.0); } else { str_appendf(s, "%" PRId64 " ns", diff_ns); } return s; } newsraft/src/db-items.c000066400000000000000000000060101516312403600153440ustar00rootroot00000000000000#include #include "newsraft.h" sqlite3_stmt * db_find_item_by_rowid(int64_t rowid) { INFO("Looking for item with rowid %" PRId64 "...", rowid); sqlite3_stmt *res = db_prepare("SELECT * FROM items WHERE rowid=? LIMIT 1", 42, NULL); if (res == NULL) { return NULL; } sqlite3_bind_int64(res, 1, rowid); if (sqlite3_step(res) != SQLITE_ROW) { WARN("Item with rowid %" PRId64 " is not found!", rowid); sqlite3_finalize(res); return NULL; } INFO("Item with rowid %" PRId64 " is found.", rowid); return res; } static inline bool db_set_item_int(const char *cmd, size_t cmd_len, int value, int64_t rowid) { INFO("Running \"%s\" with \"%d\" on item \"%" PRId64 "\".", cmd, value, rowid); sqlite3_stmt *res = db_prepare(cmd, cmd_len + 1, NULL); if (res == NULL) { return false; } sqlite3_bind_int(res, 1, value); sqlite3_bind_int64(res, 2, rowid); int status = sqlite3_step(res); sqlite3_finalize(res); return status == SQLITE_DONE; } bool db_mark_item_read(int64_t rowid, bool status) { return db_set_item_int("UPDATE items SET unread=? WHERE rowid=?", 39, status ? 0 : 1, rowid); } bool db_mark_item_important(int64_t rowid, bool status) { return db_set_item_int("UPDATE items SET important=? WHERE rowid=?", 42, status ? 1 : 0, rowid); } int64_t db_count_items(struct feed_entry **feeds, size_t feeds_count, bool count_only_unread) { int64_t count = 0; struct string *query = crtas("SELECT COUNT(*) FROM items WHERE ", 33); struct string *cond = generate_items_search_condition(feeds, feeds_count); if (feeds_count == 0 || query == NULL || cond == NULL) { goto error; } catss(query, cond); if (count_only_unread) { catas(query, " AND unread=1", 13); } const char *error = "unknown error"; sqlite3_stmt *res = db_prepare(query->ptr, query->len + 1, &error); if (res == NULL) { if (feeds_count == 1) { str_appendf((*feeds)->errors, "Failed to prepare statement, %s: %s\n", error, query->ptr); } goto error; } for (size_t i = 0; i < feeds_count; ++i) { db_bind_feed_url(res, i + 1, feeds[i]->url); } if (sqlite3_step(res) == SQLITE_ROW) { count = sqlite3_column_int64(res, 0); } else { FAIL("sqlite3_step() in db_count_items() has failed!"); } sqlite3_finalize(res); error: free_string(query); free_string(cond); return count > 0 ? count : 0; } bool db_change_unread_status_of_all_items_in_feeds(struct feed_entry **feeds, size_t feeds_count, bool unread) { if (feeds_count == 0) { return true; } char *query = newsraft_malloc(sizeof(char) * (46 + feeds_count * 2 + 100)); strcpy(query, "UPDATE items SET unread=? WHERE feed_url IN (?"); for (size_t i = 1; i < feeds_count; ++i) { strcat(query, ",?"); } strcat(query, ")"); sqlite3_stmt *res = db_prepare(query, strlen(query) + 1, NULL); if (res == NULL) { free(query); return false; } sqlite3_bind_int(res, 1, unread); for (size_t i = 0; i < feeds_count; ++i) { db_bind_feed_url(res, i + 2, feeds[i]->url); } bool status = sqlite3_step(res) == SQLITE_DONE ? true : false; sqlite3_finalize(res); free(query); return status; } newsraft/src/db.c000066400000000000000000000336301516312403600142350ustar00rootroot00000000000000#include #include #include "newsraft.h" static sqlite3 *db; static void newsraft_regexp_cmp(sqlite3_context *ctx, int argc, sqlite3_value **argv) { if (argc != 2) { sqlite3_result_error(ctx, "regexp() called with invalid number of arguments.\n", -1); return; } const char *exp = (const char *)sqlite3_value_text(argv[0]); const char *text = (const char *)sqlite3_value_text(argv[1]); if (exp == NULL || text == NULL) { sqlite3_result_error(ctx, "regexp() called with invalid argument values.\n", -1); return; } regex_t regex; if (regcomp(®ex, exp, REG_EXTENDED | REG_NOSUB) != 0) { sqlite3_result_error(ctx, "regexp() failed to compile regular expression.\n", -1); return; } int ret = regexec(®ex, text , 0, NULL, 0); regfree(®ex); sqlite3_result_int(ctx, ret != REG_NOMATCH); } static bool db_make_sure_column_exists(const char *table, const char *column, const char *type) { char query[100]; int len = snprintf(query, sizeof(query), "PRAGMA table_info(%s);", table); if (len <= 0 || len >= (int)sizeof(query)) { return false; } sqlite3_stmt *stmt = db_prepare(query, len + 1, NULL); if (stmt == NULL) { write_error("Failed to query table_info from the database: %s!\n", sqlite3_errmsg(db)); return false; } bool column_exists = false; while (sqlite3_step(stmt) == SQLITE_ROW) { const char *name = (const char *)sqlite3_column_text(stmt, 1); if (strcmp(name, column) == 0) { column_exists = true; break; } } sqlite3_finalize(stmt); if (column_exists) { INFO("Column %s (%s) already exists in %s table", column, type, table); return true; } len = snprintf(query, sizeof(query), "ALTER TABLE %s ADD COLUMN %s %s;", table, column, type); if (len <= 0 || len >= (int)sizeof(query)) { return false; } char *errmsg = NULL; if (sqlite3_exec(db, query, NULL, NULL, &errmsg) != SQLITE_OK || errmsg != NULL) { write_error("Failed to add %s column to the %s table in database: %s!\n", column, table, errmsg); sqlite3_free(errmsg); return false; } return true; } bool db_init(void) { const char *path = get_db_path(); if (path == NULL) { // Error message written by get_db_path. return false; } if (sqlite3_config(SQLITE_CONFIG_SERIALIZED) != SQLITE_OK) { write_error("Failed to set threading mode of database to serialized!\n"); write_error("It probably happens because SQLite was compiled without multithreading functionality.\n"); return false; } if (sqlite3_open(path, &db) != SQLITE_OK) { write_error("Failed to open database!\n"); sqlite3_close(db); return false; } char *errmsg = NULL; sqlite3_exec(db, "PRAGMA locking_mode = EXCLUSIVE;", NULL, NULL, &errmsg); if (errmsg != NULL) { write_error("Failed to open database in exclusive locking mode: %s!\n", errmsg); goto error; } if (sqlite3_create_function(db, "regexp", 2, SQLITE_ANY, 0, &newsraft_regexp_cmp, 0, 0) != SQLITE_OK) { write_error("Failed to register REGEXP database function!\n"); goto error; } // All dates are stored as the number of seconds since 1970. // Note that numeric arguments in parentheses that follow the type name // are ignored by SQLite - there's no need to impose any length limits. // // feeds' download_date and update_date are special timestamps: // download_date stores the date of last successful feed download, // update_date stores the date of last feed download attempt. sqlite3_exec( db, "CREATE TABLE IF NOT EXISTS feeds(" "feed_url TEXT NOT NULL UNIQUE," "title TEXT," "link TEXT," "content TEXT," "attachments TEXT," "persons TEXT," "extras TEXT," "download_date INTEGER NOT NULL DEFAULT 0," "update_date INTEGER NOT NULL DEFAULT 0," "time_to_live INTEGER NOT NULL DEFAULT 0," "http_header_etag TEXT," "http_header_last_modified INTEGER NOT NULL DEFAULT 0," "http_header_expires INTEGER NOT NULL DEFAULT 0," "user_data TEXT" ");" "CREATE TABLE IF NOT EXISTS items(" "feed_url TEXT NOT NULL," "guid TEXT NOT NULL," "title TEXT," "link TEXT," "content TEXT," "attachments TEXT," "persons TEXT," "extras TEXT," "download_date INTEGER NOT NULL DEFAULT 0," "publication_date INTEGER NOT NULL DEFAULT 0," "update_date INTEGER NOT NULL DEFAULT 0," "unread INTEGER NOT NULL DEFAULT 0," "important INTEGER NOT NULL DEFAULT 0," "user_data TEXT" ");" "CREATE INDEX IF NOT EXISTS idx_items_guid ON items(" "feed_url," "guid" ");" "CREATE INDEX IF NOT EXISTS idx_items_eight_way ON items(" "feed_url," "guid," "title," "link," "publication_date," "update_date," "unread," "important" ");", NULL, NULL, &errmsg ); if (errmsg != NULL) { write_error("Failed to initialize database in exclusive locking mode: %s!\n", errmsg); goto error; } // These columns were implemented after users already created their database files, // therefore we have to make sure that existing database tables get them post factum. if (!db_make_sure_column_exists("feeds", "user_data", "TEXT")) { goto error; } if (!db_make_sure_column_exists("items", "download_date", "INTEGER NOT NULL DEFAULT 0")) { goto error; } if (!db_make_sure_column_exists("items", "user_data", "TEXT")) { goto error; } return true; error: write_error("Probably there's other process currently working with this database.\n"); sqlite3_free(errmsg); sqlite3_close(db); return false; } bool db_vacuum(void) { char *error = NULL; sqlite3_exec(db, "VACUUM;", NULL, NULL, &error); if (error != NULL) { write_error("Failed to vacuum the database: %s!\n", error); sqlite3_free(error); return false; } return true; } bool exec_database_file_optimization(void) { if (get_cfg_bool(NULL, CFG_DATABASE_CLEAN_ON_STARTUP) == true) { if (db_vacuum() == false) { return false; } } char *error = NULL; if (get_cfg_bool(NULL, CFG_DATABASE_ANALYZE_ON_STARTUP) == true) { sqlite3_exec(db, "ANALYZE;", NULL, NULL, &error); if (error != NULL) { write_error("Failed to analyze the database: %s!\n", error); sqlite3_free(error); return false; } } return true; } bool db_transaction_begin(void) { if (!get_cfg_bool(NULL, CFG_DATABASE_BATCH_TRANSACTIONS)) { INFO("Not beginning database transaction because it's turned off."); return true; } INFO("Starting database transaction."); char *errmsg; sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, &errmsg); if (errmsg != NULL) { FAIL("Can not begin transaction: %s", errmsg); sqlite3_free(errmsg); return false; } return true; } bool db_transaction_commit(void) { if (!get_cfg_bool(NULL, CFG_DATABASE_BATCH_TRANSACTIONS)) { INFO("Not committing database transaction because it's turned off."); return true; } INFO("Committing database transaction."); char *errmsg; sqlite3_exec(db, "COMMIT;", NULL, NULL, &errmsg); if (errmsg != NULL) { FAIL("Can not commit transaction: %s", errmsg); sqlite3_free(errmsg); return false; } return true; } bool db_rollback_transaction(void) { INFO("Rolling back database transaction."); char *errmsg; sqlite3_exec(db, "ROLLBACK;", NULL, NULL, &errmsg); if (errmsg != NULL) { FAIL("Can not rollback transaction: %s", errmsg); sqlite3_free(errmsg); return false; } return true; } void db_stop(void) { sqlite3_close(db); } sqlite3_stmt * db_prepare(const char *statement, int size, const char **p_error) { sqlite3_stmt *stmt = NULL; // Last argument to sqlite3_prepare_v2() is a pointer to unused portion of // statement (sqlite3_prepare_v2() will prepare only first command from the // statement). But given that we always pass one command in the statement, // this parameter is practically useless. int status = sqlite3_prepare_v2(db, statement, size, &stmt, NULL); if (status != SQLITE_OK) { // We don't use sqlite3_errmsg() here because it's not thread-safe. const char *error = sqlite3_errstr(status); error = error ? error : "unknown error"; FAIL("Failed to prepare statement, %s: %s", error, statement); if (p_error) { *p_error = error; } return NULL; } INFO("Prepared statement: %s", statement); return stmt; } const char * db_error_string(void) { // FIXME: sqlite3_errmsg() is not thread-safe return sqlite3_errmsg(db); } int db_bind_string(sqlite3_stmt *stmt, int pos, const struct string *str) { if (STRING_IS_EMPTY(str)) { return sqlite3_bind_null(stmt, pos); } else { return sqlite3_bind_text(stmt, pos, str->ptr, str->len, SQLITE_STATIC); } } // You might wonder why we would ever need a special function to bind a feed URL // specifically. Well, that's simple: sometimes user specifies URL with trailing // slashes, sometimes without them. These URLs can point to different content in // practice, but for the most part it's either // * both URLs (with and without trailing slashes) point to the same content, or // * one URL points to useful content and the other one is simply empty (404ing) // // Database-wise, we should treat these URLs as a single entity, // so that the user doesn't have to suffer from the fragmentation of the // same feed based on the presence or absence of slashes in the URL. int db_bind_feed_url(sqlite3_stmt *stmt, int pos, const struct string *str) { if (STRING_IS_EMPTY(str)) { return sqlite3_bind_null(stmt, pos); } size_t len = str->len; while (len > 0 && str->ptr[len - 1] == '/') { len -= 1; } return sqlite3_bind_text(stmt, pos, str->ptr, len, SQLITE_STATIC); } int64_t db_get_date_from_feeds_table(const struct string *url, const char *column, size_t column_len) { char query[100] = {0}; // longest column name (25) + rest of query (35) + terminator (1) < 100 memcpy(query, "SELECT ", 7); memcpy(query + 7, column, column_len); memcpy(query + 7 + column_len, " FROM feeds WHERE feed_url=?", 29); sqlite3_stmt *res = db_prepare(query, 7 + column_len + 29, NULL); if (res == NULL) { return -1; } db_bind_feed_url(res, 1, url); int64_t date = sqlite3_step(res) == SQLITE_ROW ? sqlite3_column_int64(res, 0) : 0; sqlite3_finalize(res); INFO("%s of %s is %" PRId64, column, url->ptr, date); return date; } struct string * db_get_string_from_feed_table(const struct string *url, const char *column, size_t column_len) { char query[100] = {0}; memcpy(query, "SELECT ", 7); memcpy(query + 7, column, column_len); memcpy(query + 7 + column_len, " FROM feeds WHERE feed_url=?", 29); sqlite3_stmt *res = db_prepare(query, 7 + column_len + 29, NULL); if (res != NULL) { db_bind_feed_url(res, 1, url); if (sqlite3_step(res) == SQLITE_ROW) { const char *str_ptr = (char *)sqlite3_column_text(res, 0); if (str_ptr != NULL) { size_t str_len = strlen(str_ptr); if (str_len > 0) { struct string *str = crtas(str_ptr, str_len); sqlite3_finalize(res); return str; } } } sqlite3_finalize(res); } return NULL; } void db_update_feed_int64(const struct string *url, const char *column_name, int64_t value, bool only_positive) { if (only_positive == false || value > 0) { char query[100] = {0}; strcpy(query, "UPDATE feeds SET "); strcat(query, column_name); strcat(query, "=? WHERE feed_url=?"); INFO("Setting %s of %s to %" PRId64, column_name, url->ptr, value); sqlite3_stmt *res = db_prepare(query, strlen(query) + 1, NULL); if (res != NULL) { sqlite3_bind_int64(res, 1, value); db_bind_feed_url(res, 2, url); if (sqlite3_step(res) != SQLITE_DONE) { FAIL("Failed to update %s!", column_name); } sqlite3_finalize(res); } } } void db_update_feed_string(const struct string *url, const char *column_name, const struct string *value, bool only_nonempty) { if (only_nonempty == false || (value != NULL && value->len > 0)) { char query[100] = {0}; strcpy(query, "UPDATE feeds SET "); strcat(query, column_name); strcat(query, "=? WHERE feed_url=?"); INFO("Setting %s of %s to %s", column_name, url->ptr, value->ptr); sqlite3_stmt *res = db_prepare(query, strlen(query) + 1, NULL); if (res != NULL) { db_bind_string(res, 1, value); db_bind_feed_url(res, 2, url); if (sqlite3_step(res) != SQLITE_DONE) { FAIL("Failed to update %s!", column_name); } sqlite3_finalize(res); } } } void db_perform_user_edit(const struct wstring *fmt, struct feed_entry **feeds, size_t feeds_count, const struct item_entry *item) { struct timespec start = newsraft_get_monotonic_time(); size_t replacements_count = 0; struct wstring *cmd = wcrtes(1000); for (size_t i = 0; i + 8 < fmt->len; ++i) { if (wcsncmp(fmt->ptr + i, L"@selected", 9) == 0) { i += 8; if (item) { wcatas(cmd, L"(rowid=? AND feed_url=? AND guid=?)", 35); replacements_count += 1; continue; } if (feeds && feeds_count > 0) { wcatcs(cmd, L'('); for (size_t i = 0; i < feeds_count; ++i) { if (i > 0) { wcatas(cmd, L" OR ", 4); } wcatas(cmd, L"feed_url=?", 10); } wcatcs(cmd, L')'); replacements_count += 1; continue; } } else { wcatcs(cmd, fmt->ptr[i]); } } struct string *query = convert_wstring_to_string(cmd); free_wstring(cmd); if (query == NULL) { return; } const char *error = ""; sqlite3_stmt *stmt = db_prepare(query->ptr, query->len + 1, &error); if (stmt == NULL) { fail_status("%s: %ls", error, fmt->ptr); free_string(query); return; } for (size_t i = 0, j = 1; i < replacements_count; ++i) { if (item) { sqlite3_bind_int64(stmt, j++, item->rowid); db_bind_feed_url(stmt, j++, item->feed[0]->url); db_bind_string(stmt, j++, item->guid); continue; } if (feeds && feeds_count > 0) { for (size_t k = 0; k < feeds_count; ++k) { db_bind_feed_url(stmt, j++, feeds[k]->url); } continue; } } while (true) { int status = sqlite3_step(stmt); if (status == SQLITE_DONE) { int changes = sqlite3_changes(db); struct timespec stop = newsraft_get_monotonic_time(); struct string *time_diff = newsraft_get_pretty_time_diff(&start, &stop); info_status("Successful edit (%d change%s took %s)", changes, changes > 1 ? "s" : "", time_diff->ptr); free_string(time_diff); break; } if (status != SQLITE_ROW) { fail_status("Failed: %ls", fmt->ptr); break; } } sqlite3_finalize(stmt); free_string(query); } newsraft/src/downloader.c000066400000000000000000000322701516312403600160050ustar00rootroot00000000000000#include #include #include #include "newsraft.h" static CURLM *multi; static struct curl_slist * create_list_of_headers(const struct feed_update_state *data) { // A-IM header is a hint for servers that they can send only a part of data, // often known as "delta". Server will know which part of data to send // exactly by analyzing our If-None-Match header. See RFC3229 for more info. struct curl_slist *headers = curl_slist_append(NULL, "A-IM: feed"); if (headers == NULL) { return NULL; } INFO("Attached header - A-IM: feed"); if (get_cfg_bool(&data->feed_entry->cfg, CFG_SEND_IF_NONE_MATCH_HEADER) == true) { struct string *etag = db_get_string_from_feed_table(data->feed_entry->url, "http_header_etag", 16); if (etag != NULL) { struct string *if_none_match = crtas("If-None-Match: ", 15); if (if_none_match == NULL) { free_string(if_none_match); free_string(etag); goto error; } catss(if_none_match, etag); free_string(etag); struct curl_slist *tmp = curl_slist_append(headers, if_none_match->ptr); if (tmp == NULL) { free_string(if_none_match); goto error; } headers = tmp; INFO("Attached header - %s", if_none_match->ptr); free_string(if_none_match); } } return headers; error: curl_slist_free_all(headers); return NULL; } static bool parse_xml_data(struct feed_update_state *ctx, const char *data, size_t size, bool is_final) { if (XML_Parse(ctx->xml_parser, data, size, is_final ? XML_TRUE : XML_FALSE) != XML_STATUS_OK) { str_appendf(ctx->new_errors, "XML parser failed: %s\n", XML_ErrorString(XML_GetErrorCode(ctx->xml_parser))); return false; } return true; } static bool parse_json_data(struct feed_update_state *ctx, const char *data, size_t size, bool is_final) { if (is_final) { return newsraft_json_parse(ctx, ctx->text->ptr, ctx->text->len); } else { catas(ctx->text, data, size); } return true; } static size_t parse_stream_callback(char *data, size_t length, size_t nmemb, void *userdata) { struct feed_update_state *ctx = userdata; const size_t real_size = length * nmemb; if (!ctx->process_fn) { for (size_t i = 0; i < real_size; ++i) { if (data[i] == '<') { INFO("The stream has \"<\" character in the beginning - engaging XML parser."); if (setup_xml_parser(ctx) == false) { FAIL("Failed to setup XML parser!"); return CURL_WRITEFUNC_ERROR; } ctx->process_fn = parse_xml_data; break; } else if (data[i] == '{') { INFO("The stream has \"{\" character in the beginning - engaging JSON parser."); ctx->process_fn = parse_json_data; break; } } } if (ctx->process_fn) { if (!ctx->process_fn(ctx, data, real_size, false)) { return CURL_WRITEFUNC_ERROR; } } return they_want_us_to_stop ? CURL_WRITEFUNC_ERROR : real_size; } static size_t header_callback(char *contents, size_t length, size_t nmemb, void *userdata) { struct getfeed_feed *feed = userdata; const size_t real_size = nmemb * length; size_t header_name_len = 0; for (size_t i = 0; i < real_size; ++i) { if (contents[i] == ':') { header_name_len = i; break; } } if (header_name_len == 0) { return real_size; // Ignore invalid headers. } struct string *header_name = crtas(contents, header_name_len); struct string *header_value = crtas(contents + header_name_len + 1, real_size - header_name_len - 1); if (header_name == NULL || header_value == NULL) { free_string(header_name); free_string(header_value); return CURL_WRITEFUNC_ERROR; } trim_whitespace_from_string(header_name); trim_whitespace_from_string(header_value); if (strcasecmp(header_name->ptr, "ETag") == 0) { INFO("Got ETag header > %s", header_value->ptr); cpyss(&feed->http_header_etag, header_value); } else if (strcasecmp(header_name->ptr, "Last-Modified") == 0) { INFO("Got Last-Modified header > %s", header_value->ptr); feed->http_header_last_modified = curl_getdate(header_value->ptr, NULL); if (feed->http_header_last_modified < 0) { FAIL("Curl failed to parse date string!"); feed->http_header_last_modified = 0; } } else if (strcasecmp(header_name->ptr, "Expires") == 0) { INFO("Got Expires header > %s", header_value->ptr); feed->http_header_expires = curl_getdate(header_value->ptr, NULL); if (feed->http_header_expires < 0) { FAIL("Curl failed to parse date string!"); feed->http_header_expires = 0; } } else { INFO("Got needless header > %s: %s", header_name->ptr, header_value->ptr); } free_string(header_name); free_string(header_value); return they_want_us_to_stop ? CURL_WRITEFUNC_ERROR : real_size; } static inline struct string * get_proxy_auth_info_encoded(const char *user, const char *password) { if (user == NULL || password == NULL) { return NULL; } struct string *result = crtas(user, strlen(user)); catcs(result, ':'); catas(result, password, strlen(password)); return result; } static inline bool prepare_feed_update_state_for_download(struct feed_update_state *data) { struct feed_entry *feed = data->feed_entry; if (get_cfg_bool(&feed->cfg, CFG_RESPECT_EXPIRES_HEADER) == true) { int64_t expires_date = db_get_date_from_feeds_table(feed->url, "http_header_expires", 19); if (expires_date < 0) { FAIL("Skipping %s because its HTTP header is invalid", feed->url->ptr); goto fail; } else if (expires_date > 0 && feed->update_date < expires_date) { INFO("Skipping %s because its HTTP header is not expired yet", feed->url->ptr); goto cancel; } } if (get_cfg_bool(&feed->cfg, CFG_RESPECT_TTL_ELEMENT) == true) { int64_t download_date = db_get_date_from_feeds_table(feed->url, "download_date", 13); int64_t ttl = db_get_date_from_feeds_table(feed->url, "time_to_live", 12); if (download_date < 0 || ttl < 0) { FAIL("Skipping %s because its ttl element is invalid", feed->url->ptr); goto fail; } else if (download_date > 0 && ttl > 0 && (download_date + ttl) > feed->update_date) { INFO("Skipping %s because its ttl element is not expired yet", feed->url->ptr); goto cancel; } } CURL *curl = curl_easy_init(); data->curl = curl; if (data->curl == NULL) { goto fail; } data->download_headers = create_list_of_headers(data); if (data->download_headers == NULL) { goto fail; } curl_easy_setopt(curl, CURLOPT_URL, feed->url->ptr); curl_easy_setopt(curl, CURLOPT_PRIVATE, data); const struct string *useragent = get_cfg_string(&feed->cfg, CFG_USER_AGENT); if (useragent->len > 0) { INFO("Attached header - User-Agent: %s", useragent->ptr); curl_easy_setopt(curl, CURLOPT_USERAGENT, useragent->ptr); } if (get_cfg_bool(&feed->cfg, CFG_SEND_IF_MODIFIED_SINCE_HEADER) == true) { int64_t last_modified = db_get_date_from_feeds_table(feed->url, "http_header_last_modified", 25); if (last_modified > 0) { curl_easy_setopt(curl, CURLOPT_TIMEVALUE_LARGE, (curl_off_t)last_modified); curl_easy_setopt(curl, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE); INFO("Attached header - If-Modified-Since: %" PRId64 " (it was converted to date string).", last_modified); } else if (last_modified < 0) { FAIL("Skipping %s because its http_header_last_modified is invalid", feed->url->ptr); goto fail; } } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, data->download_headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &parse_stream_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, data); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, &header_callback); curl_easy_setopt(curl, CURLOPT_HEADERDATA, &data->feed); curl_easy_setopt(curl, CURLOPT_TIMEOUT, get_cfg_uint(&feed->cfg, CFG_DOWNLOAD_TIMEOUT)); curl_easy_setopt(curl, CURLOPT_MAX_RECV_SPEED_LARGE, (curl_off_t)get_cfg_uint(&feed->cfg, CFG_DOWNLOAD_SPEED_LIMIT) * 1024); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, data->curl_error); curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 10L); curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); // Enable all supported built-in encodings. curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); FILE *log_stream = log_get_stream(); if (log_stream) { curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); curl_easy_setopt(curl, CURLOPT_STDERR, log_stream); } else { curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L); } const struct string *proxy = get_cfg_string(&feed->cfg, CFG_PROXY); if (proxy->len != 0) { curl_easy_setopt(curl, CURLOPT_PROXY, proxy->ptr); const struct string *user_str = get_cfg_string(&feed->cfg, CFG_PROXY_USER); const struct string *password_str = get_cfg_string(&feed->cfg, CFG_PROXY_PASSWORD); if ((user_str->len != 0) && (password_str->len != 0)) { char *user = curl_easy_escape(curl, user_str->ptr, user_str->len); char *password = curl_easy_escape(curl, password_str->ptr, password_str->len); struct string *auth = get_proxy_auth_info_encoded(user, password); curl_free(user); curl_free(password); if (auth == NULL) { goto fail; } curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, auth->ptr); free_string(auth); } } return true; cancel: data->is_canceled = true; return false; fail: data->is_failed = true; return false; } static bool engage_with_not_downloaded_feed(struct feed_update_state *data) { if (data->is_finished == false && data->is_in_progress == false && data->feed_entry->url->ptr[0] != '$') { data->is_in_progress = true; return true; } return false; } void * downloader_worker(void *dummy) { (void)dummy; int still_running = 0; // Number of active handles CURLMsg *msg; int msgs_left = 0; uint64_t perform_count = 0; while (they_want_us_to_stop == false) { struct feed_update_state *target = queue_pull(&engage_with_not_downloaded_feed); if (target != NULL) { if (prepare_feed_update_state_for_download(target) == true) { curl_multi_add_handle(multi, target->curl); target->curl_handle_added_to_multi = true; INFO("Populated curl multi handle with %s", target->feed_entry->url->ptr); } } else if (still_running == 0) { threads_take_a_nap(NEWSRAFT_THREAD_DOWNLOAD); } else { // This one is waken up with curl_multi_wakeup() curl_multi_poll(multi, NULL, 0, 1 /* ms */, NULL); } perform_count += 1; CURLMcode status = curl_multi_perform(multi, &still_running); if (status != CURLM_OK) { FAIL("Got an error while performing on multi handle: %s", curl_multi_strerror(status)); } while ((msg = curl_multi_info_read(multi, &msgs_left)) != NULL) { if (msg->msg != CURLMSG_DONE) { continue; } INFO("Downloader stats: %" PRIu64 " curl performances, %d still running, %d msgs left", perform_count, still_running, msgs_left ); struct feed_update_state *data = NULL; curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &data); // Set with CURLOPT_PRIVATE curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &data->http_response_code); INFO("HTTP response code: %ld", data->http_response_code); if (data->http_response_code == NEWSRAFT_HTTP_NOT_MODIFIED) { // Server says that our stored content is up to date. It knows it based on: // 1) server's ETag header is equal to our If-None-Match header; and/or // 2) server's Last-Modified header is equal to our If-Modified-Since header. data->is_canceled = true; } else if (data->http_response_code == NEWSRAFT_HTTP_TOO_MANY_REQUESTS) { WARN("The server rejected the download because updates are too frequent."); data->is_canceled = true; } if (msg->data.result != CURLE_OK) { str_appendf(data->new_errors, "curl failed: %s, %s\n", curl_easy_strerror(msg->data.result), data->curl_error); if (data->http_response_code != 0) { str_appendf(data->new_errors, "The server which keeps the feed returned %ld status code!\n", data->http_response_code); } data->is_failed = true; } // Final parsing call if (data->is_failed == false && data->is_canceled == false) { if (data->process_fn) { data->is_failed = !data->process_fn(data, NULL, 0, true); } else { str_appendf(data->new_errors, "Couldn't determine feed format!\n"); data->is_failed = true; } } data->is_downloaded = true; threads_wake_up(NEWSRAFT_THREAD_DBWRITER); } } return NULL; } void downloader_curl_wakeup(void) { curl_multi_wakeup(multi); } void remove_downloader_handle(struct feed_update_state *data) { if (data->curl_handle_added_to_multi) { curl_multi_remove_handle(multi, data->curl); } } bool curl_init(void) { curl_version_info_data *ver = curl_version_info(CURLVERSION_NOW); INFO("libcurl AsynchDNS feature: %s", ver->features & CURL_VERSION_ASYNCHDNS ? "enabled" : "disabled"); if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) { write_error("Failed to initialize curl!\n"); return false; } // Initialize "multi stack". This is the way to do asynchronous // downloads in CURL with shared connection and DNS cache. multi = curl_multi_init(); if (multi == NULL) { write_error("Failed to initialize curl multi stack!\n"); return false; } curl_multi_setopt(multi, CURLMOPT_MAX_TOTAL_CONNECTIONS, get_cfg_uint(NULL, CFG_DOWNLOAD_MAX_CONNECTIONS)); curl_multi_setopt(multi, CURLMOPT_MAX_HOST_CONNECTIONS, get_cfg_uint(NULL, CFG_DOWNLOAD_MAX_HOST_CONNECTIONS)); return true; } void curl_stop(void) { queue_destroy(); curl_multi_cleanup(multi); curl_global_cleanup(); } newsraft/src/errors.c000066400000000000000000000014021516312403600151540ustar00rootroot00000000000000#include "newsraft.h" #define ERROR_BUFFER_SIZE 1000 static char error_buffer[ERROR_BUFFER_SIZE + 1]; static size_t error_buffer_len = 0; // We have to write errors to intermediate buffer because when writing to the // stderr directly, user interface proactively erase everything that was printed. void write_error(const char *format, ...) { if (error_buffer_len < ERROR_BUFFER_SIZE) { va_list args; va_start(args, format); int add = vsnprintf( error_buffer + error_buffer_len, ERROR_BUFFER_SIZE - error_buffer_len, format, args ); if (add > 0) { error_buffer_len += add; } va_end(args); } } void flush_errors(void) { if (error_buffer_len > 0) { FAIL("Final error buffer:\n\n%s", error_buffer); fputs(error_buffer, stderr); } } newsraft/src/executor.c000066400000000000000000000042641516312403600155070ustar00rootroot00000000000000#include "newsraft.h" static void execute_feed(const struct string *cmd, struct feed_update_state *data) { struct string *real_cmd = crtas(cmd->ptr + 2, cmd->len - 3); struct string *content = crtes(10000); FILE *p = popen(real_cmd->ptr, "r"); if (p == NULL) { goto error; } for (int c = fgetc(p); c != EOF; c = fgetc(p)) { catcs(content, c); } int exit_status = pclose(p); if (exit_status != 0) { str_appendf(data->new_errors, "Failed to execute %s\nFailed with exit status %d\n", real_cmd->ptr, exit_status); goto error; } for (const char *i = content->ptr; *i != '\0'; ++i) { if (*i == '<') { INFO("The output has \"<\" character in the beginning - engaging XML parser."); if (setup_xml_parser(data) == false) { FAIL("Failed to setup XML parser!"); goto error; } enum XML_Status status = XML_Parse(data->xml_parser, content->ptr, content->len, XML_FALSE); if (status == XML_STATUS_OK) { status = XML_Parse(data->xml_parser, NULL, 0, XML_TRUE); // Final parsing call } if (status != XML_STATUS_OK) { str_appendf(data->new_errors, "XML parser failed: %s\n", XML_ErrorString(XML_GetErrorCode(data->xml_parser))); goto error; } break; } else if (*i == '{') { INFO("The output has \"{\" character in the beginning - engaging JSON parser."); if (!newsraft_json_parse(data, content->ptr, content->len)) { goto error; } break; } } free_string(real_cmd); free_string(content); return; error: free_string(real_cmd); free_string(content); data->is_failed = true; return; } static bool engage_with_not_executed_feed(struct feed_update_state *data) { if (data->is_finished == false && data->is_in_progress == false && data->feed_entry->url->ptr[0] == '$') { data->is_in_progress = true; return true; } return false; } void * executor_worker(void *dummy) { (void)dummy; while (they_want_us_to_stop == false) { struct feed_update_state *target = queue_pull(&engage_with_not_executed_feed); if (target == NULL) { threads_take_a_nap(NEWSRAFT_THREAD_SHRUNNER); continue; } execute_feed(target->feed_entry->url, target); target->is_downloaded = true; threads_wake_up(NEWSRAFT_THREAD_DBWRITER); } return NULL; } newsraft/src/feeds-parse.c000066400000000000000000000133301516312403600160410ustar00rootroot00000000000000#include #include "newsraft.h" static inline bool check_url_for_validity(const struct string *url) { if (strncmp(url->ptr, "http://", 7) != 0 && strncmp(url->ptr, "https://", 8) != 0 && strncmp(url->ptr, "ftp://", 6) != 0 && strncmp(url->ptr, "file://", 7) != 0 && strncmp(url->ptr, "gopher://", 9) != 0 && strncmp(url->ptr, "gophers://", 10) != 0) { write_error("Stumbled across an invalid URL: \"%s\"!\n", url->ptr); write_error("Every feed URL must start with a protocol scheme like \"http://\".\n"); write_error("Supported protocol schemes are http, https, ftp, file, gopher and gophers.\n"); return false; } return true; } bool parse_feeds_file(void) { const char *feeds_file_path = get_feeds_path(); if (feeds_file_path == NULL) { return false; } if (make_sure_section_exists(get_cfg_string(NULL, CFG_GLOBAL_SECTION_NAME)) != 0) { return false; } FILE *f = fopen(feeds_file_path, "r"); if (f == NULL) { write_error("Couldn't open feeds file!\n"); return false; } bool status = false; int64_t section_index = 0; struct string *line = crtes(200); struct string *section_name = crtes(200); struct string *feed_cfg = crtes(200); struct string *section_cfg = crtes(200); struct string *global_cfg = crtes(200); bool at_least_one_feed_was_added = false; struct feed_entry feed = { .url = crtes(200), .name = crtes(100), }; int c = '@'; while (c != EOF) { empty_string(line); for (c = fgetc(f); c != '\n' && c != EOF; c = fgetc(f)) { catcs(line, c); } trim_whitespace_from_string(line); if (line->len == 0 || line->ptr[0] == '#') continue; // Skip empty and comment lines. bool is_section = false; // Process first token: section, feed command or feed link size_t len = 0; if (line->ptr[0] == '@') { is_section = true; for (const char *i = line->ptr + 1; *i != '[' && *i != '{' && *i != '<' && *i != '\0'; ++i) { len += 1; } cpyas(§ion_name, line->ptr + 1, len); trim_whitespace_from_string(section_name); section_index = make_sure_section_exists(section_name); if (section_index < 0) goto error; empty_string(section_cfg); remove_start_of_string(line, 1 + len); } else if (line->ptr[0] == '$' && line->ptr[1] == '(') { bool need_next_character_escaped = false; bool inside_single_quoted_string = false; bool inside_double_quoted_string = false; cpyas(&feed.url, line->ptr, 2); for (len = 2; line->ptr[len] != '\0'; ++len) { catcs(feed.url, line->ptr[len]); if (need_next_character_escaped) { need_next_character_escaped = false; } else if (line->ptr[len] == '\\') { need_next_character_escaped = true; } else if (line->ptr[len] == '\'') { inside_single_quoted_string = !inside_single_quoted_string; } else if (line->ptr[len] == '"') { inside_double_quoted_string = !inside_double_quoted_string; } else { if (line->ptr[len] == ')' && !inside_single_quoted_string && !inside_double_quoted_string) { len += 1; break; } } } remove_start_of_string(line, len); } else { for (const char *i = line->ptr; !ISWHITESPACE(*i) && *i != '\0'; ++i) { len += 1; } cpyas(&feed.url, line->ptr, len); // Here you may want to remove the trailing slashes at the end of the URL. // We MUST NOT do that, because a URL with and without a trailing slash can // point to different resources. Treating them alike breaks HTTP semantics. // Actually removing the trailing slashes backfired at us once. Here's link: // https://codeberg.org/newsraft/newsraft/issues/224 if (!check_url_for_validity(feed.url)) { goto error; } remove_start_of_string(line, len); } trim_whitespace_from_string(line); empty_string(feed_cfg); // Process second token: feed name empty_string(feed.name); if (line->ptr[0] == '"') { char *close = strchr(line->ptr + 1, '"'); if (close == NULL) { write_error("Unclosed feed name!\n"); goto error; } *close = '\0'; cpyas(&feed.name, line->ptr + 1, strlen(line->ptr + 1)); remove_start_of_string(line, close + 1 - line->ptr); } trim_whitespace_from_string(line); if (line->ptr[0] == '[' || line->ptr[0] == '{') { write_error("Counters in [square] and {curly} brackets are deprecated!\n"); write_error("You must set reload-period and item-limit settings to individual feeds and sections instead.\n"); write_error("For example:\n"); write_error("http://example.org/feed.xml < reload-period 30; item-limit 100\n"); goto error; } if (line->ptr[0] == '<') { catas(is_section ? section_cfg : feed_cfg, line->ptr + 1, strlen(line->ptr + 1)); } if (is_section == true && section_index == 0) { catss(global_cfg, section_cfg); } if (is_section == true) continue; struct feed_entry *feed_ptr = copy_feed_to_section(&feed, section_index); if (feed_ptr == NULL) goto error; at_least_one_feed_was_added = true; if (!process_config_line(feed_ptr, global_cfg->ptr, global_cfg->len)) goto error; if (!process_config_line(feed_ptr, section_cfg->ptr, section_cfg->len)) goto error; if (!process_config_line(feed_ptr, feed_cfg->ptr, feed_cfg->len)) goto error; // Count unread items only after config is applied because some settings // can influence how items are counted, for example item-rule setting. feed_ptr->unread_count = db_count_items(&feed_ptr, 1, true); feed_ptr->items_count = db_count_items(&feed_ptr, 1, false); } if (at_least_one_feed_was_added == false) { write_error("Not a single feed was loaded!\n"); goto error; } status = true; error: free_string(line); free_string(section_name); free_string(feed_cfg); free_string(section_cfg); free_string(global_cfg); free_string(feed.url); free_string(feed.name); fclose(f); return status; } newsraft/src/feeds.c000066400000000000000000000173161516312403600147410ustar00rootroot00000000000000#include #include #include "newsraft.h" static struct feed_entry **feeds = NULL; static size_t feeds_count = 0; static sorting_method_t feeds_sort = SORT_BY_INITIAL_ASC; static struct feed_entry **feeds_original = NULL; static bool is_feed_valid(struct menu_state *ctx, size_t index) { return index < ctx->feeds_count ? true : false; } static const struct format_arg * get_feed_args(struct menu_state *ctx, size_t index) { static struct format_arg feed_fmt[] = { {L'i', L'd', {.i = 0 }}, {L'u', L'd', {.i = 0 }}, {L'n', L'd', {.i = 0 }}, {L'l', L's', {.s = NULL}}, {L't', L's', {.s = NULL}}, {L'\0', L'\0', {.i = 0 }}, // terminator }; feed_fmt[0].value.i = index + 1; feed_fmt[1].value.i = ctx->feeds[index]->unread_count; feed_fmt[2].value.i = ctx->feeds[index]->items_count; feed_fmt[3].value.s = ctx->feeds[index]->url->ptr; feed_fmt[4].value.s = STRING_IS_EMPTY(ctx->feeds[index]->name) ? ctx->feeds[index]->url->ptr : ctx->feeds[index]->name->ptr; return feed_fmt; } static struct config_color paint_feed(struct menu_state *ctx, size_t index, bool is_selected) { struct config_context **cfg = &ctx->feeds[index]->cfg; struct config_color color; if (ctx->feeds[index]->errors->len > 0 && !get_cfg_bool(&ctx->feeds[index]->cfg, CFG_SUPPRESS_ERRORS)) { color = get_cfg_color(cfg, CFG_COLOR_LIST_FEED_FAILED); } else if (ctx->feeds[index]->unread_count > 0) { color = get_cfg_color(cfg, CFG_COLOR_LIST_FEED_UNREAD); } else { color = get_cfg_color(cfg, CFG_COLOR_LIST_FEED); } if (is_selected) { if (is_cfg_color_set(cfg, CFG_COLOR_LIST_FEED_SELECTED)) { color = get_cfg_color(cfg, CFG_COLOR_LIST_FEED_SELECTED); } else { color.attributes |= TB_REVERSE; } } return color; } static bool is_feed_unread(struct menu_state *ctx, size_t index) { return ctx->feeds[index]->unread_count > 0; } static bool is_feed_failed(struct menu_state *ctx, size_t index) { return ctx->feeds[index]->errors->len > 0; } static int compare_feeds_initial(const void *data1, const void *data2) { struct feed_entry *feed1 = *(struct feed_entry **)data1; struct feed_entry *feed2 = *(struct feed_entry **)data2; size_t index1 = 0, index2 = 0; for (size_t i = 0; i < feeds_count; ++i) { if (feed1 == feeds_original[i]) index1 = i; if (feed2 == feeds_original[i]) index2 = i; } if (index1 > index2) return feeds_sort & 1 ? -1 : 1; if (index1 < index2) return feeds_sort & 1 ? 1 : -1; return 0; } static int compare_feeds_unread(const void *data1, const void *data2) { struct feed_entry *feed1 = *(struct feed_entry **)data1; struct feed_entry *feed2 = *(struct feed_entry **)data2; if (feed1->unread_count > feed2->unread_count) return feeds_sort & 1 ? -1 : 1; if (feed1->unread_count < feed2->unread_count) return feeds_sort & 1 ? 1 : -1; return compare_feeds_initial(data1, data2) * (feeds_sort & 1 ? -1 : 1); } static int compare_feeds_alphabet(const void *data1, const void *data2) { struct feed_entry *feed1 = *(struct feed_entry **)data1; struct feed_entry *feed2 = *(struct feed_entry **)data2; const char *token1 = STRING_IS_EMPTY(feed1->name) ? feed1->url->ptr : feed1->name->ptr; const char *token2 = STRING_IS_EMPTY(feed2->name) ? feed2->url->ptr : feed2->name->ptr; return strcmp(token1, token2) * (feeds_sort & 1 ? -1 : 1); } static inline void sort_feeds(struct menu_state *m, sorting_method_t method, bool we_are_already_in_feeds_menu) { pthread_mutex_lock(&interface_lock); bool need_status_message = method != feeds_sort; feeds = m->feeds; feeds_count = m->feeds_count; feeds_original = m->feeds_original; feeds_sort = method; switch (feeds_sort & ~1) { case SORT_BY_UNREAD_ASC: qsort(feeds, feeds_count, sizeof(struct feed_entry *), &compare_feeds_unread); break; case SORT_BY_INITIAL_ASC: qsort(feeds, feeds_count, sizeof(struct feed_entry *), &compare_feeds_initial); break; case SORT_BY_ALPHABET_ASC: qsort(feeds, feeds_count, sizeof(struct feed_entry *), &compare_feeds_alphabet); break; } pthread_mutex_unlock(&interface_lock); if (we_are_already_in_feeds_menu == true) { expose_all_visible_entries_of_the_list_menu(); if (need_status_message) { info_status(get_sorting_message(feeds_sort), "feeds"); } } } struct menu_state * feeds_menu_loop(struct menu_state *m) { m->enumerator = &is_feed_valid; m->printer = &list_menu_writer; m->get_args = &get_feed_args; m->paint_action = &paint_feed; m->unread_state = &is_feed_unread; m->failed_state = &is_feed_failed; m->entry_format = get_cfg_wstring(NULL, CFG_MENU_FEED_ENTRY_FORMAT); if (m->feeds_count < 1) { info_status("There are no feeds in this section"); return close_menu(); } else if (!(m->flags & MENU_DISABLE_SETTINGS)) { // Don't set the menu names here because it's redundant! if (get_cfg_bool(NULL, CFG_FEEDS_MENU_PARAMOUNT_EXPLORE) && db_count_items(m->feeds_original, m->feeds_count, false)) { return setup_menu(&items_menu_loop, NULL, m->feeds_original, m->feeds_count, MENU_IS_EXPLORE, NULL); } else if (m->feeds_count == 1 && db_count_items(m->feeds_original, m->feeds_count, false)) { return setup_menu(&items_menu_loop, NULL, m->feeds_original, m->feeds_count, MENU_SWALLOW, NULL); } } if (m->is_initialized == false) { m->age = fetch_menu_age(); m->feeds = newsraft_malloc(sizeof(struct feed_entry *) * m->feeds_count); memcpy(m->feeds, m->feeds_original, sizeof(struct feed_entry *) * m->feeds_count); sort_feeds(m, get_sorting_id(get_cfg_string(NULL, CFG_MENU_FEED_SORTING)->ptr), false); } start_menu(); const struct wstring *arg; while (true) { if (get_cfg_bool(NULL, CFG_MENU_RESPONSIVENESS) && m->age != fetch_menu_age()) { m->age = fetch_menu_age(); sort_feeds(m, feeds_sort, true); } input_id cmd = get_input(m->feeds[m->view_sel]->binds, NULL, &arg); if (handle_list_menu_control(m, cmd, arg) == true) { continue; } switch (cmd) { case INPUT_MARK_READ: mark_feeds_read(m->feeds + m->view_sel, 1, true); break; case INPUT_MARK_UNREAD: mark_feeds_read(m->feeds + m->view_sel, 1, false); break; case INPUT_MARK_READ_ALL: mark_feeds_read(m->feeds, m->feeds_count, true); break; case INPUT_MARK_UNREAD_ALL: mark_feeds_read(m->feeds, m->feeds_count, false); break; case INPUT_RELOAD: queue_updates(m->feeds + m->view_sel, 1); break; case INPUT_RELOAD_ALL: queue_updates(m->feeds_original, m->feeds_count); break; case INPUT_QUIT_HARD: return NULL; case INPUT_ENTER: return setup_menu(&items_menu_loop, NULL, m->feeds + m->view_sel, 1, MENU_NORMAL, NULL); case INPUT_TOGGLE_EXPLORE_MODE: return setup_menu(&items_menu_loop, NULL, m->feeds, m->feeds_count, MENU_IS_EXPLORE, NULL); case INPUT_APPLY_SEARCH_MODE_FILTER: return setup_menu(&items_menu_loop, NULL, m->feeds, m->feeds_count, MENU_IS_SEARCH | MENU_IS_EXPLORE, NULL); case INPUT_NAVIGATE_BACK: if (get_menu_depth() < 2) break; // fall through case INPUT_QUIT_SOFT: return close_menu(); case INPUT_SORT_BY_UNREAD: sort_feeds(m, feeds_sort == SORT_BY_UNREAD_DESC ? SORT_BY_UNREAD_ASC : SORT_BY_UNREAD_DESC, true); break; case INPUT_SORT_BY_INITIAL: sort_feeds(m, feeds_sort == SORT_BY_INITIAL_ASC ? SORT_BY_INITIAL_DESC : SORT_BY_INITIAL_ASC, true); break; case INPUT_SORT_BY_ALPHABET: sort_feeds(m, feeds_sort == SORT_BY_ALPHABET_ASC ? SORT_BY_ALPHABET_DESC : SORT_BY_ALPHABET_ASC, true); break; case INPUT_FIND_COMMAND: return setup_menu(&items_menu_loop, NULL, m->feeds_original, m->feeds_count, MENU_IS_EXPLORE, arg); case INPUT_DATABASE_COMMAND: db_perform_user_edit(arg, m->feeds + m->view_sel, 1, NULL); break; case INPUT_VIEW_ERRORS: return setup_menu(&errors_pager_loop, NULL, m->feeds + m->view_sel, 1, MENU_NORMAL, NULL); } } return NULL; } newsraft/src/input.h000066400000000000000000000131051516312403600150070ustar00rootroot00000000000000#ifndef INPUT_H #define INPUT_H struct input_entry { const char *names[10]; const char *default_binds[10]; }; #define INPUT(NAME, ...) NAME, enum { #endif // INPUT_H #ifdef INPUT_ARRAY #define INPUT(NAME, ...) [NAME] = {__VA_ARGS__}, static struct input_entry inputs[] = { #endif // INPUT_ARRAY INPUT(INPUT_SELECT_NEXT, {"select-next"}, {"j", "KEY_DOWN", "^E"}) INPUT(INPUT_SELECT_PREV, {"select-prev"}, {"k", "KEY_UP", "^Y"}) INPUT(INPUT_SELECT_NEXT_PAGE, {"select-next-page"}, {"space", "^F", "KEY_NPAGE"}) INPUT(INPUT_SELECT_NEXT_PAGE_HALF, {"select-next-page-half"}, {"^D"}) INPUT(INPUT_SELECT_PREV_PAGE, {"select-prev-page"}, {"^B", "KEY_PPAGE"}) INPUT(INPUT_SELECT_PREV_PAGE_HALF, {"select-prev-page-half"}, {"^U"}) INPUT(INPUT_SELECT_FIRST, {"select-first"}, {"g", "KEY_HOME"}) INPUT(INPUT_SELECT_LAST, {"select-last"}, {"G", "KEY_END"}) INPUT(INPUT_JUMP_TO_NEXT, {"jump-to-next"}, {"J"}) INPUT(INPUT_JUMP_TO_PREV, {"jump-to-prev"}, {"K"}) INPUT(INPUT_JUMP_TO_NEXT_UNREAD, {"next-unread", "jump-to-next-unread"}, {"n"}) INPUT(INPUT_JUMP_TO_PREV_UNREAD, {"prev-unread", "jump-to-prev-unread"}, {"N"}) INPUT(INPUT_JUMP_TO_NEXT_IMPORTANT, {"next-important", "jump-to-next-important"}, {"p"}) INPUT(INPUT_JUMP_TO_PREV_IMPORTANT, {"prev-important", "jump-to-prev-important"}, {"P"}) INPUT(INPUT_JUMP_TO_NEXT_ERROR, {"next-error", "jump-to-next-error"}, {"e"}) INPUT(INPUT_JUMP_TO_PREV_ERROR, {"prev-error", "jump-to-prev-error"}, {"E"}) INPUT(INPUT_GOTO_FEED, {"goto-feed"}, {"*"}) INPUT(INPUT_SHIFT_WEST, {"shift-west"}, {","}) INPUT(INPUT_SHIFT_EAST, {"shift-east"}, {"."}) INPUT(INPUT_SHIFT_RESET, {"shift-reset"}, {"<"}) INPUT(INPUT_SORT_BY_TIME, {"sort-by-time"}, {"t"}) INPUT(INPUT_SORT_BY_TIME_DOWNLOAD, {"sort-by-time-download"}, {/* not set by default */}) INPUT(INPUT_SORT_BY_TIME_PUBLICATION, {"sort-by-time-publication"}, {/* not set by default */}) INPUT(INPUT_SORT_BY_TIME_UPDATE, {"sort-by-time-update"}, {/* not set by default */}) INPUT(INPUT_SORT_BY_ROWID, {"sort-by-rowid"}, {"w"}) INPUT(INPUT_SORT_BY_UNREAD, {"sort-by-unread"}, {"u"}) INPUT(INPUT_SORT_BY_INITIAL, {"sort-by-initial"}, {"z"}) INPUT(INPUT_SORT_BY_ALPHABET, {"sort-by-alphabet"}, {"a"}) INPUT(INPUT_SORT_BY_IMPORTANT, {"sort-by-important"}, {"i"}) INPUT(INPUT_ENTER, {"enter"}, {"l", "enter", "KEY_RIGHT", "KEY_ENTER"}) INPUT(INPUT_RELOAD, {"reload"}, {"r"}) INPUT(INPUT_RELOAD_ALL, {"reload-all"}, {"R", "^R"}) INPUT(INPUT_MARK_READ, {"read", "mark-read"}, {/* see assign_default_binds() */}) INPUT(INPUT_MARK_UNREAD, {"unread", "mark-unread"}, {/* see assign_default_binds() */}) INPUT(INPUT_MARK_READ_ALL, {"read-all", "mark-read-all"}, {"A"}) INPUT(INPUT_MARK_UNREAD_ALL, {"unread-all", "mark-unread-all"}, {/* not set by default */}) INPUT(INPUT_MARK_IMPORTANT, {"important", "mark-important"}, {"f"}) INPUT(INPUT_MARK_UNIMPORTANT, {"unimportant", "mark-unimportant"}, {"F"}) INPUT(INPUT_TOGGLE_READ, {"toggle-read"}, {/* not set by default */}) INPUT(INPUT_TOGGLE_IMPORTANT, {"toggle-important"}, {/* not set by default */}) INPUT(INPUT_TOGGLE_EXPLORE_MODE, {"explore", "toggle-explore-mode"}, {"tab"}) INPUT(INPUT_VIEW_ERRORS, {"view-errors"}, {"v"}) INPUT(INPUT_OPEN_IN_BROWSER, {"open-in-browser"}, {"o"}) INPUT(INPUT_COPY_TO_CLIPBOARD, {"copy-to-clipboard"}, {"y", "c"}) INPUT(INPUT_START_SEARCH_INPUT, {"start-search-input"}, {"/"}) INPUT(INPUT_CLEAN_STATUS, {"clean-status"}, {"`"}) INPUT(INPUT_NAVIGATE_BACK, {"return", "navigate-back"}, {"h", "backspace", "KEY_LEFT", "KEY_BACKSPACE"}) INPUT(INPUT_QUIT_SOFT, {"quit"}, {"q"}) INPUT(INPUT_QUIT_HARD, {"quit-hard"}, {"Q"}) INPUT(INPUT_FIND_COMMAND, {"find"}, {}) INPUT(INPUT_SYSTEM_COMMAND, {"exec"}, {}) INPUT(INPUT_DATABASE_COMMAND, {"edit"}, {}) INPUT(INPUT_APPLY_SEARCH_MODE_FILTER, {}, {}) INPUT(INPUT_EMPTY /* does nothing */, {}, {}) INPUT(INPUT_ERROR, {}, {}) #ifdef INPUT }; #endif #undef INPUT newsraft/src/insert_feed/000077500000000000000000000000001516312403600157665ustar00rootroot00000000000000newsraft/src/insert_feed/insert-feed-data.c000066400000000000000000000033261516312403600212520ustar00rootroot00000000000000#include "insert_feed/insert_feed.h" bool insert_feed_data(const struct feed_entry *feed_entry, struct getfeed_feed *feed) { sqlite3_stmt *s = db_prepare("INSERT OR REPLACE INTO feeds(feed_url,title,link,content,attachments,persons,extras,download_date,update_date,time_to_live,http_header_etag,http_header_last_modified,http_header_expires) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)", 221, NULL); if (s == NULL) { return false; } db_bind_feed_url(s, 1 + FEED_COLUMN_FEED_URL, feed_entry->url); db_bind_string(s, 1 + FEED_COLUMN_TITLE, feed->title); db_bind_string(s, 1 + FEED_COLUMN_LINK, feed->link); db_bind_string(s, 1 + FEED_COLUMN_CONTENT, feed->content); db_bind_string(s, 1 + FEED_COLUMN_ATTACHMENTS, feed->attachments); db_bind_string(s, 1 + FEED_COLUMN_PERSONS, feed->persons); db_bind_string(s, 1 + FEED_COLUMN_EXTRAS, feed->extras); sqlite3_bind_int64(s, 1 + FEED_COLUMN_DOWNLOAD_DATE, feed_entry->update_date); sqlite3_bind_int64(s, 1 + FEED_COLUMN_UPDATE_DATE, feed_entry->update_date); sqlite3_bind_int64(s, 1 + FEED_COLUMN_TIME_TO_LIVE, feed->time_to_live); db_bind_string(s, 1 + FEED_COLUMN_HTTP_HEADER_ETAG, feed->http_header_etag); sqlite3_bind_int64(s, 1 + FEED_COLUMN_HTTP_HEADER_LAST_MODIFIED, feed->http_header_last_modified); sqlite3_bind_int64(s, 1 + FEED_COLUMN_HTTP_HEADER_EXPIRES, feed->http_header_expires); if (sqlite3_step(s) != SQLITE_DONE) { FAIL("Failed to insert or replace feed data: %s", db_error_string()); sqlite3_finalize(s); return false; } sqlite3_finalize(s); return true; } newsraft/src/insert_feed/insert-item-data.c000066400000000000000000000107251516312403600213060ustar00rootroot00000000000000#include #include "insert_feed/insert_feed.h" bool delete_excess_items(struct feed_entry *feed, int64_t limit) { char query[300] = "DELETE FROM items WHERE rowid IN " "(SELECT rowid FROM items WHERE feed_url=? ORDER BY publication_date DESC, update_date DESC, download_date DESC, rowid DESC LIMIT -1 OFFSET ?)"; if (get_cfg_bool(&feed->cfg, CFG_ITEM_LIMIT_UNREAD) == false) { strcat(query, " AND unread=0"); } if (get_cfg_bool(&feed->cfg, CFG_ITEM_LIMIT_IMPORTANT) == false) { strcat(query, " AND important=0"); } INFO("Deleting excess items..."); sqlite3_stmt *s = db_prepare(query, strlen(query) + 1, NULL); if (s == NULL) { FAIL("Failed to prepare an excess items deletion statement!"); return false; } db_bind_feed_url(s, 1, feed->url); sqlite3_bind_int64(s, 2, limit); sqlite3_step(s); sqlite3_finalize(s); return true; } static inline bool db_insert_item(struct feed_entry *feed, struct getfeed_item *item, int64_t rowid) { sqlite3_stmt *s; if (rowid == -1) { s = db_prepare("INSERT INTO items(feed_url,guid,title,link,content,attachments,persons,extras,download_date,publication_date,update_date,unread) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", 161, NULL); } else { if (get_cfg_bool(&feed->cfg, CFG_MARK_ITEM_UNREAD_ON_CHANGE) == true) { s = db_prepare("UPDATE items SET feed_url=?,guid=?,title=?,link=?,content=?,attachments=?,persons=?,extras=?,download_date=?,publication_date=?,update_date=?,unread=1 WHERE rowid=?", 165, NULL); } else { s = db_prepare("UPDATE items SET feed_url=?,guid=?,title=?,link=?,content=?,attachments=?,persons=?,extras=?,download_date=?,publication_date=?,update_date=? WHERE rowid=?", 156, NULL); } } if (s == NULL) { FAIL("Failed to prepare item insertion/update statement!"); return false; } db_bind_feed_url(s, 1 + ITEM_COLUMN_FEED_URL, feed->url); db_bind_string(s, 1 + ITEM_COLUMN_GUID, item->guid); db_bind_string(s, 1 + ITEM_COLUMN_TITLE, item->title); db_bind_string(s, 1 + ITEM_COLUMN_LINK, STRING_IS_EMPTY(item->link) && item->guid_is_link == true ? item->guid : item->link); db_bind_string(s, 1 + ITEM_COLUMN_CONTENT, item->content); db_bind_string(s, 1 + ITEM_COLUMN_ATTACHMENTS, item->attachments); db_bind_string(s, 1 + ITEM_COLUMN_PERSONS, item->persons); db_bind_string(s, 1 + ITEM_COLUMN_EXTRAS, item->extras); sqlite3_bind_int64(s, 1 + ITEM_COLUMN_DOWNLOAD_DATE, feed->update_date); sqlite3_bind_int64(s, 1 + ITEM_COLUMN_PUBLICATION_DATE, item->publication_date); sqlite3_bind_int64(s, 1 + ITEM_COLUMN_UPDATE_DATE, item->update_date); if (rowid == -1) { sqlite3_bind_int(s, 1 + ITEM_COLUMN_UNREAD, get_cfg_bool(&feed->cfg, CFG_READ_ON_ARRIVAL) ? 0 : 1); } else { sqlite3_bind_int64(s, 2 + ITEM_COLUMN_UPDATE_DATE, rowid); } if (sqlite3_step(s) != SQLITE_DONE) { if (rowid == -1) { FAIL("Item insertion failed: %s", db_error_string()); } else { FAIL("Item update failed: %s", db_error_string()); } sqlite3_finalize(s); return false; } sqlite3_finalize(s); return true; } bool insert_item_data(struct feed_entry *feed, struct getfeed_item *item) { // Create guid if it was not set. if (STRING_IS_EMPTY(item->guid)) { if (!STRING_IS_EMPTY(item->link)) { cpyss(&item->guid, item->link); } else if (!STRING_IS_EMPTY(item->title)) { cpyss(&item->guid, item->title); } else if (!STRING_IS_EMPTY(item->content)) { newsraft_simple_hash(&item->guid, item->content->ptr); } else { WARN("Couldn't generate GUID for the item!"); return true; // Probably this item is just empty. Ignore it. } } // Before trying to write some item to the database we have to check if this // item is duplicate or not. sqlite3_stmt *s = db_prepare("SELECT rowid,content FROM items WHERE feed_url=? AND guid=? LIMIT 1", 68, NULL); if (s == NULL) { FAIL("Failed to prepare SELECT statement for searching item duplicate by guid!"); return false; } db_bind_feed_url(s, 1, feed->url); db_bind_string(s, 2, item->guid); int64_t item_rowid; if (sqlite3_step(s) == SQLITE_ROW) { const char *content = (char *)sqlite3_column_text(s, 1); if (((content == NULL) && (item->content == NULL)) || ((content != NULL) && (item->content != NULL) && (strcmp(content, item->content->ptr) == 0))) { sqlite3_finalize(s); return true; } item_rowid = sqlite3_column_int64(s, 0); } else { item_rowid = -1; } sqlite3_finalize(s); return db_insert_item(feed, item, item_rowid); } newsraft/src/insert_feed/insert_feed.c000066400000000000000000000011321516312403600204160ustar00rootroot00000000000000#include "insert_feed/insert_feed.h" bool insert_feed(struct feed_entry *feed, struct getfeed_feed *feed_data) { if (insert_feed_data(feed, feed_data) == false) { FAIL("Failed to insert feed data!"); return false; } for (struct getfeed_item *item = feed_data->item; item != NULL; item = item->next) { if (insert_item_data(feed, item) == false) { FAIL("Failed to insert item data!"); return false; } } size_t limit = get_cfg_uint(&feed->cfg, CFG_ITEM_LIMIT); if (limit > 0 && delete_excess_items(feed, limit) == false) { WARN("Failed to delete excess items!"); } return true; } newsraft/src/insert_feed/insert_feed.h000066400000000000000000000004771516312403600204360ustar00rootroot00000000000000#ifndef INSERT_FEED_H #define INSERT_FEED_H #include "newsraft.h" bool delete_excess_items(struct feed_entry *feed, int64_t limit); bool insert_feed_data(const struct feed_entry *feed_entry, struct getfeed_feed *feed); bool insert_item_data(struct feed_entry *feed, struct getfeed_item *item); #endif // INSERT_FEED_H newsraft/src/inserter.c000066400000000000000000000101761516312403600155030ustar00rootroot00000000000000#include #include #include #include "newsraft.h" static bool engage_with_not_inserted_feed(struct feed_update_state *data) { if (data->is_finished == false && (data->is_downloaded == true || data->is_canceled == true || data->is_failed == true)) { return true; } return false; } void * inserter_worker(void *dummy) { (void)dummy; time_t last_auto_update = 0; while (they_want_us_to_stop == false) { if (time(NULL) - last_auto_update > 60) { process_auto_updating_feeds(); last_auto_update = time(NULL); } struct feed_update_state *target = queue_pull(&engage_with_not_inserted_feed); if (target == NULL) { threads_take_a_nap(NEWSRAFT_THREAD_DBWRITER); continue; } struct feed_entry *feed = target->feed_entry; if (target->is_failed) { str_appendf(target->new_errors, "Feed update failed!\n"); if (strstr(target->new_errors->ptr, "unknown encoding")) { str_appendf( target->new_errors, "\n" "Looks like Newsraft couldn't recognize the feed encoding.\n" "Please note that Newsraft only supports UTF-8 encoding!\n" "\n" "If you need to read a feed encoded with anything other than UTF-8,\n" "you'll need to do the conversion yourself using a shell interlayer:\n" "\n" "$(curl -s %s | iconv -f FROM -t UTF-8 - | tail -n +2)\n" "\n" "You can add a feed like this to your feed file, and it will\n" "do all the conversion shenanigans automatically on each reload.\n", target->feed_entry->url->ptr ); } } else { if (target->is_canceled == false) { if (!insert_feed(feed, &target->feed)) { str_appendf(target->new_errors, "Feed insertion failed!\n"); } } else { db_update_feed_int64(feed->url, "update_date", feed->update_date, true); // RFC9111, Section 4.3.4 // For each stored response identified, the cache MUST update its // header fields with the header fields provided in the 304 (Not // Modified) response, as per Section 3.2. if (target->http_response_code == NEWSRAFT_HTTP_NOT_MODIFIED) { db_update_feed_string(feed->url, "http_header_etag", target->feed.http_header_etag, true); db_update_feed_int64(feed->url, "http_header_last_modified", target->feed.http_header_last_modified, true); db_update_feed_int64(feed->url, "http_header_expires", target->feed.http_header_expires, true); } } } // Interface routines actively access feed's name and errors strings in other threads. // The lock is here to prevent race condition which can potentially lead to segmentation fault. // TODO: make it less ugly perhaps? pthread_mutex_lock(&interface_lock); bool need_redraw = false; if (target->new_errors->len == 0) { if (feed->errors->len > 0) { need_redraw = true; } empty_string(feed->errors); } else { if (feed->errors->len == 0) { need_redraw = true; } catss(feed->errors, target->new_errors); } size_t errors_len_before_count = feed->errors->len; int64_t new_unread_count = db_count_items(&feed, 1, true); int64_t new_items_count = db_count_items(&feed, 1, false); size_t errors_len_after_count = feed->errors->len; if (new_unread_count >= 0 && new_unread_count != feed->unread_count) { if (new_unread_count > feed->unread_count) { target->new_items_count = new_unread_count - feed->unread_count; } feed->unread_count = new_unread_count; need_redraw = true; } if (new_items_count >= 0 && new_items_count != feed->items_count) { feed->items_count = new_items_count; need_redraw = true; } if (errors_len_before_count != errors_len_after_count) { need_redraw = true; } if (STRING_IS_EMPTY(feed->name)) { struct string *title = db_get_string_from_feed_table(feed->url, "title", 5); if (title != NULL) { inlinefy_string(title); cpyss(&feed->name, title); free_string(title); need_redraw = true; } } pthread_mutex_unlock(&interface_lock); if (ui_is_running() && need_redraw == true) { refresh_sections_statistics_about_underlying_feeds(); expose_all_visible_entries_of_the_list_menu(); } target->is_finished = true; queue_examine(); } return NULL; } newsraft/src/interface-errors-pager.c000066400000000000000000000034211516312403600202110ustar00rootroot00000000000000#include "newsraft.h" struct menu_state * errors_pager_loop(struct menu_state *m) { if (m->feeds_original == NULL || m->feeds_count < 1) { return close_menu(); } m->enumerator = &is_pager_pos_valid; m->printer = &pager_menu_writer; struct render_blocks_list blocks = {NULL, 0, {}}; pthread_mutex_lock(&interface_lock); for (size_t i = 0; i < m->feeds_count; ++i) { struct feed_entry *feed = m->feeds_original[i]; if (feed->errors->len < 1) { continue; } blocks.ptr = newsraft_realloc(blocks.ptr, sizeof(struct render_block) * (blocks.len + 2)); blocks.len += 2; struct string *header_str = blocks.len > 2 ? crtas("


", 12) : crtes(100); catas(header_str, "Errors of ", 13); catss(header_str, feed->url); catas(header_str, "

", 12); struct render_block *header_block = &blocks.ptr[blocks.len - 2]; struct render_block *content_block = &blocks.ptr[blocks.len - 1]; header_block->content = convert_string_to_wstring(header_str); header_block->content_type = TEXT_HTML; header_block->needs_trimming = false; content_block->content = convert_string_to_wstring(feed->errors); content_block->content_type = TEXT_RAW; content_block->needs_trimming = false; free_string(header_str); } pthread_mutex_unlock(&interface_lock); if (start_pager_menu(NULL, &blocks) == false) { free_render_blocks(&blocks); return close_menu(); } start_menu(); const struct wstring *arg; while (true) { input_id cmd = get_input(NULL, NULL, &arg); if (handle_pager_menu_control(cmd) == true) { // Rest a little. } else if (cmd == INPUT_NAVIGATE_BACK || cmd == INPUT_QUIT_SOFT) { break; } else if (cmd == INPUT_QUIT_HARD) { free_render_blocks(&blocks); return NULL; } } free_render_blocks(&blocks); return close_menu(); } newsraft/src/interface-list-pager.c000066400000000000000000000017501516312403600176530ustar00rootroot00000000000000#include #include "newsraft.h" static struct config_context **pager_context = NULL; static struct render_result content = {0}; static struct render_blocks_list *pager_blocks = NULL; bool is_pager_pos_valid(struct menu_state *ctx, size_t index) { (void)ctx; return index < content.lines_len; } void pager_menu_writer(size_t index, WINDOW *w) { if (index < content.lines_len) { for (size_t i = 0; i < content.lines[index].indent; ++i) { waddnwstr(w, L" ", 1); } for (size_t i = 0; i < content.lines[index].ws->len; ++i) { wattrset(w, content.lines[index].hints[i]); waddnwstr(w, content.lines[index].ws->ptr + i, 1); } wattrset(w, 0); } } bool start_pager_menu(struct config_context **new_ctx, struct render_blocks_list *new_blocks) { pager_context = new_ctx; pager_blocks = new_blocks; return refresh_pager_menu(); } bool refresh_pager_menu(void) { free_render_result(&content); return render_data(pager_context, &content, pager_blocks, list_menu_width); } newsraft/src/interface-list.c000066400000000000000000000360401516312403600165570ustar00rootroot00000000000000#include #include #include "newsraft.h" // Note to the future. // TODO: explain why we need all these *_unprotected functions size_t list_menu_height; size_t list_menu_width; static WINDOW **windows = NULL; static size_t windows_count = 0; static size_t scrolloff; static size_t horizontal_shift = 0; static struct wstring *list_fmtout = NULL; static uint64_t menu_age = 0; static struct menu_state *menus = NULL; static struct menu_state *menu = NULL; bool is_current_menu_a_pager(void) { return menu != NULL && menu->enumerator == &is_pager_pos_valid ? true : false; } bool adjust_list_menu(void) { INFO("Adjusting list menu."); for (size_t i = 0; i < windows_count; ++i) { delwin(windows[i]); } windows = newsraft_realloc(windows, sizeof(WINDOW *) * (list_menu_height + 1)); for (size_t i = 0; i < list_menu_height; ++i) { windows[i] = newwin(i); } windows_count = list_menu_height; scrolloff = get_cfg_uint(NULL, CFG_SCROLLOFF); if (scrolloff > (list_menu_height / 2)) { scrolloff = list_menu_height / 2; } wstr_set(&list_fmtout, NULL, 0, 200); return true; } void free_list_menu(void) { for (size_t i = 0; i < windows_count; ++i) { delwin(windows[i]); } newsraft_free(windows); free_wstring(list_fmtout); } void list_menu_writer(size_t index, WINDOW *w) { if (menu->enumerator(menu, index) == true) { do_format(list_fmtout, menu->entry_format->ptr, menu->get_args(menu, index)); if (list_fmtout->len > horizontal_shift) { waddwstr(w, list_fmtout->ptr + horizontal_shift); } wbkgd(w, menu->paint_action(menu, index, index == menu->view_sel)); } } static inline void expose_entry_of_the_list_menu_unprotected(size_t index) { if (index < menu->view_min || (index - menu->view_min) >= windows_count) { WARN("Ignoring list entry expose because of zero size."); return; } WINDOW *w = windows[index - menu->view_min]; werase(w); wmove(w, 0); wbkgd(w, (struct config_color){TB_DEFAULT, TB_DEFAULT, TB_DEFAULT}); wattrset(w, TB_DEFAULT); menu->printer(index, w); } void expose_entry_of_the_list_menu(size_t index) { pthread_mutex_lock(&interface_lock); if (index >= menu->view_min && index <= menu->view_max) { expose_entry_of_the_list_menu_unprotected(index); tb_present(); } pthread_mutex_unlock(&interface_lock); } static inline void expose_all_visible_entries_of_the_list_menu_unprotected(void) { for (size_t i = menu->view_min; i <= menu->view_max; ++i) { expose_entry_of_the_list_menu_unprotected(i); } tb_present(); update_status_window_content_unprotected(); } void expose_all_visible_entries_of_the_list_menu(void) { pthread_mutex_lock(&interface_lock); expose_all_visible_entries_of_the_list_menu_unprotected(); pthread_mutex_unlock(&interface_lock); } void redraw_list_menu_unprotected(void) { if (list_menu_height < 1) { WARN("Ignoring list view redraw because of zero size."); return; } menu->view_max = menu->view_min + (list_menu_height - 1); if (menu->view_sel > menu->view_max) { menu->view_max = menu->view_sel; menu->view_min = menu->view_max - (list_menu_height - 1); } while (menu->view_max >= list_menu_height && menu->enumerator(menu, menu->view_max) == false) { menu->view_max -= 1; } menu->view_min = menu->view_max - (list_menu_height - 1); expose_all_visible_entries_of_the_list_menu_unprotected(); } void reset_list_menu_unprotected(void) { if (menu->enumerator(menu, menu->view_sel) == false) { menu->view_sel = 0; menu->view_min = 0; } redraw_list_menu_unprotected(); } static size_t obtain_list_entries_count_unprotected(struct menu_state *m) { size_t i = 0; while (m->enumerator(m, i) == true) { i += 1; } return i; } static void change_list_view_unprotected(struct menu_state *m, size_t new_sel, bool is_wrappable) { if (list_menu_height < 1) { WARN("Ignoring list view change because of zero size."); return; } if (is_wrappable && get_cfg_bool(NULL, CFG_SCROLLWRAP)) { if (new_sel > m->view_sel) { // Tried to go forward if (m->enumerator(m, m->view_sel) && !m->enumerator(m, m->view_sel + 1)) { new_sel = 0; } } else if (new_sel == 0 && m->view_sel == 0) { // Tried to go backward new_sel = obtain_list_entries_count_unprotected(m); } } while (m->enumerator(m, new_sel) == false && new_sel > 0) { new_sel -= 1; } if (m->enumerator(m, new_sel) == false) { return; } if (new_sel + scrolloff > m->view_max) { m->view_max = new_sel + scrolloff; while (m->view_max >= list_menu_height && m->enumerator(m, m->view_max) == false) { m->view_max -= 1; } m->view_min = m->view_max - (list_menu_height - 1); m->view_sel = new_sel; if (m == menu) { expose_all_visible_entries_of_the_list_menu_unprotected(); } } else if (((new_sel >= scrolloff) && (new_sel - scrolloff < m->view_min)) || ((new_sel < scrolloff) && (m->view_min > 0))) { if (new_sel >= scrolloff) { m->view_min = new_sel - scrolloff; if ((scrolloff == list_menu_height / 2) && (list_menu_height % 2 == 0)) { // Makes scrolling with huge scrolloff work consistently in both direcetions. m->view_min += 1; } } else { m->view_min = 0; } m->view_max = m->view_min + (list_menu_height - 1); m->view_sel = new_sel; if (m == menu) { expose_all_visible_entries_of_the_list_menu_unprotected(); } } else if (new_sel != m->view_sel) { if (m == menu) { wbkgd(windows[m->view_sel - m->view_min], m->paint_action(m, m->view_sel, false)); m->view_sel = new_sel; wbkgd(windows[m->view_sel - m->view_min], m->paint_action(m, m->view_sel, true)); tb_present(); } else { m->view_sel = new_sel; } } } static void change_list_view_filtered_unprotected(struct menu_state *m, bool (*filter)(struct menu_state *, size_t), bool is_forward) { if (!filter) { return; } if (is_forward) { for (size_t i = m->view_sel + 1, j = 0; j < 2; i = 0, ++j) { while (m->enumerator(m, i) == true) { if (filter(m, i) == true) { change_list_view_unprotected(m, i, false); j = 2; break; } i += 1; } if (!get_cfg_bool(NULL, CFG_SCROLLWRAP)) { break; } } } else { for (size_t i = m->view_sel, j = 0; j < 2; ++j) { while (i > 0 && m->enumerator(m, i - 1) == true) { if (filter(m, i - 1) == true) { change_list_view_unprotected(m, i - 1, false); j = 2; break; } i -= 1; } if (j < 2) { i = obtain_list_entries_count_unprotected(m); } if (!get_cfg_bool(NULL, CFG_SCROLLWRAP)) { break; } } } } bool handle_list_menu_control(struct menu_state *m, input_id cmd, const struct wstring *arg) { pthread_mutex_lock(&interface_lock); if (cmd == INPUT_SELECT_NEXT || cmd == INPUT_JUMP_TO_NEXT) { change_list_view_unprotected(m, m->view_sel + 1, true); } else if (cmd == INPUT_SELECT_PREV || cmd == INPUT_JUMP_TO_PREV) { change_list_view_unprotected(m, m->view_sel > 1 ? m->view_sel - 1 : 0, true); } else if (cmd == INPUT_JUMP_TO_NEXT_UNREAD) { change_list_view_filtered_unprotected(m, m->unread_state, true); } else if (cmd == INPUT_JUMP_TO_PREV_UNREAD) { change_list_view_filtered_unprotected(m, m->unread_state, false); } else if (cmd == INPUT_JUMP_TO_NEXT_IMPORTANT && m->run == &items_menu_loop) { change_list_view_filtered_unprotected(m, important_item_condition, true); } else if (cmd == INPUT_JUMP_TO_PREV_IMPORTANT && m->run == &items_menu_loop) { change_list_view_filtered_unprotected(m, important_item_condition, false); } else if (cmd == INPUT_JUMP_TO_NEXT_ERROR) { change_list_view_filtered_unprotected(m, m->failed_state, true); } else if (cmd == INPUT_JUMP_TO_PREV_ERROR) { change_list_view_filtered_unprotected(m, m->failed_state, false); } else if (cmd == INPUT_SELECT_NEXT_PAGE) { change_list_view_unprotected(m, m->view_sel + list_menu_height, true); } else if (cmd == INPUT_SELECT_NEXT_PAGE_HALF) { change_list_view_unprotected(m, m->view_sel + list_menu_height / 2, true); } else if (cmd == INPUT_SELECT_PREV_PAGE) { change_list_view_unprotected(m, m->view_sel > list_menu_height ? m->view_sel - list_menu_height : 0, true); } else if (cmd == INPUT_SELECT_PREV_PAGE_HALF) { size_t step = list_menu_height / 2; change_list_view_unprotected(m, m->view_sel > step ? m->view_sel - step : 0, true); } else if (cmd == INPUT_SELECT_FIRST) { change_list_view_unprotected(m, 0, false); } else if (cmd == INPUT_SELECT_LAST) { change_list_view_unprotected(m, obtain_list_entries_count_unprotected(m), false); } else if (cmd == INPUT_SHIFT_WEST) { size_t shift_delta = 1 + list_menu_width / 50; if (horizontal_shift >= shift_delta) { horizontal_shift -= shift_delta; expose_all_visible_entries_of_the_list_menu_unprotected(); } else if (horizontal_shift > 0) { horizontal_shift = 0; expose_all_visible_entries_of_the_list_menu_unprotected(); } } else if (cmd == INPUT_SHIFT_EAST) { horizontal_shift += 1 + list_menu_width / 50; expose_all_visible_entries_of_the_list_menu_unprotected(); } else if (cmd == INPUT_SHIFT_RESET) { if (horizontal_shift != 0) { horizontal_shift = 0; expose_all_visible_entries_of_the_list_menu_unprotected(); } } else if (cmd == INPUT_CLEAN_STATUS) { status_clean_unprotected(); } else if (cmd == INPUT_SYSTEM_COMMAND) { pthread_mutex_unlock(&interface_lock); run_formatted_command(arg, m->get_args(m, m->view_sel)); return true; } else { pthread_mutex_unlock(&interface_lock); return false; } pthread_mutex_unlock(&interface_lock); return true; } static void change_pager_view_unprotected(size_t new_sel) { if (list_menu_height < 1) { WARN("Ignoring pager view change because of zero size."); return; } while (menu->enumerator(menu, new_sel) == false && new_sel > 0) { new_sel -= 1; } if (menu->enumerator(menu, new_sel) == false) { return; } size_t entries_count = obtain_list_entries_count_unprotected(menu); if (entries_count - new_sel < list_menu_height) { new_sel = entries_count > list_menu_height ? entries_count - list_menu_height : 0; } if (new_sel != menu->view_min) { menu->view_max = new_sel + (list_menu_height - 1); while (menu->view_max >= list_menu_height && menu->enumerator(menu, menu->view_max) == false) { menu->view_max -= 1; } menu->view_min = menu->view_max - (list_menu_height - 1); expose_all_visible_entries_of_the_list_menu_unprotected(); } } bool handle_pager_menu_control(input_id cmd) { pthread_mutex_lock(&interface_lock); if (cmd == INPUT_SELECT_NEXT || cmd == INPUT_ENTER) { change_pager_view_unprotected(menu->view_min + 1); } else if (cmd == INPUT_SELECT_PREV) { change_pager_view_unprotected(menu->view_min > 0 ? menu->view_min - 1 : 0); } else if (cmd == INPUT_SELECT_NEXT_PAGE) { change_pager_view_unprotected(menu->view_min + list_menu_height); } else if (cmd == INPUT_SELECT_NEXT_PAGE_HALF) { change_pager_view_unprotected(menu->view_min + list_menu_height / 2); } else if (cmd == INPUT_SELECT_PREV_PAGE) { change_pager_view_unprotected(menu->view_min > list_menu_height ? menu->view_min - list_menu_height : 0); } else if (cmd == INPUT_SELECT_PREV_PAGE_HALF) { size_t step = list_menu_height / 2; change_pager_view_unprotected(menu->view_min > step ? menu->view_min - step : 0); } else if (cmd == INPUT_SELECT_FIRST) { change_pager_view_unprotected(0); } else if (cmd == INPUT_SELECT_LAST) { change_pager_view_unprotected(obtain_list_entries_count_unprotected(menu)); } else if (cmd == INPUT_CLEAN_STATUS) { status_clean_unprotected(); } else { pthread_mutex_unlock(&interface_lock); return false; } pthread_mutex_unlock(&interface_lock); return true; } static inline void free_deleted_menus(void) { while (menus != NULL && menus->is_deleted == true) { struct menu_state *tmp = menus; menus = menus->prev; free_string(tmp->name); free_string(tmp->search_token); free_items_list(tmp->items); newsraft_free(tmp->feeds); newsraft_free(tmp); } } void free_menus(void) { while (menus != NULL) { struct menu_state *tmp = menus; menus = menus->prev; free_string(tmp->name); free_string(tmp->search_token); free_items_list(tmp->items); newsraft_free(tmp->feeds); newsraft_free(tmp); } } size_t get_menu_depth(void) { size_t depth = 0; for (struct menu_state *i = menus; i != NULL && i->is_deleted == false; i = i->prev) { depth += 1; } return depth; } static void update_unread_items_count_of_last_menu(void) { if (menus != NULL && menus->feeds_original != NULL && menus->feeds_count > 0) { for (size_t i = 0; i < menus->feeds_count; ++i) { menus->feeds_original[i]->unread_count = db_count_items(&menus->feeds_original[i], 1, true); } } } struct menu_state * setup_menu(struct menu_state *(*run)(struct menu_state *), const struct string *name, struct feed_entry **feeds, size_t feeds_count, uint32_t flags, const void *ctx) { pthread_mutex_lock(&interface_lock); update_unread_items_count_of_last_menu(); struct menu_state *new = newsraft_calloc(1, sizeof(*new)); new->run = run; new->feeds_original = feeds; new->feeds_count = feeds_count; new->flags = flags; new->prev = menus; new->find_filter = ctx; new->search_token = pop_search_filter(); if (!STRING_IS_EMPTY(name)) { cpyss(&new->name, name); } else if (!STRING_IS_EMPTY(new->search_token)) { cpyss(&new->name, new->search_token); } else if (new->find_filter) { new->name = convert_wstring_to_string(new->find_filter); } else if (feeds != NULL && feeds_count == 1) { cpyss(&new->name, STRING_IS_EMPTY(feeds[0]->name) ? feeds[0]->url : feeds[0]->name); } if ((flags & MENU_SWALLOW) && menus != NULL) { menus->is_deleted = true; } menus = new; pthread_mutex_unlock(&interface_lock); return menus; } struct menu_state * close_menu(void) { pthread_mutex_lock(&interface_lock); update_unread_items_count_of_last_menu(); struct menu_state *next_menu = NULL; for (struct menu_state *i = menus; i != NULL; i = i->prev) { if (i->is_deleted == false) { i->is_deleted = true; break; } } for (struct menu_state *i = menus; i != NULL; i = i->prev) { if (i->is_deleted == false) { next_menu = i; next_menu->flags |= MENU_DISABLE_SETTINGS; break; } } pthread_mutex_unlock(&interface_lock); return next_menu; } void start_menu(void) { pthread_mutex_lock(&interface_lock); free_deleted_menus(); menu = menus; // These methods are mandatory for all menus. assert(menu->enumerator); assert(menu->printer); horizontal_shift = 0; if (menu->is_initialized == false) { menu->view_sel = 0; menu->view_min = 0; status_clean_unprotected(); } redraw_list_menu_unprotected(); menu->is_initialized = true; pthread_mutex_unlock(&interface_lock); } void write_menu_path_string(struct string *names, struct menu_state *m) { if (menus == NULL) { return; } else if (m == NULL) { write_menu_path_string(names, menus); return; } else if (m->prev != NULL) { write_menu_path_string(names, m->prev); } if (m->is_deleted == false && m->name != NULL && m->name->len > 0) { catas(names, " > ", 5); catss(names, m->name); } } void raise_menu_age(void) { pthread_mutex_lock(&interface_lock); menu_age += 1; yield_control_to_menu(); pthread_mutex_unlock(&interface_lock); } uint64_t fetch_menu_age(void) { pthread_mutex_lock(&interface_lock); const uint64_t ret = menu_age; pthread_mutex_unlock(&interface_lock); return ret; } newsraft/src/interface-status.c000066400000000000000000000162341516312403600171320ustar00rootroot00000000000000#include #include "newsraft.h" static WINDOW *status_window = NULL; static volatile bool status_window_is_cleanable = true; static volatile bool status_window_is_initialized = false; static struct string *message_text = NULL; static config_entry_id message_color; static bool search_mode_is_enabled = false; static struct wstring *search_mode_text_input = NULL; // We take 999999999 as the maximum value for count variable to avoid overflow // of the uint32_t integer. The width of this number is hardcoded into the // terminal width limit, so when changing it here, consider changing the limit. // 9 (max length of input) + 1 (terminator) = 10 static char count_buf[10]; static uint8_t count_buf_len = 0; static const struct timespec input_polling_period = {0, 30000000}; // 0.03 seconds static volatile bool they_want_us_to_quit_waiting_for_input = false; void update_status_window_content_unprotected(void) { if (status_window_is_initialized != true) { return; } // FIXME: werase doesn't clear screen garbage, so sometimes artifacts // appear in the window. The solution to this is wclear, but it makes // screen flicker on the old hardware. werase(status_window); wmove(status_window, 0); if (they_want_us_to_stop == true) { waddwstr(status_window, L"Terminating..."); wbkgd(status_window, get_cfg_color(NULL, CFG_COLOR_STATUS_FAIL)); } else if (search_mode_is_enabled == true) { waddwstr(status_window, L"/"); waddwstr(status_window, search_mode_text_input->ptr); wbkgd(status_window, get_cfg_color(NULL, CFG_COLOR_STATUS)); } else if (!STRING_IS_EMPTY(message_text)) { waddstr(status_window, message_text->ptr); wbkgd(status_window, get_cfg_color(NULL, message_color)); } else { struct string *path = NULL; if (get_cfg_bool(NULL, CFG_STATUS_SHOW_MENU_PATH)) { path = crtes(100); write_menu_path_string(path, NULL); } if (path != NULL && path->len > 0) { waddstr(status_window, path->ptr); } else { waddwstr(status_window, get_cfg_wstring(NULL, CFG_STATUS_PLACEHOLDER)->ptr); } wbkgd(status_window, get_cfg_color(NULL, CFG_COLOR_STATUS)); free_string(path); } if (count_buf_len > 0 && list_menu_width >= 11) { wmove(status_window, list_menu_width - 11); waddwstr(status_window, L" "); // Little divider to separate from previous stuff waddstr(status_window, count_buf); } tb_present(); } static void update_status_window_content(void) { pthread_mutex_lock(&interface_lock); update_status_window_content_unprotected(); pthread_mutex_unlock(&interface_lock); } bool status_recreate_unprotected(void) { if (status_window != NULL) { delwin(status_window); } status_window = newwin(list_menu_height); INFO("Created status window"); status_window_is_initialized = true; update_status_window_content_unprotected(); return true; } void status_clean_unprotected(void) { if (status_window_is_cleanable == true && !STRING_IS_EMPTY(message_text)) { empty_string(message_text); update_status_window_content_unprotected(); } } void status_clean(void) { pthread_mutex_lock(&interface_lock); status_clean_unprotected(); pthread_mutex_unlock(&interface_lock); } void prevent_status_cleaning(void) { status_window_is_cleanable = false; } void allow_status_cleaning(void) { status_window_is_cleanable = true; } void status_write(config_entry_id color, const char *format, ...) { if (status_window_is_initialized != true) { va_list args; va_start(args, format); vprintf(format, args); putchar('\n'); fflush(stdout); va_end(args); return; } pthread_mutex_lock(&interface_lock); struct string *new_message = crtes(100); va_list args; va_start(args, format); str_vappendf(new_message, format, args); va_end(args); message_color = color; cpyss(&message_text, new_message); free_string(new_message); INFO("Printed status message: %s", message_text->ptr); update_status_window_content_unprotected(); pthread_mutex_unlock(&interface_lock); } void status_delete(void) { pthread_mutex_lock(&interface_lock); free_string(message_text); message_text = NULL; delwin(status_window); pthread_mutex_unlock(&interface_lock); } input_id get_input(struct input_binding *ctx, uint32_t *count, const struct wstring **p_arg) { static char key[1000]; static size_t queued_action_index = 0; if (queued_action_index > 0) { input_id next = get_action_of_bind(ctx, key, queued_action_index, p_arg); if (next != INPUT_ERROR) { queued_action_index += 1; return next; } else { queued_action_index = 0; } } while (they_want_us_to_stop == false) { // We have to read input from a child window because // reading it from the main window will bring stdscr // on top of other windows and overlap them. pthread_mutex_lock(&interface_lock); int status = get_wch(key); pthread_mutex_unlock(&interface_lock); if (status == TB_ERR) { if (they_want_us_to_quit_waiting_for_input == true) { they_want_us_to_quit_waiting_for_input = false; return INPUT_EMPTY; } nanosleep(&input_polling_period, NULL); continue; } else if (status == TB_EVENT_RESIZE) { return resize_handler(); } INFO("Read key: %s", key); if (search_mode_is_enabled == true) { if (strcmp(key, "enter") == 0 || strcmp(key, "^C") == 0 || strcmp(key, "escape") == 0) { search_mode_is_enabled = false; status_clean(); if (strcmp(key, "enter") == 0 && search_mode_text_input->len > 0) { return INPUT_APPLY_SEARCH_MODE_FILTER; } else { search_mode_text_input->len = 0; search_mode_text_input->ptr[0] = L'\0'; } } else if (strcmp(key, "backspace") == 0) { if (search_mode_text_input->len > 0) { search_mode_text_input->len -= 1; search_mode_text_input->ptr[search_mode_text_input->len] = L'\0'; } else { search_mode_is_enabled = false; status_clean(); } } else { struct wstring *wkey = convert_array_to_wstring(key, strlen(key)); wcatss(search_mode_text_input, wkey); free_wstring(wkey); } update_status_window_content(); } else if (ISDIGIT(key[0])) { count_buf_len %= 9; count_buf[count_buf_len++] = key[0]; count_buf[count_buf_len] = '\0'; update_status_window_content(); } else { input_id cmd = get_action_of_bind(ctx, key, 0, p_arg); uint32_t count_value = 1; if (sscanf(count_buf, "%" SCNu32, &count_value) != 1) { count_value = 1; } count_buf[0] = '\0'; count_buf_len = 0; if (cmd == INPUT_START_SEARCH_INPUT) { wstr_set(&search_mode_text_input, L"", 0, 100); search_mode_is_enabled = true; update_status_window_content(); } else { if (count) { *count = count_value; } queued_action_index = 1; update_status_window_content(); return cmd; } } } INFO("Received signal requesting termination of program."); return INPUT_QUIT_HARD; } struct string * pop_search_filter(void) { if (search_mode_is_enabled == true) { return NULL; // Must not yield search filter when it's not complete yet! } if (search_mode_text_input != NULL && search_mode_text_input->len > 0) { struct string *search_query = convert_wstring_to_string(search_mode_text_input); empty_wstring(search_mode_text_input); return search_query; } return NULL; } void yield_control_to_menu(void) { they_want_us_to_quit_waiting_for_input = true; } newsraft/src/interface.c000066400000000000000000000102061516312403600156020ustar00rootroot00000000000000#include #include #include "load_config/load_config.h" #include "newsraft.h" static bool paint_it_black = true; static volatile bool newsraft_has_successfully_initialized_ui = false; pthread_mutex_t interface_lock = PTHREAD_MUTEX_INITIALIZER; static bool obtain_list_menu_size(size_t *width, size_t *height) { int terminal_width = tb_width(); if (terminal_width < 0) { FAIL("Terminal width of %d is invalid!", terminal_width); return false; } int terminal_height = tb_height(); if (terminal_height < 0) { FAIL("Terminal height of %d is invalid!", terminal_height); return false; } INFO("Obtained terminal size: %d width, %d height.", terminal_width, terminal_height); *width = terminal_width; *height = terminal_height > 1 ? terminal_height - 1 : 0; // 1 row is reserved for status window return true; } static int ui_log_function(const char *fmt, ...) { va_list args; va_start(args, fmt); int len = log_vprint("TERMBOX", fmt, args); va_end(args); return len; } bool ui_init(void) { INFO("Initializing user interface..."); tb_set_log_function(ui_log_function); // Please, note that tb_init() hides the cursor automatically for us. int status = tb_init(); if (status != TB_OK) { write_error("Initialization of user interface failed: %s.\n", tb_strerror(status)); return false; } if (is_escape_key_used()) { WARN("Escape key has been bound - engaging ESC input mode!"); tb_set_input_mode(TB_INPUT_ESC); } else { INFO("No Escape key binds was found - engaging ALT input mode."); tb_set_input_mode(TB_INPUT_ALT); } if (obtain_list_menu_size(&list_menu_width, &list_menu_height) == false) { write_error("Invalid terminal size obtained!\n"); return false; } if (!get_cfg_bool(NULL, CFG_IGNORE_NO_COLOR) && getenv("NO_COLOR") != NULL) { INFO("NO_COLOR environment variable is set, canceling colors initialization."); } else { paint_it_black = false; // Some iridescent sensation at last! if (config_uses_256_colors) { tb_set_output_mode(TB_OUTPUT_256); } } newsraft_has_successfully_initialized_ui = true; return true; } void ui_term(void) { INFO("Terminating user interface..."); tb_shutdown(); } bool ui_is_running(void) { return newsraft_has_successfully_initialized_ui; } bool ui_set_window_title(void) { // setting the window title! escape codes are confusing fputs("\033]0;newsraft\007", stdout); fflush(stdout); return true; } bool run_menu_loop(void) { struct timespec idling = {0, 100000000}; // 0.1 seconds struct menu_state *menu = setup_menu(§ions_menu_loop, NULL, NULL, 0, MENU_NORMAL, NULL); if (menu == NULL) { return false; } while (they_want_us_to_stop == false) { menu = menu->run(menu); if (menu == NULL) { break; // TODO: don't stop feed downloader? nanosleep(&idling, NULL); // Avoids CPU cycles waste while awaiting termination menu = setup_menu(§ions_menu_loop, NULL, NULL, 0, MENU_DISABLE_SETTINGS, NULL); } } return true; } input_id resize_handler(void) { pthread_mutex_lock(&interface_lock); // We need to call clear and refresh before all resize actions because // the junk text of previous size may remain in inactive areas. tb_clear(); tb_present(); if (obtain_list_menu_size(&list_menu_width, &list_menu_height) == false) { // Some really crazy resize happend. It is either a glitch or user // deliberately trying to break something. This state is unusable anyways. write_error("Don't flex around with me, okay?\n"); goto error; } if (adjust_list_menu() == false) { goto error; } if (status_recreate_unprotected() == false) { goto error; } if (is_current_menu_a_pager() == true) { refresh_pager_menu(); } redraw_list_menu_unprotected(); pthread_mutex_unlock(&interface_lock); return INPUT_ERROR; error: they_want_us_to_stop = true; pthread_mutex_unlock(&interface_lock); return INPUT_QUIT_HARD; } bool call_resize_handler_if_current_list_menu_size_is_different_from_actual(void) { size_t width = 0, height = 0; obtain_list_menu_size(&width, &height); if (width != list_menu_width || height != list_menu_height) { resize_handler(); return true; } return false; } bool arent_we_colorful(void) { return !paint_it_black; } newsraft/src/items-list.c000066400000000000000000000241651516312403600157450ustar00rootroot00000000000000#include #include #include "newsraft.h" static inline bool append_sorting_order_expression_to_query(struct string *q, sorting_method_t order) { switch (order) { case SORT_BY_TIME_ASC: catas(q, " ORDER BY MAX(publication_date, update_date) ASC, download_date ASC, rowid ASC", 79); break; case SORT_BY_TIME_DESC: catas(q, " ORDER BY MAX(publication_date, update_date) DESC, download_date DESC, rowid DESC", 82); break; case SORT_BY_TIME_DOWNLOAD_ASC: catas(q, " ORDER BY download_date ASC, rowid ASC", 38); break; case SORT_BY_TIME_DOWNLOAD_DESC: catas(q, " ORDER BY download_date DESC, rowid DESC", 40); break; case SORT_BY_TIME_PUBLICATION_ASC: catas(q, " ORDER BY publication_date ASC, download_date ASC, rowid ASC", 60); break; case SORT_BY_TIME_PUBLICATION_DESC: catas(q, " ORDER BY publication_date DESC, download_date DESC, rowid DESC", 63); break; case SORT_BY_TIME_UPDATE_ASC: catas(q, " ORDER BY update_date ASC, download_date ASC, rowid ASC", 55); break; case SORT_BY_TIME_UPDATE_DESC: catas(q, " ORDER BY update_date DESC, download_date DESC, rowid DESC", 58); break; case SORT_BY_ROWID_ASC: catas(q, " ORDER BY rowid ASC", 19); break; case SORT_BY_ROWID_DESC: catas(q, " ORDER BY rowid DESC", 20); break; case SORT_BY_UNREAD_ASC: catas(q, " ORDER BY unread ASC, MAX(publication_date, update_date) DESC, download_date DESC, rowid DESC", 93); break; case SORT_BY_UNREAD_DESC: catas(q, " ORDER BY unread DESC, MAX(publication_date, update_date) DESC, download_date DESC, rowid DESC", 94); break; case SORT_BY_IMPORTANT_ASC: catas(q, " ORDER BY important ASC, MAX(publication_date, update_date) DESC, download_date DESC, rowid DESC", 96); break; case SORT_BY_IMPORTANT_DESC: catas(q, " ORDER BY important DESC, MAX(publication_date, update_date) DESC, download_date DESC, rowid DESC", 97); break; case SORT_BY_ALPHABET_ASC: catas(q, " ORDER BY title ASC, rowid ASC", 30); break; case SORT_BY_ALPHABET_DESC: catas(q, " ORDER BY title DESC, rowid DESC", 32); break; default: fail_status("Unknown sorting method name!"); return false; } return true; } static inline struct string * generate_search_query_string(const struct menu_state *ctx, const struct items_list *items) { struct string *query = crtas("SELECT rowid,feed_url,guid,title,link,publication_date,update_date,unread,important FROM items WHERE ", 101); struct string *cond = generate_items_search_condition(items->feeds, items->feeds_count); if (!STRING_IS_EMPTY(cond)) { catss(query, cond); } free_string(cond); if (!STRING_IS_EMPTY(items->find_filter)) { catas(query, " AND (", 6); catss(query, items->find_filter); catcs(query, ')'); } for (const struct menu_state *m = ctx; m != NULL; m = m->prev) { if (m->search_token) { catas(query, " AND ((title LIKE '%' || ? || '%') OR (content LIKE '%' || ? || '%'))", 69); } } if (append_sorting_order_expression_to_query(query, items->sorting) == false) { free_string(query); return NULL; } return query; } void free_items_list(struct items_list *items) { if (items != NULL) { for (size_t i = 0; i < items->len; ++i) { free_string(items->ptr[i].guid); free_string(items->ptr[i].title); free_string(items->ptr[i].url); free_string(items->ptr[i].date_str); free_string(items->ptr[i].pub_date_str); } sqlite3_finalize(items->res); free_string(items->query); free_string(items->find_filter); free(items->ptr); free(items); } } static inline struct feed_entry ** find_feed_entry_by_url(struct feed_entry **feeds, size_t feeds_count, const char *feed_url) { if (feed_url != NULL) { size_t feed_url_len = strlen(feed_url); for (size_t i = 0; i < feeds_count; ++i) { // Feed URLs in the database are stored without trailing slashes. // To make the search legit we have to strip slashes from the user's URLs also. size_t clean_len = feeds[i]->url->len; while (clean_len > 0 && feeds[i]->url->ptr[clean_len - 1] == '/') { clean_len -= 1; } if (feed_url_len == clean_len && strncmp(feed_url, feeds[i]->url->ptr, clean_len) == 0) { return feeds + i; } } } return NULL; } void obtain_items_at_least_up_to_the_given_index(struct items_list *items, size_t index) { sqlite3_stmt *res = items->res; if (items->finished == true) { return; } const char *text; int status; while (index >= items->len) { status = sqlite3_step(res); if (status != SQLITE_ROW) { items->finished = true; return; } items->ptr = newsraft_realloc(items->ptr, sizeof(struct item_entry) * (items->len + 1)); items->ptr[items->len].rowid = sqlite3_column_int64(res, 0); text = (const char *)sqlite3_column_text(res, 1); items->ptr[items->len].feed = find_feed_entry_by_url(items->feeds, items->feeds_count, text); if (items->ptr[items->len].feed == NULL) { continue; } text = (const char *)sqlite3_column_text(res, 2); if (text == NULL) { continue; } items->ptr[items->len].guid = crtas(text, strlen(text)); text = (const char *)sqlite3_column_text(res, 3); items->ptr[items->len].title = text == NULL ? crtes(1) : crtas(text, strlen(text)); inlinefy_string(items->ptr[items->len].title); text = (const char *)sqlite3_column_text(res, 4); items->ptr[items->len].url = crtes(1); // Convert URL to absolute notation in case it's stored relative. // // For example, convert "/index.xml" to "http://example.org/index.xml" // We consider 2 base URLs for conversion to absolute notation: // // 1. link to webpage related to item's feed // 2. feed URL specified by the user // // If the first method doesn't yield a valid absolute notation, we try the second one. char *full_url = NULL; if (!STRING_IS_EMPTY(items->ptr[items->len].feed[0]->link)) { full_url = complete_url(items->ptr[items->len].feed[0]->link->ptr, text); } if (full_url == NULL) { full_url = complete_url(items->ptr[items->len].feed[0]->url->ptr, text); } if (full_url) { cpyas(&items->ptr[items->len].url, full_url, strlen(full_url)); free(full_url); } items->ptr[items->len].pub_date = sqlite3_column_int64(res, 5); items->ptr[items->len].upd_date = sqlite3_column_int64(res, 6); if (items->ptr[items->len].pub_date <= 0 && items->ptr[items->len].upd_date > 0) { items->ptr[items->len].pub_date = items->ptr[items->len].upd_date; } if (items->ptr[items->len].upd_date <= 0 && items->ptr[items->len].pub_date > 0) { items->ptr[items->len].upd_date = items->ptr[items->len].pub_date; } items->ptr[items->len].date_str = get_cfg_date(NULL, CFG_LIST_ENTRY_DATE_FORMAT, items->ptr[items->len].upd_date); items->ptr[items->len].pub_date_str = get_cfg_date(NULL, CFG_LIST_ENTRY_DATE_FORMAT, items->ptr[items->len].pub_date); items->ptr[items->len].is_unread = sqlite3_column_int(res, 7); // unread items->ptr[items->len].is_important = sqlite3_column_int(res, 8); // important items->len += 1; } } bool update_menu_item_list(struct menu_state *ctx) { INFO("Updating menu's items list."); if (ctx->feeds_count == 0) { return false; } struct items_list *new_items = newsraft_calloc(1, sizeof(*new_items)); new_items->feeds = ctx->feeds_original; new_items->feeds_count = ctx->feeds_count; new_items->sorting = ctx->items ? ctx->items->sorting : get_sorting_id(get_cfg_string(NULL, CFG_MENU_ITEM_SORTING)->ptr); new_items->find_filter = ctx->find_filter ? convert_wstring_to_string(ctx->find_filter) : NULL; new_items->query = generate_search_query_string(ctx, new_items); if (new_items->query == NULL) { goto undo1; } new_items->res = db_prepare(new_items->query->ptr, new_items->query->len + 1, NULL); if (new_items->res == NULL) { fail_status("Can't run items search query! Make sure all item-rule settings are valid SQL conditions."); goto undo2; } for (size_t i = 0; i < new_items->feeds_count; ++i) { db_bind_feed_url(new_items->res, i + 1, new_items->feeds[i]->url); } int search_token_iter = 0; for (const struct menu_state *m = ctx; m != NULL; m = m->prev) { if (m->search_token) { search_token_iter += 1; db_bind_string(new_items->res, new_items->feeds_count + search_token_iter, m->search_token); search_token_iter += 1; db_bind_string(new_items->res, new_items->feeds_count + search_token_iter, m->search_token); } } obtain_items_at_least_up_to_the_given_index(new_items, 0); if (new_items->len < 1) { if (search_token_iter > 0) { info_status("No items found. Search query didn't get any matches!"); } else if (!STRING_IS_EMPTY(new_items->find_filter)) { info_status("No items found. Find query didn't get any matches!"); } else { fail_status("No items found. Make sure this feed is updated!"); } goto undo3; } pthread_mutex_lock(&interface_lock); free_items_list(ctx->items); ctx->items = new_items; if (ctx->is_initialized) { reset_list_menu_unprotected(); } pthread_mutex_unlock(&interface_lock); ctx->age = fetch_menu_age(); return true; undo3: sqlite3_finalize(new_items->res); undo2: free_string(new_items->query); undo1: free_string(new_items->find_filter); newsraft_free(new_items); return false; } void change_items_list_sorting(struct menu_state *ctx, input_id cmd) { static const struct { sorting_method_t primary; sorting_method_t secondary; } sort_map[] = { [INPUT_SORT_BY_TIME] = {SORT_BY_TIME_DESC, SORT_BY_TIME_ASC}, [INPUT_SORT_BY_TIME_DOWNLOAD] = {SORT_BY_TIME_DOWNLOAD_DESC, SORT_BY_TIME_DOWNLOAD_ASC}, [INPUT_SORT_BY_TIME_PUBLICATION] = {SORT_BY_TIME_PUBLICATION_DESC, SORT_BY_TIME_PUBLICATION_ASC}, [INPUT_SORT_BY_TIME_UPDATE] = {SORT_BY_TIME_UPDATE_DESC, SORT_BY_TIME_UPDATE_ASC}, [INPUT_SORT_BY_ROWID] = {SORT_BY_ROWID_DESC, SORT_BY_ROWID_ASC}, [INPUT_SORT_BY_UNREAD] = {SORT_BY_UNREAD_DESC, SORT_BY_UNREAD_ASC}, [INPUT_SORT_BY_ALPHABET] = {SORT_BY_ALPHABET_ASC, SORT_BY_ALPHABET_DESC}, [INPUT_SORT_BY_IMPORTANT] = {SORT_BY_IMPORTANT_DESC, SORT_BY_IMPORTANT_ASC}, }; ctx->items->sorting = ctx->items->sorting == sort_map[cmd].primary ? sort_map[cmd].secondary : sort_map[cmd].primary; update_menu_item_list(ctx); info_status(get_sorting_message(ctx->items->sorting), "items"); } newsraft/src/items-metadata-content.c000066400000000000000000000040751516312403600202200ustar00rootroot00000000000000#include #include "newsraft.h" static inline render_block_format get_content_type_by_string(const char *type) { return (strstr(type, "html") != NULL) || (strstr(type, "HTML") != NULL) ? TEXT_HTML : TEXT_PLAIN; } static bool get_largest_text_piece_from_item_serialized_data( const char *data, struct string **text, render_block_format *type, const char *text_prefix, size_t text_prefix_len, const char *type_prefix, size_t type_prefix_len) { if (data == NULL) { return true; // There's no data. Ignore it. } struct string *temp_text = crtes(1000); struct deserialize_stream *stream = open_deserialize_stream(data); if ((temp_text == NULL) || (stream == NULL)) { free_string(temp_text); close_deserialize_stream(stream); return false; } render_block_format temp_type = TEXT_PLAIN; const struct string *entry = get_next_entry_from_deserialize_stream(stream); while (entry != NULL) { if (strcmp(entry->ptr, "^") == 0) { if (temp_text->len > (*text)->len) { cpyss(text, temp_text); *type = temp_type; } empty_string(temp_text); temp_type = TEXT_PLAIN; } else if (strncmp(entry->ptr, type_prefix, type_prefix_len) == 0) { temp_type = get_content_type_by_string(entry->ptr + type_prefix_len); } else if (strncmp(entry->ptr, text_prefix, text_prefix_len) == 0) { cpyas(&temp_text, entry->ptr + text_prefix_len, entry->len - text_prefix_len); } entry = get_next_entry_from_deserialize_stream(stream); } if (temp_text->len > (*text)->len) { cpyss(text, temp_text); *type = temp_type; } free_string(temp_text); close_deserialize_stream(stream); return true; } bool get_largest_piece_from_item_content(const char *content, struct string **text, render_block_format *type) { return get_largest_text_piece_from_item_serialized_data(content, text, type, "text=", 5, "type=", 5); } bool get_largest_piece_from_item_attachments(const char *attachments, struct string **text, render_block_format *type) { return get_largest_text_piece_from_item_serialized_data(attachments, text, type, "description_text=", 17, "description_type=", 17); } newsraft/src/items-metadata-links.c000066400000000000000000000161471516312403600176710ustar00rootroot00000000000000#include #include #include #include "newsraft.h" static inline int convert_bytes_to_human_readable_size_string(char *dest, size_t dest_size, const char *value) { float size = -1; if (sscanf(value, "%f", &size) != 1 || size < 0) { FAIL("Failed to convert \"%s\" to size number!", value); return 0; } if (size < 1100) { return snprintf(dest, dest_size, "%.0f bytes", size); } else if (size < 1100000) { return snprintf(dest, dest_size, "%.1f KB", size / 1000); } else if (size < 1100000000) { return snprintf(dest, dest_size, "%.1f MB", size / 1000000); } return snprintf(dest, dest_size, "%.2f GB", size / 1000000000); } static inline int convert_seconds_to_human_readable_duration_string(char *dest, size_t dest_size, const char *value) { float duration = -1; if (sscanf(value, "%f", &duration) != 1 || duration < 0) { FAIL("Failed to convert \"%s\" to duration number!", value); return 0; } if (duration < 90) { return snprintf(dest, dest_size, "%.0f seconds", duration); } else if (duration < 4000) { return snprintf(dest, dest_size, "%.1f minutes", duration / 60); } return snprintf(dest, dest_size, "%.1f hours", duration / 3600); } // See tests/complete_url.c to understand what it takes in and spews out. char * complete_url(const char *base, const char *rel) { if (base == NULL || rel == NULL) { return NULL; } // Don't complete links with protocol scheme // Don't complete telephone links // Don't complete email links if (strstr(rel, "://") != NULL || strstr(rel, "tel:") == rel || strstr(rel, "mailto:") == rel) { return strdup(rel); } CURLU *link = curl_url(); if (link == NULL) { return NULL; } if (curl_url_set(link, CURLUPART_URL, base, 0) != CURLUE_OK) { curl_url_cleanup(link); return NULL; } if (curl_url_set(link, CURLUPART_URL, rel, 0) != CURLUE_OK) { curl_url_cleanup(link); return NULL; } char *new_url = NULL; if (curl_url_get(link, CURLUPART_URL, &new_url, 0) != CURLUE_OK || new_url == NULL) { curl_free(new_url); curl_url_cleanup(link); return NULL; } char *complete = strdup(new_url); curl_free(new_url); curl_url_cleanup(link); return complete; } // Returns link index in the links list or -1 in case of failure. int64_t add_url_to_links_list(struct links_list *links, const char *url, size_t url_len) { char *full_url = NULL; if (links->len > 0) { full_url = complete_url(links->ptr[0].url->ptr, url); if (full_url != NULL) { url = full_url; url_len = strlen(full_url); } } for (int64_t i = 0; i < (int64_t)links->len; ++i) { if (url_len == links->ptr[i].url->len && strncmp(url, links->ptr[i].url->ptr, url_len) == 0) { free(full_url); return i; // Don't add duplicate. } } int64_t index = (links->len)++; links->ptr = newsraft_realloc(links->ptr, sizeof(struct link) * (links->len + 1)); memset(&links->ptr[index], 0, sizeof(struct link)); cpyas(&links->ptr[index].url, url, url_len); free(full_url); return links->ptr[index].url == NULL ? -1 : index; } static void free_contents_of_link(const struct link *link) { free_string(link->url); free_string(link->type); free_string(link->size); free_string(link->duration); } static inline bool add_another_link_to_trim_link_list(struct links_list *links, const struct link *link) { if (STRING_IS_EMPTY(link->url)) { free_contents_of_link(link); return true; // Ignore empty links. } for (int64_t i = 0; i < (int64_t)links->len; ++i) { if (strcmp(link->url->ptr, links->ptr[i].url->ptr) == 0) { // Don't add duplicate. free_contents_of_link(link); return true; } } links->ptr = newsraft_realloc(links->ptr, sizeof(struct link) * (links->len + 1)); size_t index = (links->len)++; links->ptr[index].url = link->url; links->ptr[index].type = link->type; links->ptr[index].size = link->size; links->ptr[index].duration = link->duration; return true; } bool add_item_attachments_to_links_list(struct links_list *links, sqlite3_stmt *res) { const char *text = (const char *)sqlite3_column_text(res, ITEM_COLUMN_ATTACHMENTS); if (text == NULL) { return true; // It is not an error because this item simply does not have attachments set. } struct deserialize_stream *s = open_deserialize_stream(text); if (s == NULL) { return false; } struct link another_link = {0}; const struct string *entry = get_next_entry_from_deserialize_stream(s); while (entry != NULL) { if (strcmp(entry->ptr, "^") == 0) { add_another_link_to_trim_link_list(links, &another_link); memset(&another_link, 0, sizeof(struct link)); } else if (strncmp(entry->ptr, "url=", 4) == 0) { cpyas(&another_link.url, entry->ptr + 4, entry->len - 4); } else if (strncmp(entry->ptr, "type=", 5) == 0) { cpyas(&another_link.type, entry->ptr + 5, entry->len - 5); } else if (strncmp(entry->ptr, "size=", 5) == 0) { cpyas(&another_link.size, entry->ptr + 5, entry->len - 5); } else if (strncmp(entry->ptr, "duration=", 9) == 0) { cpyas(&another_link.duration, entry->ptr + 9, entry->len - 9); } entry = get_next_entry_from_deserialize_stream(s); } add_another_link_to_trim_link_list(links, &another_link); close_deserialize_stream(s); return true; } struct wstring * generate_link_list_wstring_for_pager(struct config_context **ctx, const struct links_list *links) { struct wstring *list = wcrtes(200); struct wstring *fmt_out = wcrtes(200); struct string *str = crtes(200); const struct wstring *link_fmt = get_cfg_wstring(ctx, CFG_ITEM_CONTENT_LINK_FORMAT); #define CONVERT_OUT_SIZE 200 char convert_out[CONVERT_OUT_SIZE]; int convert_len; for (size_t i = 0; i < links->len; ++i) { if (STRING_IS_EMPTY(links->ptr[i].url)) { continue; } cpyss(&str, links->ptr[i].url); bool parentheses_are_open = false; if ((links->ptr[i].type != NULL) && (links->ptr[i].type->len != 0)) { catas(str, " (type: ", 8); catss(str, links->ptr[i].type); parentheses_are_open = true; } if ((links->ptr[i].size != NULL) && (links->ptr[i].size->len != 0) && (strcmp(links->ptr[i].size->ptr, "0") != 0)) { convert_len = convert_bytes_to_human_readable_size_string(convert_out, CONVERT_OUT_SIZE, links->ptr[i].size->ptr); if (convert_len > 0 && convert_len < CONVERT_OUT_SIZE) { catas(str, parentheses_are_open ? ", size: " : " (size: ", 8); catas(str, convert_out, convert_len); parentheses_are_open = true; } } if ((links->ptr[i].duration != NULL) && (links->ptr[i].duration->len != 0) && (strcmp(links->ptr[i].duration->ptr, "0") != 0)) { convert_len = convert_seconds_to_human_readable_duration_string(convert_out, CONVERT_OUT_SIZE, links->ptr[i].duration->ptr); if (convert_len > 0 && convert_len < CONVERT_OUT_SIZE) { catas(str, parentheses_are_open ? ", duration: " : " (duration: ", 12); catas(str, convert_out, convert_len); parentheses_are_open = true; } } if (parentheses_are_open == true) { catcs(str, ')'); } struct format_arg link_fmt_args[] = { {L'i', L'd', {.i = i + 1 }}, {L'l', L's', {.s = str->ptr}}, {L'\0', L'\0', {.i = 0 }}, // terminator }; do_format(fmt_out, link_fmt->ptr, link_fmt_args); wcatss(list, fmt_out); } free_wstring(fmt_out); free_string(str); return list; } newsraft/src/items-metadata-persons.c000066400000000000000000000060311516312403600202310ustar00rootroot00000000000000#include #include "newsraft.h" struct person { struct string *type; struct string *name; struct string *email; struct string *url; }; static inline bool initialize_person(struct person *p) { p->type = crtes(17); p->name = crtes(19); p->email = crtes(23); p->url = crtes(29); return p->type != NULL && p->name != NULL && p->email != NULL && p->url != NULL; } static inline void empty_person(struct person *p) { empty_string(p->type); empty_string(p->name); empty_string(p->email); empty_string(p->url); } static inline void free_person(struct person *p) { free_string(p->type); free_string(p->name); free_string(p->email); free_string(p->url); } static bool write_person_to_result(struct string *result, const struct person *person) { if (person->type->len == 0 || (person->name->len == 0 && person->email->len == 0 && person->url->len == 0)) { return true; // Ignore empty persons >,< } if (result->len > 0) { catas(result, ", ", 2); } if (person->name->len > 0) { catss(result, person->name); } if (person->email->len > 0) { if (person->name->len > 0) { catas(result, " <", 2); } catss(result, person->email); if (person->name->len > 0) { catcs(result, '>'); } } if (person->url->len > 0) { if (person->name->len > 0 || person->email->len > 0) { catas(result, " (", 2); } catss(result, person->url); if (person->name->len > 0 || person->email->len > 0) { catcs(result, ')'); } } if (person->type->len > 0 && strcmp(person->type->ptr, "author") != 0) { if (person->name->len > 0 || person->email->len > 0 || person->url->len > 0) { catas(result, " [", 2); } catss(result, person->type); if (person->name->len > 0 || person->email->len > 0 || person->url->len > 0) { catcs(result, ']'); } } return true; } struct string * deserialize_persons_string(const char *src) { struct person person; struct string *result = crtes(100); struct deserialize_stream *stream = open_deserialize_stream(src); if ((initialize_person(&person) == false) || (result == NULL) || (stream == NULL)) { goto error; } const struct string *field = get_next_entry_from_deserialize_stream(stream); while (field != NULL) { if (strcmp(field->ptr, "^") == 0) { if (write_person_to_result(result, &person) == false) { goto error; } empty_person(&person); } else if (strncmp(field->ptr, "type=", 5) == 0) { cpyas(&person.type, field->ptr + 5, field->len - 5); } else if (strncmp(field->ptr, "name=", 5) == 0) { cpyas(&person.name, field->ptr + 5, field->len - 5); } else if (strncmp(field->ptr, "email=", 6) == 0) { cpyas(&person.email, field->ptr + 6, field->len - 6); } else if (strncmp(field->ptr, "url=", 4) == 0) { cpyas(&person.url, field->ptr + 4, field->len - 4); } field = get_next_entry_from_deserialize_stream(stream); } if (write_person_to_result(result, &person) == false) { goto error; } close_deserialize_stream(stream); free_person(&person); return result; error: close_deserialize_stream(stream); free_person(&person); free_string(result); return NULL; } newsraft/src/items-metadata.c000066400000000000000000000111471516312403600165460ustar00rootroot00000000000000#include #include "newsraft.h" static struct string * block_str(const struct string *text) { if (text != NULL && text->len > 0) { struct string *data = crtss(text); if (data != NULL) { inlinefy_string(data); return data; } } return NULL; } static struct string * block_date(const struct item_entry *item) { if (item->pub_date == 0 && item->upd_date == 0) { return NULL; } struct string *date_entry = crtes(100); struct string *date_str = get_cfg_date(&item->feed[0]->cfg, CFG_ITEM_CONTENT_DATE_FORMAT, item->pub_date == 0 ? item->upd_date : item->pub_date); if (date_str == NULL) goto error; if (item->pub_date > 0 && item->upd_date > 0 && item->pub_date != item->upd_date) { catss(date_entry, date_str); catas(date_entry, " (updated ", 10); free_string(date_str); date_str = get_cfg_date(&item->feed[0]->cfg, CFG_ITEM_CONTENT_DATE_FORMAT, item->upd_date); if (date_str == NULL) goto error; catcs(date_str, ')'); } catss(date_entry, date_str); struct string *data = block_str(date_entry); free_string(date_entry); free_string(date_str); return data; error: free_string(date_entry); free_string(date_str); return NULL; } static struct string * block_persons(sqlite3_stmt *res) { const char *serialized_persons = (char *)sqlite3_column_text(res, ITEM_COLUMN_PERSONS); if (serialized_persons == NULL) return NULL; struct string *persons = deserialize_persons_string(serialized_persons); struct string *data = block_str(persons); free_string(persons); return data; } static struct string * block_max_content(sqlite3_stmt *res, render_block_format *output_type) { const char *content = (char *)sqlite3_column_text(res, ITEM_COLUMN_CONTENT); struct string *text = crtes(50000); if (text == NULL) { return NULL; } render_block_format type = TEXT_PLAIN; if (get_largest_piece_from_item_content(content, &text, &type) == false) { goto error; } if (text->len == 0) { // There were no texts in the content, let's try to search in // the descriptions for item's attachments. const char *attachments = (char *)sqlite3_column_text(res, ITEM_COLUMN_ATTACHMENTS); if (get_largest_piece_from_item_attachments(attachments, &text, &type) == false) { goto error; } } if (text->len > 0) { *output_type = type; return text; } error: free_string(text); return NULL; } bool generate_render_blocks_based_on_item_data(struct render_blocks_list *blocks, const struct item_entry *item, sqlite3_stmt *res) { #define MAX_ENTRY_LENGTH 1000 char entry[MAX_ENTRY_LENGTH + 10]; size_t entry_len = 0; const struct string *content_order = get_cfg_string(&item->feed[0]->cfg, CFG_ITEM_CONTENT_FORMAT); for (const char *i = content_order->ptr; ; ++i) { if (*i == '|' || *i == '\0') { entry[entry_len] = '\0'; char *percent_pos = strchr(entry, '%'); if (percent_pos == NULL) { add_render_block(blocks, entry, entry_len, TEXT_HTML, false); } else if (*(percent_pos + 1) != '\0') { *percent_pos = '\0'; char specifier = *(percent_pos + 1); if (specifier == 'L') { add_render_block(blocks, entry, strlen(entry), TEXT_HTML, false); add_render_block(blocks, " ", 1, TEXT_LINKS, false); // Later filled with links of item add_render_block(blocks, percent_pos + 2, strlen(percent_pos + 2), TEXT_HTML, false); } else if (specifier == 'c') { render_block_format type = TEXT_PLAIN; struct string *content = block_max_content(res, &type); if (content != NULL) { add_render_block(blocks, entry, strlen(entry), TEXT_HTML, false); add_render_block(blocks, content->ptr, content->len, type, true); add_render_block(blocks, percent_pos + 2, strlen(percent_pos + 2), TEXT_HTML, false); free_string(content); } } else { struct string *value = NULL; pthread_mutex_lock(&interface_lock); switch (specifier) { case 'f': value = block_str(STRING_IS_EMPTY(item->feed[0]->name) ? item->feed[0]->url : item->feed[0]->name); break; case 't': value = block_str(item->title); break; case 'l': value = block_str(item->url); break; case 'd': value = block_date(item); break; case 'a': value = block_persons(res); break; } pthread_mutex_unlock(&interface_lock); if (value != NULL) { struct string *text = crtas(entry, strlen(entry)); catss(text, value); catas(text, percent_pos + 2, strlen(percent_pos + 2)); add_render_block(blocks, text->ptr, text->len, TEXT_HTML, false); free_string(value); free_string(text); } } } if (*i == '\0') break; entry_len = 0; } else if (entry_len < MAX_ENTRY_LENGTH) { entry[entry_len++] = *i; } } return true; } newsraft/src/items-pager.c000066400000000000000000000073551516312403600160720ustar00rootroot00000000000000#include #include "newsraft.h" static inline bool populate_render_blocks_list_with_data_from_item(const struct item_entry *item, struct render_blocks_list *blocks) { sqlite3_stmt *res = db_find_item_by_rowid(item->rowid); if (res == NULL) { return false; } if (item->url != NULL && item->url->len > 0) { if (add_url_to_links_list(&blocks->links, item->url->ptr, item->url->len) < 0) { goto error; } } if (add_item_attachments_to_links_list(&blocks->links, res) == false) { goto error; } if (generate_render_blocks_based_on_item_data(blocks, item, res) == false) { goto error; } start_pager_menu(&item->feed[0]->cfg, blocks); if (blocks->links.len > 0) { struct wstring *links_wstr = generate_link_list_wstring_for_pager(&item->feed[0]->cfg, &blocks->links); if (links_wstr == NULL) { goto error; } apply_links_render_blocks(blocks, links_wstr); free_wstring(links_wstr); } sqlite3_finalize(res); return true; error: sqlite3_finalize(res); free_render_blocks(blocks); return false; } struct menu_state * item_pager_loop(struct menu_state *m) { m->enumerator = &is_pager_pos_valid; m->printer = &pager_menu_writer; struct render_blocks_list blocks = {0}; struct menu_state *items_menu = NULL; size_t item_id = 0; for (struct menu_state *i = m->prev; i != NULL; i = i->prev) { if (i->items != NULL) { items_menu = i; item_id = i->view_sel; break; } } if (items_menu == NULL) { goto quit; } struct format_arg items_pager_fmt_args[] = { {L'l', L's', {.s = NULL}}, {L'\0', L'\0', {.i = 0 }}, // terminator }; INFO("Trying to view an item with the rowid %" PRId64 "...", items_menu->items->ptr[item_id].rowid); if (populate_render_blocks_list_with_data_from_item(&items_menu->items->ptr[item_id], &blocks) == false) { goto quit; } if (start_pager_menu(&items_menu->items->ptr[item_id].feed[0]->cfg, &blocks) == false) { goto quit; } db_mark_item_read(items_menu->items->ptr[item_id].rowid, true); items_menu->items->ptr[item_id].is_unread = false; start_menu(); uint32_t count; const struct wstring *arg; while (true) { input_id cmd = get_input(items_menu->items->ptr[item_id].feed[0]->binds, &count, &arg); if (handle_pager_menu_control(cmd) == true) { continue; } switch (cmd) { case INPUT_JUMP_TO_NEXT: case INPUT_JUMP_TO_PREV: case INPUT_JUMP_TO_NEXT_UNREAD: case INPUT_JUMP_TO_PREV_UNREAD: case INPUT_JUMP_TO_NEXT_IMPORTANT: case INPUT_JUMP_TO_PREV_IMPORTANT: handle_list_menu_control(items_menu, cmd, NULL); if (items_menu->view_sel != item_id) { free_render_blocks(&blocks); return setup_menu(&item_pager_loop, items_menu->items->ptr[items_menu->view_sel].title, NULL, 0, MENU_SWALLOW, NULL); } break; case INPUT_NAVIGATE_BACK: case INPUT_QUIT_SOFT: case INPUT_QUIT_HARD: free_render_blocks(&blocks); return cmd == INPUT_QUIT_HARD ? NULL : close_menu(); case INPUT_OPEN_IN_BROWSER: if (count > 0 && count <= blocks.links.len) { struct config_context **cfg = &items_menu->items->ptr[item_id].feed[0]->cfg; const struct wstring *browser = get_cfg_wstring(cfg, CFG_OPEN_IN_BROWSER_COMMAND); items_pager_fmt_args[0].value.s = blocks.links.ptr[count - 1].url->ptr; run_formatted_command(browser, items_pager_fmt_args); } break; case INPUT_COPY_TO_CLIPBOARD: if (count > 0 && count <= blocks.links.len) { copy_string_to_clipboard(blocks.links.ptr[count - 1].url); } break; case INPUT_SYSTEM_COMMAND: if (count > 0 && count <= blocks.links.len) { items_pager_fmt_args[0].value.s = blocks.links.ptr[count - 1].url->ptr; run_formatted_command(arg, items_pager_fmt_args); } break; default: break; } } quit: free_render_blocks(&blocks); return close_menu(); } newsraft/src/items.c000066400000000000000000000211011516312403600147570ustar00rootroot00000000000000#include "newsraft.h" static bool is_item_valid(struct menu_state *ctx, size_t index) { if (ctx->items == NULL) { return false; } obtain_items_at_least_up_to_the_given_index(ctx->items, index); return index < ctx->items->len ? true : false; } static const struct format_arg * get_item_args(struct menu_state *ctx, size_t index) { static struct format_arg item_fmt[] = { {L'i', L'd', {.i = 0 }}, {L'u', L's', {.s = NULL}}, {L'd', L's', {.s = NULL}}, {L'D', L's', {.s = NULL}}, {L'l', L's', {.s = NULL}}, {L't', L's', {.s = NULL}}, {L'o', L's', {.s = NULL}}, {L'L', L's', {.s = NULL}}, {L'T', L's', {.s = NULL}}, {L'O', L's', {.s = NULL}}, {L'\0', L'\0', {.i = 0 }}, // terminator }; item_fmt[0].value.i = index + 1; item_fmt[1].value.s = ctx->items->ptr[index].is_unread == true ? "N" : " "; item_fmt[2].value.s = ctx->items->ptr[index].date_str->ptr; item_fmt[3].value.s = ctx->items->ptr[index].pub_date_str->ptr; item_fmt[4].value.s = ctx->items->ptr[index].url->ptr; item_fmt[5].value.s = ctx->items->ptr[index].title->ptr; item_fmt[6].value.s = ctx->items->ptr[index].title->len > 0 ? ctx->items->ptr[index].title->ptr : ctx->items->ptr[index].url->ptr; item_fmt[7].value.s = ctx->items->ptr[index].feed[0]->url->ptr; item_fmt[8].value.s = ctx->items->ptr[index].feed[0]->name ? ctx->items->ptr[index].feed[0]->name->ptr : ""; item_fmt[9].value.s = ctx->items->ptr[index].feed[0]->name ? ctx->items->ptr[index].feed[0]->name->ptr : ctx->items->ptr[index].feed[0]->url->ptr; return item_fmt; } static struct config_color paint_item(struct menu_state *ctx, size_t index, bool is_selected) { struct config_context **cfg = &ctx->items->ptr[index].feed[0]->cfg; struct config_color color; if (ctx->items->ptr[index].is_important) { color = get_cfg_color(cfg, CFG_COLOR_LIST_ITEM_IMPORTANT); } else if (ctx->items->ptr[index].is_unread) { color = get_cfg_color(cfg, CFG_COLOR_LIST_ITEM_UNREAD); } else { color = get_cfg_color(cfg, CFG_COLOR_LIST_ITEM); } if (is_selected) { if (is_cfg_color_set(cfg, CFG_COLOR_LIST_ITEM_SELECTED)) { color = get_cfg_color(cfg, CFG_COLOR_LIST_ITEM_SELECTED); } else { color.attributes |= TB_REVERSE; } } return color; } static bool is_item_unread(struct menu_state *ctx, size_t index) { return ctx->items->ptr[index].is_unread; } bool important_item_condition(struct menu_state *ctx, size_t index) { return ctx->items->ptr[index].is_important; } struct string * generate_items_search_condition(struct feed_entry **feeds, size_t feeds_count) { if (feeds_count == 0) { return NULL; } struct string *cond = crtas("(", 1); for (size_t i = 0; i < feeds_count; ++i) { if (i > 0) { catas(cond, " OR ", 4); } catas(cond, "feed_url=?", 10); const struct string *rule = get_cfg_string(&feeds[i]->cfg, CFG_ITEM_RULE); if (!STRING_IS_EMPTY(rule)) { catas(cond, " AND (", 6); catss(cond, rule); catcs(cond, ')'); } } catcs(cond, ')'); return cond; } static void mark_item_read(struct menu_state *ctx, size_t view_sel, bool status) { if (ctx->items->ptr[view_sel].is_unread == status) { if (db_mark_item_read(ctx->items->ptr[view_sel].rowid, status) == true) { ctx->items->ptr[view_sel].is_unread = !status; expose_entry_of_the_list_menu(view_sel); } } } static void mark_item_important(struct menu_state *ctx, size_t view_sel, bool status) { if (ctx->items->ptr[view_sel].is_important != status) { if (db_mark_item_important(ctx->items->ptr[view_sel].rowid, status) == true) { ctx->items->ptr[view_sel].is_important = status; expose_entry_of_the_list_menu(view_sel); } } } static void toggle_item_read(struct menu_state *ctx, size_t view_sel) { mark_item_read(ctx, view_sel, ctx->items->ptr[view_sel].is_unread); } static void toggle_item_important(struct menu_state *ctx, size_t view_sel) { mark_item_important(ctx, view_sel, !ctx->items->ptr[view_sel].is_important); } static void mark_all_items_read(struct menu_state *ctx, bool status) { pthread_mutex_lock(&interface_lock); if (ctx->flags & MENU_IS_SEARCH) { obtain_items_at_least_up_to_the_given_index(ctx->items, SIZE_MAX); for (size_t i = 0; i < ctx->items->len; ++i) { if (db_mark_item_read(ctx->items->ptr[i].rowid, status)) { ctx->items->ptr[i].is_unread = !status; } } pthread_mutex_unlock(&interface_lock); expose_all_visible_entries_of_the_list_menu(); } else { // Use intermediate variables to avoid race condition struct feed_entry **items_feeds = ctx->items->feeds; size_t items_feeds_count = ctx->items->feeds_count; pthread_mutex_unlock(&interface_lock); mark_feeds_read(items_feeds, items_feeds_count, status); update_menu_item_list(ctx); } } struct menu_state * items_menu_loop(struct menu_state *m) { m->enumerator = &is_item_valid; m->printer = &list_menu_writer; m->get_args = &get_item_args; m->paint_action = &paint_item; m->unread_state = &is_item_unread; m->entry_format = get_cfg_wstring(NULL, m->flags & MENU_IS_EXPLORE ? CFG_MENU_EXPLORE_ITEM_ENTRY_FORMAT : CFG_MENU_ITEM_ENTRY_FORMAT); raise_menu_age(); if (m->is_initialized == false) { if (!update_menu_item_list(m)) { return close_menu(); // Error displayed by update_menu_item_list() } } start_menu(); const struct wstring *arg, *browser; while (true) { if (get_cfg_bool(NULL, CFG_MENU_RESPONSIVENESS) && m->age != fetch_menu_age()) { update_menu_item_list(m); } if (get_cfg_bool(&m->items->ptr[m->view_sel].feed[0]->cfg, CFG_MARK_ITEM_READ_ON_HOVER)) { mark_item_read(m, m->view_sel, true); } input_id cmd = get_input(m->items->ptr[m->view_sel].feed[0]->binds, NULL, &arg); if (handle_list_menu_control(m, cmd, arg) == true) { continue; } switch (cmd) { case INPUT_MARK_READ: mark_item_read(m, m->view_sel, true); break; case INPUT_MARK_UNREAD: mark_item_read(m, m->view_sel, false); break; case INPUT_TOGGLE_READ: toggle_item_read(m, m->view_sel); break; case INPUT_MARK_READ_ALL: mark_all_items_read(m, true); break; case INPUT_MARK_UNREAD_ALL: mark_all_items_read(m, false); break; case INPUT_MARK_IMPORTANT: mark_item_important(m, m->view_sel, true); break; case INPUT_MARK_UNIMPORTANT: mark_item_important(m, m->view_sel, false); break; case INPUT_TOGGLE_IMPORTANT: toggle_item_important(m, m->view_sel); break; case INPUT_RELOAD: queue_updates(m->items->ptr[m->view_sel].feed, 1); break; case INPUT_RELOAD_ALL: queue_updates(m->feeds_original, m->feeds_count); break; case INPUT_COPY_TO_CLIPBOARD: copy_string_to_clipboard(m->items->ptr[m->view_sel].url); break; case INPUT_QUIT_HARD: return NULL; case INPUT_NAVIGATE_BACK: if (get_menu_depth() < 3 && (!(m->flags & MENU_IS_SEARCH) && (m->flags & MENU_IS_EXPLORE) && (m->find_filter == NULL))) { break; } // fall through case INPUT_QUIT_SOFT: if (!(m->flags & MENU_IS_SEARCH) && (m->flags & MENU_IS_EXPLORE) && (m->find_filter == NULL)) { close_menu(); } return close_menu(); case INPUT_TOGGLE_EXPLORE_MODE: if (m->flags & MENU_IS_EXPLORE) return close_menu(); break; case INPUT_GOTO_FEED: if (!(m->flags & MENU_IS_EXPLORE)) break; return setup_menu(&items_menu_loop, NULL, m->items->ptr[m->view_sel].feed, 1, MENU_NORMAL, NULL); case INPUT_APPLY_SEARCH_MODE_FILTER: return setup_menu(&items_menu_loop, NULL, m->feeds_original, m->feeds_count, MENU_IS_SEARCH | MENU_IS_EXPLORE, m->find_filter); case INPUT_OPEN_IN_BROWSER: browser = get_cfg_wstring(&m->items->ptr[m->view_sel].feed[0]->cfg, CFG_OPEN_IN_BROWSER_COMMAND); run_formatted_command(browser, get_item_args(m, m->view_sel)); break; case INPUT_SORT_BY_TIME: case INPUT_SORT_BY_TIME_DOWNLOAD: case INPUT_SORT_BY_TIME_PUBLICATION: case INPUT_SORT_BY_TIME_UPDATE: case INPUT_SORT_BY_ROWID: case INPUT_SORT_BY_UNREAD: case INPUT_SORT_BY_ALPHABET: case INPUT_SORT_BY_IMPORTANT: change_items_list_sorting(m, cmd); break; case INPUT_FIND_COMMAND: if (m->find_filter) return setup_menu(&items_menu_loop, NULL, m->feeds_original, m->feeds_count, MENU_IS_EXPLORE | MENU_SWALLOW, arg);; return setup_menu(&items_menu_loop, NULL, m->feeds_original, m->feeds_count, MENU_IS_EXPLORE, arg); case INPUT_DATABASE_COMMAND: db_perform_user_edit(arg, NULL, 0, &m->items->ptr[m->view_sel]); break; case INPUT_ENTER: return setup_menu(&item_pager_loop, m->items->ptr[m->view_sel].title, NULL, 0, MENU_NORMAL, NULL); } } return close_menu(); } newsraft/src/load_config/000077500000000000000000000000001516312403600157435ustar00rootroot00000000000000newsraft/src/load_config/config-auto.c000066400000000000000000000032221516312403600203210ustar00rootroot00000000000000#include #include #include #include "load_config/load_config.h" bool obtain_useragent_string(struct config_context **ctx, config_type_id id) { struct string *ua = crtas("newsraft/", 9); catas(ua, NEWSRAFT_VERSION, strlen(NEWSRAFT_VERSION)); struct utsname sys_data = {0}; if (uname(&sys_data) >= 0 && strlen(sys_data.sysname) > 0) { catas(ua, " (", 2); catas(ua, sys_data.sysname, strlen(sys_data.sysname)); catcs(ua, ')'); } bool status = set_cfg_string(ctx, id, ua->ptr, ua->len); free_string(ua); return status; } bool obtain_browser_command(struct config_context **ctx, config_type_id id) { #ifdef __APPLE__ return set_cfg_string(ctx, id, "open \"%l\"", 9); #endif return set_cfg_string(ctx, id, "${BROWSER:-xdg-open} \"%l\"", 25); } bool obtain_clipboard_command(struct config_context **ctx, config_type_id id) { #ifdef __APPLE__ return set_cfg_string(ctx, id, "pbcopy", 6); #endif if (getenv("WAYLAND_DISPLAY") != NULL) { return set_cfg_string(ctx, id, "wl-copy", 7); } else if (getenv("DISPLAY") != NULL) { return set_cfg_string(ctx, id, "xclip -selection clipboard", 26); } else { return set_cfg_string(ctx, id, "newsraft-osc-52", 15); } } bool obtain_notification_command(struct config_context **ctx, config_type_id id) { #ifdef __APPLE__ return set_cfg_string(ctx, id, "osascript -e 'display notification \"Newsraft brought %q news!\"'", 63); #endif if (getenv("WAYLAND_DISPLAY") != NULL || getenv("DISPLAY") != NULL) { return set_cfg_string(ctx, id, "notify-send 'Newsraft brought %q news!'", 39); } else { return set_cfg_string(ctx, id, "printf '\\e]9;Newsraft brought %q news!\\a'", 41); } } newsraft/src/load_config/config-parse-colors.c000066400000000000000000000042171516312403600217670ustar00rootroot00000000000000#include #include "load_config/load_config.h" bool config_uses_256_colors = false; bool parse_color_setting(struct config_context **ctx, config_entry_id id, const char *iter) { long long colors[2] = {TB_DEFAULT, TB_DEFAULT}; // colors[0] = fg; colors[1] = bg; uintattr_t attribute = TB_DEFAULT; unsigned i = 0; while (*iter != '\0') { while (ISWHITESPACE(*iter)) iter += 1; // Skip whitespace if (strncmp(iter, "default", 7) == 0) { colors[i] = TB_DEFAULT; i ^= 1; } else if (strncmp(iter, "black", 5) == 0) { colors[i] = TB_BLACK; i ^= 1; } else if (strncmp(iter, "red", 3) == 0) { colors[i] = TB_RED; i ^= 1; } else if (strncmp(iter, "green", 5) == 0) { colors[i] = TB_GREEN; i ^= 1; } else if (strncmp(iter, "yellow", 6) == 0) { colors[i] = TB_YELLOW; i ^= 1; } else if (strncmp(iter, "blue", 4) == 0) { colors[i] = TB_BLUE; i ^= 1; } else if (strncmp(iter, "magenta", 7) == 0) { colors[i] = TB_MAGENTA; i ^= 1; } else if (strncmp(iter, "cyan", 4) == 0) { colors[i] = TB_CYAN; i ^= 1; } else if (strncmp(iter, "white", 5) == 0) { colors[i] = TB_WHITE; i ^= 1; } else if (strncmp(iter, "bold", 4) == 0) { attribute |= TB_BOLD; } else if (strncmp(iter, "underlined", 10) == 0) { attribute |= TB_UNDERLINE; } else if (strncmp(iter, "italic", 6) == 0) { attribute |= TB_ITALIC; } else if (strncmp(iter, "color", 5) == 0) { config_uses_256_colors = true; // TODO: explain why we need this +1 here. That's some nasty stuff colors[i] = strtoll(iter + 5, NULL, 10) + 1; if (colors[i] < 0 || colors[i] > 255) { write_error("Color number must be in the range from 0 to 255!\n"); return false; } i ^= 1; } else { write_error("Color settings can only contain the following tokens:\n"); write_error("default, black, red, green, yellow, blue, magenta, cyan, white, colorN,\n"); write_error("bold, italic, underlined.\n"); return false; } while (!ISWHITESPACE(*iter) && *iter != '\0') iter += 1; // Advance to next token } set_cfg_color(ctx, id, colors[0], colors[1], attribute); return true; } newsraft/src/load_config/config-parse.c000066400000000000000000000106571516312403600204750ustar00rootroot00000000000000#include #include "load_config/load_config.h" static inline void extract_token_from_line(struct string *line, struct string *token, bool break_on_whitespace) { trim_whitespace_from_string(line); empty_string(token); size_t to_remove = 0; if (line->ptr[0] == '"' || line->ptr[0] == '\'') { to_remove = 1; for (const char *i = line->ptr + 1; *i != '\0'; ++i) { to_remove += 1; if (*i == line->ptr[0]) { break; } catcs(token, *i); } } else { for (const char *i = line->ptr; *i != ';' && *i != '\0'; ++i) { if (break_on_whitespace == true && ISWHITESPACE(*i)) { break; } catcs(token, *i); to_remove += 1; } } remove_start_of_string(line, to_remove); } static bool set_cfg_setting(struct config_context **ctx, config_entry_id id, const struct string *s) { size_t val; const char *i = s->ptr; config_type_id type = get_cfg_type(id); switch (type) { case CFG_BOOL: if (*i != '\0' && strcmp(i, "true") != 0 && strcmp(i, "false") != 0) { write_error("Boolean settings only take \"true\" and \"false\" for values!\n"); return false; } set_cfg_bool(ctx, id, *i == 'f' ? false : true); break; case CFG_UINT: if (*i == '\0' || *i == '-' || sscanf(i, "%zu", &val) != 1) { write_error("Numeric settings only take non-negative integers for values!\n"); return false; } set_cfg_uint(ctx, id, val); break; case CFG_COLOR: return parse_color_setting(ctx, id, s->ptr); case CFG_STRING: return set_cfg_string(ctx, id, s->ptr, s->len); } return true; } bool process_config_line(struct feed_entry *feed, const char *str, size_t len) { struct string *line = crtas(str, len); struct string *token = crtes(200); struct config_context **cfg = feed != NULL ? &feed->cfg : NULL; struct input_binding **binds = feed != NULL ? &feed->binds : NULL; trim_whitespace_from_string(line); if (line->len > 0) { INFO("Config line %s -> %s", feed == NULL ? "" : feed->url->ptr, line->ptr); } struct input_binding *bind = NULL; while (line->len > 0) { while (line->ptr[0] == ';') { remove_start_of_string(line, 1); } trim_whitespace_from_string(line); if (line->len <= 0) break; if (line->ptr[0] == ';') continue; INFO("Config line remainder: %s", line->ptr); if (line->ptr[0] == '#') break; // Terminate on comments extract_token_from_line(line, token, true); if (strcmp(token->ptr, "set") == 0) { // set extract_token_from_line(line, token, true); config_entry_id id = find_config_entry_by_name(token->ptr); if (id == CFG_ENTRIES_COUNT) { write_error("Setting \"%s\" doesn't exist!\n", token->ptr); goto error; } extract_token_from_line(line, token, false); if (set_cfg_setting(cfg, id, token) != true) { goto error; } continue; } else if (strcmp(token->ptr, "bind") == 0 || strcmp(token->ptr, "unbind") == 0) { // bind/unbind extract_token_from_line(line, token, true); bind = create_or_clean_bind(binds, token->ptr); continue; } else if (find_config_entry_by_name(token->ptr) != CFG_ENTRIES_COUNT) { // config_entry_id id = find_config_entry_by_name(token->ptr); extract_token_from_line(line, token, false); if (set_cfg_setting(cfg, id, token) != true) { goto error; } continue; } else if (bind != NULL) { // Takes previous bind entry into account input_id cmd = get_input_id_by_name(token->ptr); if (cmd == INPUT_ERROR) { goto error; } extract_token_from_line(line, token, false); if (!attach_action_to_bind(bind, cmd, token->ptr, token->len)) { goto error; } continue; } goto error; } free_string(line); free_string(token); return true; error: write_error("Invalid config line: %s\n", str); write_error("Erroneous token: %s\n", token->ptr); free_string(line); free_string(token); return false; } bool parse_config_file(void) { const char *config_path = get_config_path(); if (config_path == NULL) { log_config_settings(); return true; // Since a config file is optional, don't return the error. } FILE *f = fopen(config_path, "r"); if (f == NULL) { write_error("Couldn't open config file!\n"); return false; } char *line = NULL; size_t size = 0; for (ssize_t len = getline(&line, &size, f); len >= 0; len = getline(&line, &size, f)) { if (process_config_line(NULL, line, len) == false) { free(line); fclose(f); return false; } } free(line); fclose(f); log_config_settings(); return true; } newsraft/src/load_config/load_config.c000066400000000000000000000111511516312403600203520ustar00rootroot00000000000000#include #include "load_config/load_config.h" #define CONFIG_ARRAY #include "config.h" #undef CONFIG_ARRAY config_entry_id find_config_entry_by_name(const char *name) { for (config_entry_id i = 0; config[i].name != NULL; ++i) { if (strcmp(name, config[i].name) == 0) { return i; } } return CFG_ENTRIES_COUNT; } static struct config_entry * get_global_or_context_config(struct config_context **ctx, config_entry_id id, bool create) { if (ctx != NULL) { for (struct config_context *c = *ctx; c != NULL; c = c->next) { if (c->id == id) { return &c->cfg; } } if (create == true) { struct config_context *new = newsraft_calloc(1, sizeof(struct config_context)); new->id = id; new->cfg.type = get_cfg_type(id); new->next = *ctx; *ctx = new; return &new->cfg; } } return config + id; } config_type_id get_cfg_type(config_entry_id id) { return config[id].type; } bool get_cfg_bool(struct config_context **ctx, config_entry_id id) { return get_global_or_context_config(ctx, id, false)->value.b; } size_t get_cfg_uint(struct config_context **ctx, config_entry_id id) { return get_global_or_context_config(ctx, id, false)->value.u; } struct config_color get_cfg_color(struct config_context **ctx, config_entry_id id) { struct config_entry *cfg = get_global_or_context_config(ctx, id, false); return cfg->value.c; } const struct string * get_cfg_string(struct config_context **ctx, config_entry_id id) { struct config_entry *cfg = get_global_or_context_config(ctx, id, false); if (cfg->value.s.actual == NULL) { set_cfg_string(NULL, id, config[id].value.s.base, strlen(config[id].value.s.base)); } return cfg->value.s.actual; } const struct wstring * get_cfg_wstring(struct config_context **ctx, config_entry_id id) { struct config_entry *cfg = get_global_or_context_config(ctx, id, false); if (cfg->value.s.wactual == NULL) { set_cfg_string(NULL, id, config[id].value.s.base, strlen(config[id].value.s.base)); } return cfg->value.s.wactual; } void set_cfg_bool(struct config_context **ctx, config_entry_id id, bool value) { get_global_or_context_config(ctx, id, true)->value.b = value; } void set_cfg_uint(struct config_context **ctx, config_entry_id id, size_t value) { get_global_or_context_config(ctx, id, true)->value.u = value; } void set_cfg_color(struct config_context **ctx, config_entry_id id, uintattr_t fg, uintattr_t bg, uintattr_t attribute) { struct config_entry *cfg = get_global_or_context_config(ctx, id, true); cfg->value.c.fg = fg; cfg->value.c.bg = bg; cfg->value.c.attributes = attribute; } bool set_cfg_string(struct config_context **ctx, config_entry_id id, const char *src_ptr, size_t src_len) { struct config_entry *cfg = get_global_or_context_config(ctx, id, true); cpyas(&cfg->value.s.actual, src_ptr, src_len); struct wstring *w = convert_string_to_wstring(cfg->value.s.actual); if (w == NULL) { write_error("Failed to convert %s setting value to wide characters!\n", config[id].name); return false; } wstr_set(&cfg->value.s.wactual, w->ptr, w->len, w->len); free_wstring(w); if (config[id].value.s.auto_set != NULL && strcmp(cfg->value.s.actual->ptr, "auto") == 0) { if (config[id].value.s.auto_set(ctx, id) == false) { write_error("Failed to set auto value for %s setting!\n", config[id].name); return false; } } return true; } bool is_cfg_color_set(struct config_context **ctx, config_entry_id id) { struct config_entry *cfg = get_global_or_context_config(ctx, id, false); struct config_color *c = &cfg->value.c; return ( !NEWSRAFT_ALL_BITS_SET(c->fg, uintattr_t) || !NEWSRAFT_ALL_BITS_SET(c->bg, uintattr_t) || !NEWSRAFT_ALL_BITS_SET(c->attributes, uintattr_t) ); } void free_config(void) { INFO("Freeing configuration strings."); for (config_entry_id i = 0; config[i].name != NULL; ++i) { if (config[i].type == CFG_STRING) { free_string(config[i].value.s.actual); free_wstring(config[i].value.s.wactual); } } } void free_config_context(struct config_context *ctx) { for (struct config_context *c = ctx; c != NULL; ctx = c) { if (ctx->cfg.type == CFG_STRING) { free_string(ctx->cfg.value.s.actual); free_wstring(ctx->cfg.value.s.wactual); } c = ctx->next; free(ctx); } } void log_config_settings(void) { INFO("Current configuration settings:"); for (config_entry_id i = 0; config[i].name != NULL; ++i) { if (config[i].type == CFG_BOOL) { INFO("%-35s = %d", config[i].name, config[i].value.b); } else if (config[i].type == CFG_UINT) { INFO("%-35s = %zu", config[i].name, config[i].value.u); } else if (config[i].type == CFG_STRING) { INFO("%-35s = \"%s\"", config[i].name, get_cfg_string(NULL, i)->ptr); } } } newsraft/src/load_config/load_config.h000066400000000000000000000022071516312403600203610ustar00rootroot00000000000000#ifndef LOAD_CONFIG_H #define LOAD_CONFIG_H #include "newsraft.h" // See "load_config.c" file for implementation. config_entry_id find_config_entry_by_name(const char *name); config_type_id get_cfg_type(config_entry_id id); void set_cfg_bool(struct config_context **ctx, config_entry_id id, bool value); void set_cfg_uint(struct config_context **ctx, config_entry_id id, size_t value); void set_cfg_color(struct config_context **ctx, config_entry_id id, uintattr_t fg, uintattr_t bg, uintattr_t attribute); bool set_cfg_string(struct config_context **ctx, config_entry_id id, const char *src_ptr, size_t src_len); void log_config_settings(void); // See "config-auto.c" file for implementation. bool obtain_useragent_string(struct config_context **ctx, config_type_id id); bool obtain_browser_command(struct config_context **ctx, config_type_id id); bool obtain_clipboard_command(struct config_context **ctx, config_type_id id); bool obtain_notification_command(struct config_context **ctx, config_type_id id); bool config_uses_256_colors; bool parse_color_setting(struct config_context **ctx, config_entry_id id, const char *iter); #endif // LOAD_CONFIG_H newsraft/src/log.c000066400000000000000000000027571516312403600144370ustar00rootroot00000000000000#include #include #include "newsraft.h" static FILE *log_stream = NULL; bool log_init(const char *path) { if (path == NULL) { write_error("Path to the log file is not set!\n"); return false; } log_stream = fopen(path, "w"); if (log_stream == NULL) { write_error("Failed to open log file for writing!\n"); return false; } INFO("Okay... Here we go. Focus. Speed. I am speed."); INFO("newsraft version: %s", NEWSRAFT_VERSION); INFO("SQLite version: %s", sqlite3_libversion()); INFO("curl version: %s", curl_version()); INFO("expat version: %s", XML_ExpatVersion()); return true; } int log_vprint(const char *prefix, const char *format, va_list args) { int len = -1; if (log_stream != NULL) { struct timespec t = {0}; clock_gettime(CLOCK_REALTIME, &t); len = fprintf(log_stream, "%02d:%02d:%02d.%03d [%s] ", (int)(t.tv_sec / 3600), (int)(t.tv_sec / 60 % 60), (int)(t.tv_sec % 60), (int)(t.tv_nsec / 1000000), prefix ); vfprintf(log_stream, format, args); fputc('\n', log_stream); fflush(log_stream); } return len; } int log_print(const char *prefix, const char *format, ...) { int len = -1; if (log_stream != NULL) { va_list args; va_start(args, format); len = log_vprint(prefix, format, args); va_end(args); } return len; } FILE * log_get_stream(void) { return log_stream; } void log_stop(int error_code) { if (log_stream != NULL) { INFO("Quitting the program with an exit code %d.", error_code); fclose(log_stream); } } newsraft/src/newsraft.c000066400000000000000000000202331516312403600154740ustar00rootroot00000000000000#include #include #include #include "newsraft.h" /* find -name '*.c' | sed -e 's/^\.\//#include "/' -e 's/$/"/' | grep -v 'newsraft.c' | sort */ #include "curses.c" #include "alloc.c" #include "binds.c" #include "commands.c" #include "dates.c" #include "db.c" #include "db-items.c" #include "downloader.c" #include "errors.c" #include "executor.c" #include "feeds.c" #include "feeds-parse.c" #include "inserter.c" #include "insert_feed/insert_feed.c" #include "insert_feed/insert-feed-data.c" #include "insert_feed/insert-item-data.c" #include "interface.c" #include "interface-errors-pager.c" #include "interface-list.c" #include "interface-list-pager.c" #include "interface-status.c" #include "items.c" #include "items-list.c" #include "items-metadata.c" #include "items-metadata-content.c" #include "items-metadata-links.c" #include "items-metadata-persons.c" #include "items-pager.c" #include "load_config/config-auto.c" #include "load_config/config-parse.c" #include "load_config/config-parse-colors.c" #include "load_config/load_config.c" #include "log.c" #include "parse_json/setup-json-parser.c" #include "parse_xml/common.c" #include "parse_xml/format-atom.c" #include "parse_xml/format-dublincore.c" #include "parse_xml/format-georss.c" #include "parse_xml/format-mediarss.c" #include "parse_xml/format-rss.c" #include "parse_xml/gperf-data.c" #include "parse_xml/setup-opml-parser.c" #include "parse_xml/setup-xml-parser.c" #include "path.c" #include "queue.c" #include "render-block.c" #include "render_data/line.c" #include "render_data/render_data.c" #include "render_data/render-text-html.c" #include "render_data/render-text-plain.c" #include "sections.c" #include "signal.c" #include "sorting.c" #include "string.c" #include "string-serialize.c" #include "struct-item.c" #include "threads.c" #include "wstring.c" #include "wstring-format.c" struct newsraft_execution_stage { const char *description; bool (*constructor)(void); void (*destructor)(void); }; volatile bool they_want_us_to_stop = false; static inline void print_usage(void) { fputs("newsraft - feed reader for terminal\n" "\n" " -f PATH force use of PATH as feeds file\n" " -c PATH force use of PATH as config file\n" " -d PATH force use of PATH as database file\n" " -l PATH write log information to PATH\n" " -e ACTION execute ACTION and exit\n" " -v print version and successfully exit\n" " -h print this message and successfully exit\n" "\n" "ACTION is one of the following:\n" "\n" " convert-opml-to-feeds\n" " convert-feeds-to-opml\n" " reload-all\n" " print-unread-items-count\n" " purge-abandoned\n", stderr); } static const struct newsraft_execution_stage regular_mode[] = { {"register signal handlers", register_signal_handlers, NULL}, {"assign default binds", assign_default_binds, free_default_binds}, {"load config file", parse_config_file, NULL}, {"initialize database", db_init, db_stop}, {"execute database optimization", exec_database_file_optimization, NULL}, {"load feeds file", parse_feeds_file, free_sections}, {"initialize user interface", ui_init, ui_term}, {"create list menu", adjust_list_menu, free_list_menu}, {"create status field", status_recreate_unprotected, status_delete}, {"initialize curl library", curl_init, curl_stop}, {"start worker threads", threads_start, threads_stop}, {"set terminal window title", ui_set_window_title, NULL}, {"run menu loop", run_menu_loop, free_menus}, }; static const struct newsraft_execution_stage reload_mode[] = { {"register signal handlers", register_signal_handlers, NULL}, {"load config file", parse_config_file, NULL}, {"initialize database", db_init, db_stop}, {"execute database optimization", exec_database_file_optimization, NULL}, {"load feeds file", parse_feeds_file, free_sections}, {"initialize curl library", curl_init, curl_stop}, {"start worker threads", threads_start, threads_stop}, {"update all feeds", start_updating_all_feeds_and_wait_finish, NULL}, }; static const struct newsraft_execution_stage convert_opml_to_feeds_mode[] = { {"convert OPML stream to feeds", convert_opml_to_feeds, free_sections}, }; static const struct newsraft_execution_stage convert_feeds_to_opml_mode[] = { {"initialize database", db_init, db_stop}, {"load feeds file", parse_feeds_file, free_sections}, {"convert parsed feeds to OPML", convert_feeds_to_opml, NULL}, }; static const struct newsraft_execution_stage print_unread_mode[] = { {"initialize database", db_init, db_stop}, {"load feeds file", parse_feeds_file, free_sections}, {"print unread items count", print_unread_items_count, NULL}, }; static const struct newsraft_execution_stage purge_mode[] = { {"initialize database", db_init, db_stop}, {"load feeds file", parse_feeds_file, free_sections}, {"purge abandoned feeds", purge_abandoned_feeds, NULL}, }; static int run_scenario(const struct newsraft_execution_stage *scenario, size_t scenario_stages_count) { int error = 0; size_t stage = 0; for (stage = 0; stage < scenario_stages_count; ++stage) { INFO("Trying to %s...", scenario[stage].description); if (scenario[stage].constructor() != true) { write_error("Failed to %s\n", scenario[stage].description); error = stage; break; } } for (they_want_us_to_stop = true; stage > 0; --stage) { if (scenario[stage - 1].destructor) { INFO("Cleaning after %s...", scenario[stage - 1].description); scenario[stage - 1].destructor(); } } return error; } int main(int argc, char **argv) { int error = 0; const char *locale = setlocale(LC_ALL, ""); if (!locale || *locale == '\0' || strcmp(locale, "C") == 0) { write_error("Invalid locale settings detected in the environment!\n"); write_error("This usually happens when LANG or LC_ALL is not set.\n"); goto undo1; } int opt; while ((opt = getopt(argc, argv, "f:c:d:l:e:vh")) != -1) { if (opt == 'f') { if (set_feeds_path(optarg) == false) { error = 2; goto undo1; } } else if (opt == 'c') { if (set_config_path(optarg) == false) { error = 3; goto undo1; } } else if (opt == 'd') { if (set_db_path(optarg) == false) { error = 4; goto undo1; } } else if (opt == 'l') { if (log_init(optarg) == false) { error = 5; goto undo1; } } else if (opt == 'e') { if (strcmp(optarg, "reload-all") == 0) { error = run_scenario(reload_mode, LENGTH(reload_mode)); } else if (strcmp(optarg, "print-unread-items-count") == 0) { error = run_scenario(print_unread_mode, LENGTH(print_unread_mode)); } else if (strcmp(optarg, "convert-opml-to-feeds") == 0) { error = run_scenario(convert_opml_to_feeds_mode, LENGTH(convert_opml_to_feeds_mode)); } else if (strcmp(optarg, "convert-feeds-to-opml") == 0) { error = run_scenario(convert_feeds_to_opml_mode, LENGTH(convert_feeds_to_opml_mode)); } else if (strcmp(optarg, "purge-abandoned") == 0) { error = run_scenario(purge_mode, LENGTH(purge_mode)); } else { fputs("Invalid action for execution. See newsraft(1) for details.\n", stderr); error = 6; } goto undo1; } else if (opt == 'v') { fputs(NEWSRAFT_VERSION "\n", stderr); goto undo1; } else if (opt == 'h') { print_usage(); goto undo1; } else { fprintf(stderr, "Try '%s -h' for more information.\n", argv[0]); error = 1; goto undo1; } } error = run_scenario(regular_mode, LENGTH(regular_mode)); undo1: free_config(); flush_errors(); log_stop(error); return error; } newsraft/src/newsraft.h000066400000000000000000000563751516312403600155210ustar00rootroot00000000000000#ifndef NEWSRAFT_H #define NEWSRAFT_H #include #include #include #include #include #include #include #include #include #include #include "termbox2.h" #ifndef NEWSRAFT_VERSION #define NEWSRAFT_VERSION "0.36" #endif #define ISWHITESPACE(A) (((A)==' ')||((A)=='\n')||((A)=='\t')||((A)=='\v')||((A)=='\f')||((A)=='\r')) #define ISWHITESPACEEXCEPTNEWLINE(A) (((A)==' ')||((A)=='\t')||((A)=='\v')||((A)=='\f')||((A)=='\r')) #define ISWIDEWHITESPACE(A) (((A)==L' ')||((A)==L'\n')||((A)==L'\t')||((A)==L'\v')||((A)==L'\f')||((A)==L'\r')) #define ISDIGIT(A) (((A)=='0')||((A)=='1')||((A)=='2')||((A)=='3')||((A)=='4')||((A)=='5')||((A)=='6')||((A)=='7')||((A)=='8')||((A)=='9')) #define INFO(...) log_print("INFO", __VA_ARGS__) #define WARN(...) log_print("WARN", __VA_ARGS__) #define FAIL(...) log_print("FAIL", __VA_ARGS__) #define info_status(...) status_write(CFG_COLOR_STATUS_INFO, __VA_ARGS__) #define fail_status(...) status_write(CFG_COLOR_STATUS_FAIL, __VA_ARGS__) #define LENGTH(A) ((sizeof(A))/(sizeof(*A))) #define NEWSRAFT_MIN(A, B) (((A) < (B)) ? (A) : (B)) // Need prefix because some platforms have their own MIN definition #define NEWSRAFT_UI(CALL) do { if (ui_is_running()) { CALL; } } while (0) // Make CALL only if UI is running #define STRING_IS_EMPTY(A) (((A) == NULL) || ((A)->ptr == NULL) || ((A)->len == 0)) #define NEWSRAFT_ALL_BITS_SET(x, type) ((x) == (type)~(type)0) typedef uint8_t config_entry_id; typedef uint8_t input_id; typedef int newsraft_video_t; typedef struct WINDOW WINDOW; enum { FEED_COLUMN_FEED_URL, FEED_COLUMN_TITLE, FEED_COLUMN_LINK, FEED_COLUMN_CONTENT, FEED_COLUMN_ATTACHMENTS, FEED_COLUMN_PERSONS, FEED_COLUMN_EXTRAS, FEED_COLUMN_DOWNLOAD_DATE, FEED_COLUMN_UPDATE_DATE, FEED_COLUMN_TIME_TO_LIVE, FEED_COLUMN_HTTP_HEADER_ETAG, FEED_COLUMN_HTTP_HEADER_LAST_MODIFIED, FEED_COLUMN_HTTP_HEADER_EXPIRES, FEED_COLUMN_NONE, }; enum { ITEM_COLUMN_FEED_URL, ITEM_COLUMN_GUID, ITEM_COLUMN_TITLE, ITEM_COLUMN_LINK, ITEM_COLUMN_CONTENT, ITEM_COLUMN_ATTACHMENTS, ITEM_COLUMN_PERSONS, ITEM_COLUMN_EXTRAS, ITEM_COLUMN_DOWNLOAD_DATE, ITEM_COLUMN_PUBLICATION_DATE, ITEM_COLUMN_UPDATE_DATE, ITEM_COLUMN_UNREAD, ITEM_COLUMN_IMPORTANT, ITEM_COLUMN_NONE, }; enum { SECTIONS_MENU = 0, FEEDS_MENU, ITEMS_MENU, PAGER_MENU, MENUS_COUNT, }; enum { MENU_NORMAL = 0, MENU_IS_EXPLORE = 1, // Tell items menu to use explore mode MENU_IS_SEARCH = 2, // Identifies items search menu MENU_SWALLOW = 4, // Replace current menu with this menu MENU_DISABLE_SETTINGS = 8, }; typedef enum { // Even is ascending, odd is descending SORT_BY_INITIAL_ASC = 0, SORT_BY_INITIAL_DESC, SORT_BY_TIME_ASC, SORT_BY_TIME_DESC, SORT_BY_TIME_DOWNLOAD_ASC, SORT_BY_TIME_DOWNLOAD_DESC, SORT_BY_TIME_PUBLICATION_ASC, SORT_BY_TIME_PUBLICATION_DESC, SORT_BY_TIME_UPDATE_ASC, SORT_BY_TIME_UPDATE_DESC, SORT_BY_ROWID_ASC, SORT_BY_ROWID_DESC, SORT_BY_UNREAD_ASC, SORT_BY_UNREAD_DESC, SORT_BY_ALPHABET_ASC, SORT_BY_ALPHABET_DESC, SORT_BY_IMPORTANT_ASC, SORT_BY_IMPORTANT_DESC, SORT_METHODS_COUNT, } sorting_method_t; typedef uint8_t render_block_format; enum { TEXT_PLAIN, TEXT_RAW, // Same thing as TEXT_PLAIN, but without link marks TEXT_HTML, TEXT_LINKS, // Special block type which has to be populated with links }; enum { NEWSRAFT_THREAD_DOWNLOAD = 0, // Downloads the feed from the web NEWSRAFT_THREAD_SHRUNNER = 1, // Reads the feed from the command NEWSRAFT_THREAD_DBWRITER = 2, // Writes the feed to the database }; enum { NEWSRAFT_HTTP_NOT_MODIFIED = 304, NEWSRAFT_HTTP_TOO_MANY_REQUESTS = 429, }; struct config_context; struct deserialize_stream; struct string { char *ptr; size_t len; size_t lim; }; struct wstring { wchar_t *ptr; size_t len; size_t lim; }; struct binding_action { input_id cmd; struct wstring *arg; }; struct input_binding { struct string *key; struct binding_action *actions; size_t actions_count; struct input_binding *next; }; struct feed_entry { struct string *url; // feed URL specified by the user struct string *name; struct string *link; // link to webpage related to this feed int64_t unread_count; int64_t items_count; int64_t update_date; // date of last feed update attempt int64_t section_index; struct config_context *cfg; struct input_binding *binds; struct string *errors; }; struct item_entry { struct string *guid; struct string *title; struct string *url; struct feed_entry **feed; int64_t rowid; bool is_unread; bool is_important; int64_t pub_date; int64_t upd_date; struct string *date_str; struct string *pub_date_str; }; struct items_list { sqlite3_stmt *res; struct string *query; struct string *find_filter; struct item_entry *ptr; size_t len; bool finished; sorting_method_t sorting; struct feed_entry **feeds; // Just a pointer to parent feeds size_t feeds_count; }; struct format_arg { const wchar_t specifier; const wchar_t type_specifier; union { int i; const char *s; } value; }; struct menu_state { struct string *name; struct menu_state *(*run)(struct menu_state *); // Function used to start menu struct feed_entry **feeds_original; // Remains unchanged to use original order struct feed_entry **feeds; // Virtual feeds with user sorting applied size_t feeds_count; // Size of feeds_original and feeds arrays struct items_list *items; uint64_t age; // Must refresh if it doesn't match global age uint32_t flags; size_t view_sel; // Index of the selected entry size_t view_min; // Index of the first visible entry size_t view_max; // Index of the last visible entry bool is_initialized; bool is_deleted; struct string *search_token; const struct wstring *entry_format; const struct wstring *find_filter; bool (*enumerator)(struct menu_state *ctx, size_t index); // Checks if index is valid void (*printer)(size_t index, WINDOW *w); // Prints to list entry const struct format_arg *(*get_args)(struct menu_state *ctx, size_t index); struct config_color (*paint_action)(struct menu_state *ctx, size_t index, bool is_selected); bool (*unread_state)(struct menu_state *ctx, size_t index); bool (*failed_state)(struct menu_state *ctx, size_t index); struct menu_state *prev; }; struct render_block { struct wstring *content; render_block_format content_type; bool needs_trimming; }; struct link { struct string *url; // URL link to data. struct string *type; // Standard MIME type of data. struct string *size; // Size of data in bytes. struct string *duration; // Duration of data in seconds. }; struct links_list { struct link *ptr; size_t len; }; struct render_blocks_list { struct render_block *ptr; size_t len; struct links_list links; }; struct render_line { struct wstring *ws; newsraft_video_t *hints; size_t hints_len; size_t indent; }; struct render_result { struct render_line *lines; size_t lines_len; }; struct getfeed_item { struct string *guid; struct string *title; struct string *link; struct string *content; struct string *attachments; struct string *persons; struct string *extras; int64_t publication_date; // Publication date in seconds since the Epoch (0 means unset). int64_t update_date; // Update date in seconds since the Epoch (0 means unset). bool guid_is_link; struct getfeed_item *next; }; struct getfeed_feed { struct string *title; struct string *link; struct string *content; struct string *attachments; struct string *persons; struct string *extras; int64_t time_to_live; struct string *http_header_etag; int64_t http_header_last_modified; int64_t http_header_expires; struct getfeed_item *item; }; struct feed_update_state { struct feed_entry *feed_entry; struct string *new_errors; CURL *curl; char curl_error[CURL_ERROR_SIZE]; struct curl_slist *download_headers; long http_response_code; // This function processes pieces of feed coming to us during the download. bool (*process_fn)(struct feed_update_state *, const char *, size_t, bool); XML_Parser xml_parser; struct getfeed_feed feed; bool in_item; struct string *text; uint8_t path[256]; uint8_t depth; struct string decoy; struct string *emptying_target; size_t new_items_count; bool curl_handle_added_to_multi; bool is_in_progress; bool is_downloaded; bool is_failed; bool is_canceled; bool is_finished; struct feed_update_state *next; }; // See "sections.c" file for implementation. int64_t make_sure_section_exists(const struct string *section_name); struct feed_entry *copy_feed_to_section(const struct feed_entry *feed_data, int64_t section_index); void refresh_sections_statistics_about_underlying_feeds(void); bool purge_abandoned_feeds(void); struct menu_state *sections_menu_loop(struct menu_state *m); void free_sections(void); bool start_updating_all_feeds_and_wait_finish(void); bool print_unread_items_count(void); void process_auto_updating_feeds(void); void mark_feeds_read(struct feed_entry **feeds, size_t feeds_count, bool status); struct feed_entry **get_all_feeds(size_t *feeds_count); char *get_section_name(size_t section_index); // See "feeds-parse.c" file for implementation. bool parse_feeds_file(void); // See "feeds.c" file for implementation. struct menu_state *feeds_menu_loop(struct menu_state *m); // See "interface-list.c" file for implementation. bool is_current_menu_a_pager(void); bool adjust_list_menu(void); void free_list_menu(void); void list_menu_writer(size_t index, WINDOW *w); void expose_entry_of_the_list_menu(size_t index); void expose_all_visible_entries_of_the_list_menu(void); void redraw_list_menu_unprotected(void); void reset_list_menu_unprotected(void); bool handle_list_menu_control(struct menu_state *m, input_id cmd, const struct wstring *arg); bool handle_pager_menu_control(input_id cmd); void free_menus(void); size_t get_menu_depth(void); struct menu_state *setup_menu(struct menu_state *(*run)(struct menu_state *), const struct string *name, struct feed_entry **feeds, size_t feeds_count, uint32_t flags, const void *ctx); struct menu_state *close_menu(void); void start_menu(void); void write_menu_path_string(struct string *names, struct menu_state *m); void raise_menu_age(void); uint64_t fetch_menu_age(void); // See "interface-list-pager.c" file for implementation. bool is_pager_pos_valid(struct menu_state *ctx, size_t index); void pager_menu_writer(size_t index, WINDOW *w); bool start_pager_menu(struct config_context **new_ctx, struct render_blocks_list *new_blocks); bool refresh_pager_menu(void); // See "wstring-format.c" file for implementation. void do_format(struct wstring *dest, const wchar_t *fmt, const struct format_arg *args); // See "sorting.c" file for implementation. sorting_method_t get_sorting_id(const char *sorting_name); const char *get_sorting_message(int sorting_id); // See "items.c" file for implementation. struct string *generate_items_search_condition(struct feed_entry **feeds, size_t feeds_count); bool important_item_condition(struct menu_state *ctx, size_t index); struct menu_state *items_menu_loop(struct menu_state *dest); // See "items-list.c" file for implementation. bool update_menu_item_list(struct menu_state *ctx); void obtain_items_at_least_up_to_the_given_index(struct items_list *items, size_t index); void change_items_list_sorting(struct menu_state *ctx, input_id cmd); void free_items_list(struct items_list *items); // See "items-pager.c" file for implementation. struct menu_state *item_pager_loop(struct menu_state *m); // Functions responsible for managing render blocks. // Render block is a piece of text in a single format. A list of render blocks // is passed to render_data function which processes them based on their types // and generates a single plain text buffer for a pager to display. // See "render-block.c" file for implementation. bool add_render_block(struct render_blocks_list *blocks, const char *content, size_t content_len, render_block_format content_type, bool needs_trimming); void apply_links_render_blocks(struct render_blocks_list *blocks, const struct wstring *data); void free_render_blocks(struct render_blocks_list *blocks); void free_render_result(struct render_result *render); // See "render_data" directory for implementation. bool render_data(struct config_context **ctx, struct render_result *result, struct render_blocks_list *blocks, size_t content_width); // See "items-metadata.c" file for implementation. bool generate_render_blocks_based_on_item_data(struct render_blocks_list *blocks, const struct item_entry *item, sqlite3_stmt *res); // See "items-metadata-content.c" file for implementation. bool get_largest_piece_from_item_content(const char *content, struct string **text, render_block_format *type); bool get_largest_piece_from_item_attachments(const char *attachments, struct string **text, render_block_format *type); // See "items-metadata-links.c" file for implementation. char *complete_url(const char *base, const char *rel); int64_t add_url_to_links_list(struct links_list *links, const char *url, size_t url_len); struct wstring *generate_link_list_wstring_for_pager(struct config_context **ctx, const struct links_list *links); bool add_item_attachments_to_links_list(struct links_list *links, sqlite3_stmt *res); // See "items-metadata-persons.c" file for implementation. struct string *deserialize_persons_string(const char *src); // See "path.c" file for implementation. bool set_feeds_path(const char *path); bool set_config_path(const char *path); bool set_db_path(const char *path); const char *get_feeds_path(void); const char *get_config_path(void); const char *get_db_path(void); // See "dates.c" file for implementation. int64_t parse_date(const char *str, bool rfc3339_first); struct string *get_cfg_date(struct config_context **ctx, config_entry_id format_id, int64_t date); struct timespec newsraft_get_monotonic_time(void); struct string *newsraft_get_pretty_time_diff(struct timespec *start, struct timespec *stop); // See "db.c" file for implementation. bool db_init(void); bool db_vacuum(void); bool exec_database_file_optimization(void); void db_stop(void); sqlite3_stmt *db_prepare(const char *statement, int size, const char **p_error); bool db_transaction_begin(void); bool db_transaction_commit(void); bool db_rollback_transaction(void); const char *db_error_string(void); int db_bind_string(sqlite3_stmt *stmt, int pos, const struct string *str); int db_bind_feed_url(sqlite3_stmt *stmt, int pos, const struct string *str); int64_t db_get_date_from_feeds_table(const struct string *url, const char *column, size_t column_len); struct string *db_get_string_from_feed_table(const struct string *url, const char *column, size_t column_len); void db_update_feed_int64(const struct string *url, const char *column_name, int64_t value, bool only_positive); void db_update_feed_string(const struct string *url, const char *column_name, const struct string *value, bool only_nonempty); void db_perform_user_edit(const struct wstring *fmt, struct feed_entry **feeds, size_t feeds_count, const struct item_entry *item); // See "db-items.c" file for implementation. sqlite3_stmt *db_find_item_by_rowid(int64_t rowid); bool db_mark_item_read(int64_t rowid, bool status); bool db_mark_item_important(int64_t rowid, bool status); int64_t db_count_items(struct feed_entry **feeds, size_t feeds_count, bool count_only_unread); bool db_change_unread_status_of_all_items_in_feeds(struct feed_entry **feeds, size_t feeds_count, bool unread); // See "interface.c" file for implementation. bool ui_init(void); void ui_term(void); bool ui_is_running(void); bool ui_set_window_title(void); bool run_menu_loop(void); input_id resize_handler(void); bool call_resize_handler_if_current_list_menu_size_is_different_from_actual(void); bool arent_we_colorful(void); // Functions related to window which displays status messages. // See "interface-status.c" file for implementation. void update_status_window_content_unprotected(void); bool status_recreate_unprotected(void); void status_clean_unprotected(void); void status_clean(void); void prevent_status_cleaning(void); void allow_status_cleaning(void); void status_write(config_entry_id color, const char *format, ...); void status_delete(void); input_id get_input(struct input_binding *ctx, uint32_t *count, const struct wstring **p_arg); struct string *pop_search_filter(void); // Forces get_input() function to quit waiting for user input and return an empty command. // This lets menu routines redraw the screen immediately after a state change, // rather than waiting for user to submit a valid action for menu routine to process. // Think of it as redraw alarm. void yield_control_to_menu(void); // See "interface-errors-pager.c" file for implementation. struct menu_state *errors_pager_loop(struct menu_state *m); // Functions responsible for managing of key bindings. // See "binds.c" file for implementation. input_id get_action_of_bind(struct input_binding *ctx, const char *key, size_t action_index, const struct wstring **p_arg); struct input_binding *create_or_clean_bind(struct input_binding **target, const char *key); bool attach_action_to_bind(struct input_binding *bind, input_id cmd, const char *arg, size_t arg_len); bool assign_default_binds(void); input_id get_input_id_by_name(const char *name); bool is_escape_key_used(void); void free_binds(struct input_binding *target); void free_default_binds(void); // Functions related to executing system commands. // See "commands.c" file for implementation. void copy_string_to_clipboard(const struct string *src); void run_formatted_command(const struct wstring *wcmd_fmt, const struct format_arg *args); // See "string.c" file for implementation. struct string *crtes(size_t desired_capacity); struct string *crtas(const char *src_ptr, size_t src_len); struct string *crtss(const struct string *src); void cpyas(struct string **dest, const char *src_ptr, size_t src_len); void cpyss(struct string **dest, const struct string *src); void catas(struct string *dest, const char *src_ptr, size_t src_len); void catss(struct string *dest, const struct string *src); void catcs(struct string *dest, char c); void make_string_fit_more(struct string **dest, size_t n); void str_vappendf(struct string *dest, const char *fmt, va_list args); void str_appendf(struct string *dest, const char *fmt, ...); void empty_string(struct string *dest); void free_string(struct string *str); void trim_whitespace_from_string(struct string *str); struct wstring *convert_string_to_wstring(const struct string *src); struct wstring *convert_array_to_wstring(const char *src_ptr, size_t src_len); void remove_start_of_string(struct string *str, size_t size); void inlinefy_string(struct string *title); void newsraft_simple_hash(struct string **dest, const char *src); struct string *newsraft_base64_encode(const uint8_t *data, size_t size); // See "string-serialize.c" file for implementation. void serialize_caret(struct string **target); void serialize_array(register struct string **target, register const char *key, register size_t key_len, register const char *value, register size_t value_len); void serialize_string(struct string **target, const char *key, size_t key_len, const struct string *value); struct deserialize_stream *open_deserialize_stream(const char *serialized_data); const struct string *get_next_entry_from_deserialize_stream(struct deserialize_stream *stream); void close_deserialize_stream(struct deserialize_stream *stream); // See "wstring.c" file for implementation. void wstr_set(struct wstring **dest, const wchar_t *src_ptr, size_t src_len, size_t src_lim); struct wstring *wcrtes(size_t desired_capacity); struct wstring *wcrtas(const wchar_t *src_ptr, size_t src_len); void wcatas(struct wstring *dest, const wchar_t *src_ptr, size_t src_len); void wcatss(struct wstring *dest, const struct wstring *src); void wcatcs(struct wstring *dest, wchar_t c); void make_sure_there_is_enough_space_in_wstring(struct wstring *dest, size_t need_space); void empty_wstring(struct wstring *dest); void free_wstring(struct wstring *wstr); struct string *convert_wstring_to_string(const struct wstring *src); struct string *convert_warray_to_string(const wchar_t *src_ptr, size_t src_len); // See "signal.c" file for implementation. bool register_signal_handlers(void); // Parse config file, fill out config_data structure, bind keys to actions. // See "load_config" directory for implementation. bool process_config_line(struct feed_entry *feed, const char *str, size_t len); bool parse_config_file(void); bool get_cfg_bool(struct config_context **ctx, config_entry_id id); size_t get_cfg_uint(struct config_context **ctx, config_entry_id id); struct config_color get_cfg_color(struct config_context **ctx, config_entry_id id); bool is_cfg_color_set(struct config_context **ctx, config_entry_id id); const struct string *get_cfg_string(struct config_context **ctx, config_entry_id id); const struct wstring *get_cfg_wstring(struct config_context **ctx, config_entry_id id); void free_config(void); void free_config_context(struct config_context *cfg); // Download, process and store new items of feed. // See "queue.c" file for implementation. void queue_updates(struct feed_entry **feeds, size_t feeds_count); void queue_wait_finish(void); struct feed_update_state *queue_pull(bool (*condition)(struct feed_update_state *)); void queue_destroy(void); void queue_examine(void); // Functions for opening and closing the log stream. // To write to the log stream use macros INFO, WARN or FAIL. // See "log.c" file for implementation. bool log_init(const char *path); int log_vprint(const char *prefix, const char *format, va_list args); int log_print(const char *prefix, const char *format, ...); FILE *log_get_stream(void); void log_stop(int error_code); // Functions for buffering errors to prevent UI from erasing printed text. // See "errors.c" file for implementation. void write_error(const char *format, ...); void flush_errors(void); // See "downloader.c" file for implementation. void *downloader_worker(void *dummy); bool curl_init(void); void curl_stop(void); void downloader_curl_wakeup(void); void remove_downloader_handle(struct feed_update_state *data); // See "executor.c" file for implementation. void *executor_worker(void *dummy); // See "inserter.c" file for implementation. void *inserter_worker(void *dummy); // See "threads.c" file for implementation. bool threads_start(void); void threads_wake_up(int thread_id); void threads_take_a_nap(int thread_id); void threads_stop(void); // See "parse_xml" directory for implementation. bool setup_xml_parser(struct feed_update_state *data); bool convert_opml_to_feeds(void); bool convert_feeds_to_opml(void); // See "parse_json" directory for implementation. bool newsraft_json_parse(struct feed_update_state *data, const char *content, size_t content_size); // See "insert_feed" directory for implementation. bool insert_feed(struct feed_entry *feed, struct getfeed_feed *feed_data); // See "struct-item.c" file for implementation. void prepend_item(struct getfeed_item **head_item_ptr); void free_item(struct getfeed_item *item); // See "alloc.c" file for implementation. void *newsraft_malloc(size_t size); void *newsraft_calloc(size_t n, size_t size); void *newsraft_realloc(void *ptr, size_t size); void newsraft_free(void *ptr); extern volatile bool they_want_us_to_stop; extern size_t list_menu_height; extern size_t list_menu_width; extern pthread_mutex_t interface_lock; #include "config.h" #include "input.h" #endif // NEWSRAFT_H newsraft/src/parse_json/000077500000000000000000000000001516312403600156425ustar00rootroot00000000000000newsraft/src/parse_json/setup-json-parser.c000066400000000000000000000137751516312403600214240ustar00rootroot00000000000000#include #include "newsraft.h" bool newsraft_json_parse(struct feed_update_state *data, const char *content, size_t content_size) { sqlite3 *db; if (sqlite3_open(":memory:", &db) != SQLITE_OK) { str_appendf(data->new_errors, "JSON parser failed: %s\n", sqlite3_errmsg(db)); return false; } sqlite3_stmt *stmt; const char *sql = "SELECT key, path, value FROM json_tree(?) WHERE type != 'object' AND type != 'array';"; if (sqlite3_prepare_v2(db, sql, strlen(sql) + 1, &stmt, 0) != SQLITE_OK) { str_appendf(data->new_errors, "JSON parser failed: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return false; } if (sqlite3_bind_text(stmt, 1, content, content_size, SQLITE_STATIC) != SQLITE_OK) { str_appendf(data->new_errors, "JSON parser failed: %s\n", sqlite3_errmsg(db)); sqlite3_finalize(stmt); sqlite3_close(db); return false; } int64_t current_item_index = -1; int64_t current_author_index = -1; int64_t current_attachment_index = -1; long long dummy = -1; while (true) { int res = sqlite3_step(stmt); if (res == SQLITE_DONE) { break; } else if (res != SQLITE_ROW) { str_appendf(data->new_errors, "JSON parser failed: %s\n", sqlite3_errmsg(db)); sqlite3_finalize(stmt); sqlite3_close(db); return false; } const char *key = (const char *)sqlite3_column_text(stmt, 0); const char *path = (const char *)sqlite3_column_text(stmt, 1); const char *value = (const char *)sqlite3_column_text(stmt, 2); INFO("%-30s|%-30s|%-30s\n", key ? key : "(null)", path ? path : "(null)", value ? value : "(null)"); if (!key || !path || !value) { continue; } long long new_item_index = -1; long long new_author_index = -1; long long new_attachment_index = -1; if (strcmp(path, "$") == 0) { if (strcmp(key, "home_page_url") == 0) { cpyas(&data->feed.link, value, strlen(value)); } else if (strcmp(key, "title") == 0) { cpyas(&data->feed.title, value, strlen(value)); } else if (strcmp(key, "description") == 0) { serialize_caret(&data->feed.content); serialize_array(&data->feed.content, "text=", 5, value, strlen(value)); } continue; } if (sscanf(path, "$.authors[%lld]", &new_author_index) == 1 && new_author_index >= 0) { if (new_author_index != current_author_index) { current_author_index = new_author_index; serialize_caret(&data->feed.persons); serialize_array(&data->feed.persons, "type=", 5, "author", 6); } if (strcmp(key, "name") == 0) { serialize_array(&data->feed.persons, "name=", 5, value, strlen(value)); } else if (strcmp(key, "url") == 0) { serialize_array(&data->feed.persons, "url=", 4, value, strlen(value)); } continue; } if (sscanf(path, "$.items[%lld]", &new_item_index) == 1 && new_item_index >= 0) { if (new_item_index != current_item_index) { current_item_index = new_item_index; current_author_index = -1; current_attachment_index = -1; prepend_item(&data->feed.item); } if (data->feed.item == NULL) { continue; } if (sscanf(path, "$.items[%lld].authors[%lld]", &dummy, &new_author_index) == 2 && new_author_index >= 0) { if (new_author_index != current_author_index) { current_author_index = new_author_index; serialize_caret(&data->feed.item->persons); serialize_array(&data->feed.item->persons, "type=", 5, "author", 6); } if (strcmp(key, "name") == 0) { serialize_array(&data->feed.item->persons, "name=", 5, value, strlen(value)); } else if (strcmp(key, "url") == 0) { serialize_array(&data->feed.item->persons, "url=", 4, value, strlen(value)); } } else if (sscanf(path, "$.items[%lld].attachments[%lld]", &dummy, &new_attachment_index) == 2 && new_attachment_index >= 0) { if (new_attachment_index != current_attachment_index) { current_attachment_index = new_attachment_index; serialize_caret(&data->feed.item->attachments); } if (strcmp(key, "url") == 0) { serialize_array(&data->feed.item->attachments, "url=", 4, value, strlen(value)); } else if (strcmp(key, "mime_type") == 0) { serialize_array(&data->feed.item->attachments, "type=", 5, value, strlen(value)); } else if (strcmp(key, "size_in_bytes") == 0) { serialize_array(&data->feed.item->attachments, "size=", 5, value, strlen(value)); } else if (strcmp(key, "duration_in_seconds") == 0) { serialize_array(&data->feed.item->attachments, "duration=", 9, value, strlen(value)); } } else { if (strcmp(key, "id") == 0) { cpyas(&data->feed.item->guid, value, strlen(value)); } else if (strcmp(key, "url") == 0) { cpyas(&data->feed.item->link, value, strlen(value)); } else if (strcmp(key, "title") == 0) { cpyas(&data->feed.item->title, value, strlen(value)); } else if (strcmp(key, "content_html") == 0) { serialize_caret(&data->feed.item->content); serialize_array(&data->feed.item->content, "type=", 5, "text/html", 9); serialize_array(&data->feed.item->content, "text=", 5, value, strlen(value)); } else if (strcmp(key, "content_text") == 0) { serialize_caret(&data->feed.item->content); serialize_array(&data->feed.item->content, "text=", 5, value, strlen(value)); } else if (strcmp(key, "summary") == 0) { serialize_caret(&data->feed.item->content); serialize_array(&data->feed.item->content, "text=", 5, value, strlen(value)); } else if (strcmp(key, "date_published") == 0) { data->feed.item->publication_date = parse_date_rfc3339(strlen(value) > 18 ? value : ""); } else if (strcmp(key, "date_modified") == 0) { data->feed.item->update_date = parse_date_rfc3339(strlen(value) > 18 ? value : ""); } else if (strcmp(key, "external_url") == 0) { serialize_caret(&data->feed.item->attachments); serialize_array(&data->feed.item->attachments, "url=", 4, value, strlen(value)); } else if (strcmp(key, "tags") == 0) { serialize_caret(&data->feed.item->extras); serialize_array(&data->feed.item->extras, "category=", 9, value, strlen(value)); } } } } sqlite3_finalize(stmt); sqlite3_close(db); return true; } newsraft/src/parse_xml/000077500000000000000000000000001516312403600154715ustar00rootroot00000000000000newsraft/src/parse_xml/common.c000066400000000000000000000052401516312403600171260ustar00rootroot00000000000000#include #include "parse_xml/parse_xml_feed.h" const char * get_value_of_attribute_key(const XML_Char **attrs, const char *key) { for (size_t i = 0; attrs[i] != NULL; i += 2) { if (strcmp(key, attrs[i]) == 0) { return attrs[i + 1]; } } return NULL; } void serialize_attribute(struct string **dest, const char *key, size_t key_len, const XML_Char **attrs, const char *attr_name) { const char *val = get_value_of_attribute_key(attrs, attr_name); if (val) { serialize_array(dest, key, key_len, val, strlen(val)); } } void generic_item_starter(struct feed_update_state *data, const XML_Char **attrs) { (void)attrs; prepend_item(&data->feed.item); data->in_item = true; } void generic_item_ender(struct feed_update_state *data) { data->in_item = false; } void generic_guid_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_ITEM) { cpyss(&data->feed.item->guid, data->text); } } void generic_title_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_ITEM) { cpyss(&data->feed.item->title, data->text); } else if (data->path[data->depth] == GENERIC_FEED) { cpyss(&data->feed.title, data->text); } } void generic_plain_content_end(struct feed_update_state *data) { if (data->in_item) { serialize_caret(&data->feed.item->content); serialize_array(&data->feed.item->content, "type=", 5, "text/plain", 10); serialize_string(&data->feed.item->content, "text=", 5, data->text); } else { serialize_caret(&data->feed.content); serialize_array(&data->feed.content, "type=", 5, "text/plain", 10); serialize_string(&data->feed.content, "text=", 5, data->text); } } void generic_html_content_end(struct feed_update_state *data) { if (data->in_item) { serialize_caret(&data->feed.item->content); serialize_array(&data->feed.item->content, "type=", 5, "text/html", 9); serialize_string(&data->feed.item->content, "text=", 5, data->text); } else { serialize_caret(&data->feed.content); serialize_array(&data->feed.content, "type=", 5, "text/html", 9); serialize_string(&data->feed.content, "text=", 5, data->text); } } void generic_category_end(struct feed_update_state *data) { if (data->in_item) { serialize_caret(&data->feed.item->extras); serialize_string(&data->feed.item->extras, "category=", 9, data->text); } else { serialize_caret(&data->feed.extras); serialize_string(&data->feed.extras, "category=", 9, data->text); } } void update_date_end(struct feed_update_state *data) { if (data->in_item) { data->feed.item->update_date = parse_date(data->text->ptr, true); } } void log_xml_element_content_end(struct feed_update_state *data) { INFO("Element content: %s", data->text->ptr); } newsraft/src/parse_xml/format-atom.c000066400000000000000000000131241516312403600200640ustar00rootroot00000000000000#include #include "parse_xml/parse_xml_feed.h" // https://web.archive.org/web/20211118181732/https://validator.w3.org/feed/docs/atom.html // https://web.archive.org/web/20211201194224/https://datatracker.ietf.org/doc/html/rfc4287 static void atom_link_start(struct feed_update_state *data, const XML_Char **attrs) { const char *attr = get_value_of_attribute_key(attrs, "href"); if (attr == NULL) { return; // Ignore empty links. } const size_t attr_len = strlen(attr); if (attr_len == 0) { return; // Ignore empty links. } const char *rel = get_value_of_attribute_key(attrs, "rel"); if (rel != NULL && strcmp(rel, "self") == 0) { return; // Ignore links to feed itself. } // Default value of rel is alternate! if (rel == NULL || strcmp(rel, "alternate") == 0) { if (data->path[data->depth] == GENERIC_ITEM) { cpyas(&data->feed.item->link, attr, attr_len); } else if (data->path[data->depth] == GENERIC_FEED) { cpyas(&data->feed.link, attr, attr_len); } } else if (data->path[data->depth] == GENERIC_ITEM) { serialize_caret(&data->feed.item->attachments); serialize_array(&data->feed.item->attachments, "url=", 4, attr, attr_len); serialize_attribute(&data->feed.item->attachments, "type=", 5, attrs, "type"); serialize_attribute(&data->feed.item->attachments, "size=", 5, attrs, "length"); if (STRING_IS_EMPTY(data->feed.item->link)) { // Fallback to attachment URL if there's still no item link. cpyas(&data->feed.item->link, attr, attr_len); } } } static void atom_content_start(struct feed_update_state *data, const XML_Char **attrs) { if (data->path[data->depth] == GENERIC_ITEM) { serialize_caret(&data->feed.item->content); serialize_attribute(&data->feed.item->content, "type=", 5, attrs, "type"); const char *type = get_value_of_attribute_key(attrs, "type"); if (type != NULL && strstr(type, "xhtml") != NULL) { data->emptying_target = &data->decoy; empty_string(data->text); } } } static void atom_content_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_ITEM) { data->emptying_target = data->text; serialize_string(&data->feed.item->content, "text=", 5, data->text); } } static void published_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_ITEM) { data->feed.item->publication_date = parse_date(data->text->ptr, true); } } static void author_start(struct feed_update_state *data, const XML_Char **attrs) { (void)attrs; if (data->path[data->depth] == GENERIC_ITEM) { serialize_caret(&data->feed.item->persons); serialize_array(&data->feed.item->persons, "type=", 5, "author", 6); } else if (data->path[data->depth] == GENERIC_FEED) { serialize_caret(&data->feed.persons); serialize_array(&data->feed.persons, "type=", 5, "author", 6); } } static void contributor_start(struct feed_update_state *data, const XML_Char **attrs) { (void)attrs; if (data->path[data->depth] == GENERIC_ITEM) { serialize_caret(&data->feed.item->persons); serialize_array(&data->feed.item->persons, "type=", 5, "contributor", 11); } else if (data->path[data->depth] == GENERIC_FEED) { serialize_caret(&data->feed.persons); serialize_array(&data->feed.persons, "type=", 5, "contributor", 11); } } static void name_end(struct feed_update_state *data) { if (data->path[data->depth] == ATOM_AUTHOR) { if (data->in_item) { serialize_string(&data->feed.item->persons, "name=", 5, data->text); } else { serialize_string(&data->feed.persons, "name=", 5, data->text); } } } static void uri_end(struct feed_update_state *data) { if (data->path[data->depth] == ATOM_AUTHOR) { if (data->in_item) { serialize_string(&data->feed.item->persons, "url=", 4, data->text); } else { serialize_string(&data->feed.persons, "url=", 4, data->text); } } } static void email_end(struct feed_update_state *data) { if (data->path[data->depth] == ATOM_AUTHOR) { if (data->in_item) { serialize_string(&data->feed.item->persons, "email=", 6, data->text); } else { serialize_string(&data->feed.persons, "email=", 6, data->text); } } } static void atom_category_start(struct feed_update_state *data, const XML_Char **attrs) { struct string **target; if (data->path[data->depth] == GENERIC_ITEM) { target = &data->feed.item->extras; } else if (data->path[data->depth] == GENERIC_FEED) { target = &data->feed.extras; } else { return; // Ignore misplaced categories. } const char *attr = get_value_of_attribute_key(attrs, "label"); if (attr == NULL) { attr = get_value_of_attribute_key(attrs, "term"); if (attr == NULL) { return; // Ignore empty categories. } } const size_t attr_len = strlen(attr); if (attr_len != 0) { serialize_caret(target); serialize_array(target, "category=", 9, attr, attr_len); } } static void atom_subtitle_start(struct feed_update_state *data, const XML_Char **attrs) { if (data->path[data->depth] == GENERIC_FEED) { serialize_caret(&data->feed.content); serialize_attribute(&data->feed.content, "type=", 5, attrs, "type"); } } static void atom_subtitle_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_FEED) { serialize_string(&data->feed.content, "text=", 5, data->text); } } static void atom_generator_start(struct feed_update_state *data, const XML_Char **attrs) { (void)data; const char *attr = get_value_of_attribute_key(attrs, "uri"); if (attr != NULL) { INFO("Feed generator URI: %s", attr); } attr = get_value_of_attribute_key(attrs, "url"); if (attr != NULL) { INFO("Feed generator URL: %s", attr); } attr = get_value_of_attribute_key(attrs, "version"); if (attr != NULL) { INFO("Feed generator version: %s", attr); } } newsraft/src/parse_xml/format-dublincore.c000066400000000000000000000023751516312403600212600ustar00rootroot00000000000000#include "parse_xml/parse_xml_feed.h" static void dublincore_title_end(struct feed_update_state *data) { if (data->in_item) { if (STRING_IS_EMPTY(data->feed.item->title)) { cpyss(&data->feed.item->title, data->text); } } else { if (STRING_IS_EMPTY(data->feed.title)) { cpyss(&data->feed.title, data->text); } } } static void dublincore_creator_end(struct feed_update_state *data) { if (data->in_item) { serialize_caret(&data->feed.item->persons); serialize_array(&data->feed.item->persons, "type=", 5, "author", 6); serialize_string(&data->feed.item->persons, "name=", 5, data->text); } else { serialize_caret(&data->feed.persons); serialize_array(&data->feed.persons, "type=", 5, "author", 6); serialize_string(&data->feed.persons, "name=", 5, data->text); } } static void dublincore_contributor_end(struct feed_update_state *data) { if (data->in_item) { serialize_caret(&data->feed.item->persons); serialize_array(&data->feed.item->persons, "type=", 5, "contributor", 11); serialize_string(&data->feed.item->persons, "name=", 5, data->text); } else { serialize_caret(&data->feed.persons); serialize_array(&data->feed.persons, "type=", 5, "contributor", 11); serialize_string(&data->feed.persons, "name=", 5, data->text); } } newsraft/src/parse_xml/format-georss.c000066400000000000000000000005551516312403600204320ustar00rootroot00000000000000#include "parse_xml/parse_xml_feed.h" // https://georss.org // https://georss.org/gml.html // https://en.wikipedia.org/wiki/Geography_Markup_Language static void georss_point_end(struct feed_update_state *data) { if (data->in_item) { serialize_caret(&data->feed.item->extras); serialize_string(&data->feed.item->extras, "coordinates=", 12, data->text); } } newsraft/src/parse_xml/format-mediarss.c000066400000000000000000000070151516312403600207350ustar00rootroot00000000000000#include #include "parse_xml/parse_xml_feed.h" // References: // https://www.rssboard.org/media-rss // // Notes to the future. // // There is no need to parse thumbnail elements, because storing information // about decorating pictures defeats the whole purpose of project being a // console application with as few distractions as possible. // // Purpose of media:group element is to group content elements that are // effectively the same content, but from the user's perspective, all // attachments are just links with some metadata. Therefore, such grouping does // not make sense and is ignored by the parser. Also, this grouping is difficult // to reflect in the database and requires various sophistications. static void mediarss_content_start(struct feed_update_state *data, const XML_Char **attrs) { const char *attr = get_value_of_attribute_key(attrs, "url"); if (attr == NULL) { return; // Ignore empty content entries. } const size_t attr_len = strlen(attr); if (attr_len == 0) { return; // Ignore empty content entries. } struct string **dest = data->in_item ? &data->feed.item->attachments : &data->feed.attachments; serialize_caret(dest); serialize_array(dest, "url=", 4, attr, attr_len); serialize_attribute(dest, "type=", 5, attrs, "type"); serialize_attribute(dest, "size=", 5, attrs, "fileSize"); serialize_attribute(dest, "duration=", 9, attrs, "duration"); serialize_attribute(dest, "width=", 6, attrs, "width"); serialize_attribute(dest, "height=", 7, attrs, "height"); if (STRING_IS_EMPTY(data->feed.item->link)) { // Fallback to attachment URL if there's still no item link. cpyas(&data->feed.item->link, attr, attr_len); } } static void embed_or_player_start(struct feed_update_state *data, const XML_Char **attrs) { const char *attr = get_value_of_attribute_key(attrs, "url"); if (attr == NULL) { return; // Ignore empty entries. } const size_t attr_len = strlen(attr); if (attr_len == 0) { return; // Ignore empty entries. } struct string **dest = data->in_item ? &data->feed.item->attachments : &data->feed.attachments; serialize_caret(dest); serialize_array(dest, "url=", 4, attr, attr_len); if (STRING_IS_EMPTY(data->feed.item->link)) { // Fallback to attachment URL if there's still no item link. cpyas(&data->feed.item->link, attr, attr_len); } } static void peerlink_start(struct feed_update_state *data, const XML_Char **attrs) { const char *attr = get_value_of_attribute_key(attrs, "href"); if (attr == NULL) { return; // Ignore empty entries. } const size_t attr_len = strlen(attr); if (attr_len == 0) { return; // Ignore empty entries. } struct string **dest = data->in_item ? &data->feed.item->attachments : &data->feed.attachments; serialize_caret(dest); serialize_array(dest, "url=", 4, attr, attr_len); serialize_attribute(dest, "type=", 5, attrs, "type"); } static void description_start(struct feed_update_state *data, const XML_Char **attrs) { if (data->in_item) { if (data->path[data->depth] == MEDIARSS_CONTENT) { serialize_attribute(&data->feed.item->attachments, "description_type=", 17, attrs, "type"); } else { serialize_caret(&data->feed.item->content); serialize_attribute(&data->feed.item->content, "type=", 5, attrs, "type"); } } } static void description_end(struct feed_update_state *data) { if (data->in_item) { if (data->path[data->depth] == MEDIARSS_CONTENT) { serialize_string(&data->feed.item->attachments, "description_text=", 17, data->text); } else { serialize_string(&data->feed.item->content, "text=", 5, data->text); } } } newsraft/src/parse_xml/format-rss.c000066400000000000000000000120011516312403600177240ustar00rootroot00000000000000#include #include "parse_xml/parse_xml_feed.h" // Note to the future. // // Since all versions of RSS specifications are basically compatible // with each other, we don't even have to check for a value in the // version attribute of the rss element. // // https://web.archive.org/web/20240202013527/http://backend.userland.com/rssChangeNotes // https://web.archive.org/web/20230124122614/http://static.userland.com/gems/backend/gratefulDead.xml // https://web.archive.org/web/20211011074123/https://www.rssboard.org/rss-0-9-0 // https://web.archive.org/web/20231208183902/https://www.rssboard.org/rss-0-9-1-netscape // https://web.archive.org/web/20231216194351/https://www.rssboard.org/rss-0-9-2 // https://web.archive.org/web/20240202013313/http://backend.userland.com/rss093 // https://web.archive.org/web/20060831192123/http://web.resource.org/rss/1.0/spec // https://web.archive.org/web/20210411040907/http://inamidst.com/rss1.1/ // https://web.archive.org/web/20211208135333/https://validator.w3.org/feed/docs/rss2.html // https://web.archive.org/web/20240201121905/https://www.rssboard.org/rss-specification static void rss_guid_start(struct feed_update_state *data, const XML_Char **attrs) { if (data->path[data->depth] == GENERIC_ITEM) { const char *val = get_value_of_attribute_key(attrs, "isPermaLink"); // Default value of isPermaLink is considered true! data->feed.item->guid_is_link = val == NULL || strcmp(val, "true") == 0; } } static void rss_link_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_ITEM) { cpyss(&data->feed.item->link, data->text); } else if (data->path[data->depth] == GENERIC_FEED) { cpyss(&data->feed.link, data->text); } } static void rss_pubdate_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_ITEM) { data->feed.item->publication_date = parse_date(data->text->ptr, false); if (data->feed.item->publication_date < 0) { data->feed.item->publication_date = 0; } } } static void rss_author_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_ITEM) { serialize_caret(&data->feed.item->persons); serialize_array(&data->feed.item->persons, "type=", 5, "author", 6); serialize_string(&data->feed.item->persons, "email=", 6, data->text); } else if (data->path[data->depth] == GENERIC_FEED) { serialize_caret(&data->feed.persons); serialize_array(&data->feed.persons, "type=", 5, "author", 6); serialize_string(&data->feed.persons, "email=", 6, data->text); } } static void rss_enclosure_start(struct feed_update_state *data, const XML_Char **attrs) { if (data->path[data->depth] != GENERIC_ITEM) { return; } const char *attr = get_value_of_attribute_key(attrs, "url"); if (attr == NULL) { return; } const size_t attr_len = strlen(attr); if (attr_len == 0) { return; } serialize_caret(&data->feed.item->attachments); serialize_array(&data->feed.item->attachments, "url=", 4, attr, attr_len); serialize_attribute(&data->feed.item->attachments, "type=", 5, attrs, "type"); serialize_attribute(&data->feed.item->attachments, "size=", 5, attrs, "length"); if (STRING_IS_EMPTY(data->feed.item->link)) { // Fallback to attachment URL if there's still no item link. cpyas(&data->feed.item->link, attr, attr_len); } } static void rss_comments_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_ITEM) { serialize_caret(&data->feed.item->attachments); serialize_string(&data->feed.item->attachments, "url=", 4, data->text); serialize_array(&data->feed.item->attachments, "content=", 8, "comments", 8); } } static void rss_ttl_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_FEED) { int64_t minutes = -1; if (sscanf(data->text->ptr, "%" SCNd64, &minutes) != 1) { WARN("Couldn't convert value of element to a number!"); return; // Continue parsing like nothing happened. } if (minutes < 0) { WARN("Value of element is negative!"); return; // Continue parsing like nothing happened. } data->feed.time_to_live = minutes * 60; } } static void rss_managingeditor_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_FEED) { serialize_caret(&data->feed.persons); serialize_array(&data->feed.persons, "type=", 5, "editor", 6); serialize_string(&data->feed.persons, "email=", 6, data->text); } } static void rss_source_start(struct feed_update_state *data, const XML_Char **attrs) { if (data->path[data->depth] != GENERIC_ITEM) { return; } const char *attr = get_value_of_attribute_key(attrs, "url"); if (attr == NULL) { return; } const size_t attr_len = strlen(attr); if (attr_len == 0) { return; } serialize_caret(&data->feed.item->attachments); serialize_array(&data->feed.item->attachments, "url=", 4, attr, attr_len); serialize_array(&data->feed.item->attachments, "content=", 8, "source", 6); } static void rss_source_end(struct feed_update_state *data) { if (data->path[data->depth] == GENERIC_ITEM) { serialize_string(&data->feed.item->attachments, "title=", 6, data->text); } } newsraft/src/parse_xml/gperf-data.c000066400000000000000000000273231516312403600176560ustar00rootroot00000000000000/* ANSI-C code produced by gperf version 3.1 */ /* Command-line: gperf -m 1000 -I -t -F ,0,NULL,NULL */ /* Computed positions: -k'1,8,$' */ #if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \ && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \ && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \ && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \ && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \ && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \ && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \ && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \ && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \ && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \ && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \ && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \ && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \ && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \ && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \ && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \ && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \ && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \ && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \ && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \ && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \ && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \ && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126)) /* The character set is not based on ISO-646. */ #error "gperf generated tables don't work with this execution character set. Please report a bug to ." #endif struct xml_element_handler; #include #define TOTAL_KEYWORDS 57 #define MIN_WORD_LENGTH 3 #define MAX_WORD_LENGTH 48 #define MIN_HASH_VALUE 3 #define MAX_HASH_VALUE 91 /* maximum key range = 89, duplicates = 0 */ #ifdef __GNUC__ __inline #else #ifdef __cplusplus inline #endif #endif static unsigned int hash (register const char *str, register size_t len) { static unsigned char asso_values[] = { 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 0, 92, 92, 15, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 0, 92, 17, 17, 6, 92, 0, 0, 0, 92, 37, 0, 0, 28, 0, 0, 92, 11, 16, 0, 92, 92, 0, 92, 25, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92 }; register unsigned int hval = len; switch (hval) { default: hval += asso_values[(unsigned char)str[7]]; /*FALLTHROUGH*/ case 7: case 6: case 5: case 4: case 3: case 2: case 1: hval += asso_values[(unsigned char)str[0]]; break; } return hval + asso_values[(unsigned char)str[len - 1]]; } struct xml_element_handler * in_word_set (register const char *str, register size_t len) { static struct xml_element_handler wordlist[] = { {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"ttl", XML_UNKNOWN_POS, NULL, &rss_ttl_end}, {"item", GENERIC_ITEM, &generic_item_starter, &generic_item_ender}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"Channel", GENERIC_FEED, NULL, NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"title", XML_UNKNOWN_POS, NULL, &generic_title_end}, {"",0,NULL,NULL}, {"pubDate", XML_UNKNOWN_POS, NULL, &rss_pubdate_end}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"author", XML_UNKNOWN_POS, NULL, &rss_author_end}, {"",0,NULL,NULL}, {"lastBuildDate", XML_UNKNOWN_POS, NULL, &log_xml_element_content_end}, {"generator", XML_UNKNOWN_POS, NULL, &log_xml_element_content_end}, {"guid", XML_UNKNOWN_POS, &rss_guid_start, &generic_guid_end}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"channel", GENERIC_FEED, NULL, NULL}, {"managingEditor", XML_UNKNOWN_POS, NULL, &rss_managingeditor_end}, {"webMaster", XML_UNKNOWN_POS, NULL, &log_xml_element_content_end}, {"http://www.rbc.ru full-text", XML_UNKNOWN_POS, NULL, &generic_plain_content_end}, {"source", XML_UNKNOWN_POS, &rss_source_start, &rss_source_end}, {"http://purl.org/rss/1.0/ item", GENERIC_ITEM, &generic_item_starter, &generic_item_ender}, {"http://turbo.yandex.ru content", XML_UNKNOWN_POS, NULL, &generic_html_content_end}, {"http://www.w3.org/2005/Atom uri", XML_UNKNOWN_POS, NULL, &uri_end}, {"enclosure", XML_UNKNOWN_POS, &rss_enclosure_start, NULL}, {"http://www.w3.org/2005/Atom email", XML_UNKNOWN_POS, NULL, &email_end}, {"http://www.georss.org/georss point", XML_UNKNOWN_POS, NULL, &georss_point_end}, {"http://www.w3.org/2005/Atom content", XML_UNKNOWN_POS, &atom_content_start, &atom_content_end}, {"http://purl.org/rss/1.0/ title", XML_UNKNOWN_POS, NULL, &generic_title_end}, {"",0,NULL,NULL}, {"http://www.w3.org/2005/Atom name", XML_UNKNOWN_POS, NULL, &name_end}, {"http://www.w3.org/2005/Atom title", XML_UNKNOWN_POS, NULL, &generic_title_end}, {"http://purl.org/dc/elements/1.1/ subject", XML_UNKNOWN_POS, NULL, &generic_category_end}, {"link", XML_UNKNOWN_POS, NULL, &rss_link_end}, {"http://www.w3.org/2005/Atom subtitle", XML_UNKNOWN_POS, &atom_subtitle_start, &atom_subtitle_end}, {"http://purl.org/dc/elements/1.1/ date", XML_UNKNOWN_POS, NULL, &update_date_end}, {"http://purl.org/dc/elements/1.1/ title", XML_UNKNOWN_POS, NULL, &dublincore_title_end}, {"http://www.w3.org/2005/Atom author", ATOM_AUTHOR, &author_start, NULL}, {"http://www.opengis.net/gml pos", XML_UNKNOWN_POS, NULL, &georss_point_end}, {"http://www.w3.org/2005/Atom id", XML_UNKNOWN_POS, NULL, &generic_guid_end}, {"http://www.w3.org/2005/Atom generator", XML_UNKNOWN_POS, &atom_generator_start, &log_xml_element_content_end}, {"http://www.w3.org/2005/Atom feed", GENERIC_FEED, NULL, NULL}, {"http://www.w3.org/2005/Atom contributor", ATOM_AUTHOR, &contributor_start, NULL}, {"http://purl.org/dc/elements/1.1/ creator", XML_UNKNOWN_POS, NULL, &dublincore_creator_end}, {"http://www.w3.org/2005/Atom updated", XML_UNKNOWN_POS, NULL, &update_date_end}, {"http://search.yahoo.com/mrss/ content", MEDIARSS_CONTENT, &mediarss_content_start, NULL}, {"http://www.w3.org/2005/Atom published", XML_UNKNOWN_POS, NULL, &published_end}, {"http://purl.org/dc/elements/1.1/ contributor", XML_UNKNOWN_POS, NULL, &dublincore_contributor_end}, {"description", XML_UNKNOWN_POS, NULL, &generic_html_content_end}, {"comments", XML_UNKNOWN_POS, NULL, &rss_comments_end}, {"http://www.w3.org/2005/Atom entry", GENERIC_ITEM, &generic_item_starter, &generic_item_ender}, {"http://news.yandex.ru full-text", XML_UNKNOWN_POS, NULL, &generic_html_content_end}, {"http://www.w3.org/2005/Atom summary", XML_UNKNOWN_POS, &atom_content_start, &atom_content_end}, {"http://www.w3.org/2005/Atom category", XML_UNKNOWN_POS, &atom_category_start, NULL}, {"http://www.w3.org/1999/02/22-rdf-syntax-ns# RDF", GENERIC_FEED, NULL, NULL}, {"http://search.yahoo.com/mrss/ player", XML_UNKNOWN_POS, &embed_or_player_start, NULL}, {"http://purl.org/rss/1.0/ description", XML_UNKNOWN_POS, NULL, &generic_html_content_end}, {"http://purl.org/rss/1.0/modules/content/ encoded", XML_UNKNOWN_POS, NULL, &generic_html_content_end}, {"http://purl.org/rss/1.0/ link", XML_UNKNOWN_POS, NULL, &rss_link_end}, {"",0,NULL,NULL}, {"http://search.yahoo.com/mrss/ embed", XML_UNKNOWN_POS, &embed_or_player_start, NULL}, {"http://www.w3.org/2005/Atom link", XML_UNKNOWN_POS, &atom_link_start, NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"http://purl.org/dc/elements/1.1/ description", XML_UNKNOWN_POS, NULL, &generic_plain_content_end}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"category", XML_UNKNOWN_POS, NULL, &generic_category_end}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"http://search.yahoo.com/mrss/ description", XML_UNKNOWN_POS, &description_start, &description_end}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"",0,NULL,NULL}, {"http://search.yahoo.com/mrss/ peerLink", XML_UNKNOWN_POS, &peerlink_start, NULL} }; if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH) { register unsigned int key = hash (str, len); if (key <= MAX_HASH_VALUE) { register const char *s = wordlist[key].name; if (*str == *s && !strcmp (str + 1, s + 1)) return &wordlist[key]; } } return 0; } newsraft/src/parse_xml/gperf-data.in000066400000000000000000000142171516312403600200400ustar00rootroot00000000000000struct xml_element_handler; %% channel, GENERIC_FEED, NULL, NULL Channel, GENERIC_FEED, NULL, NULL item, GENERIC_ITEM, &generic_item_starter, &generic_item_ender guid, XML_UNKNOWN_POS, &rss_guid_start, &generic_guid_end title, XML_UNKNOWN_POS, NULL, &generic_title_end link, XML_UNKNOWN_POS, NULL, &rss_link_end description, XML_UNKNOWN_POS, NULL, &generic_html_content_end pubDate, XML_UNKNOWN_POS, NULL, &rss_pubdate_end lastBuildDate, XML_UNKNOWN_POS, NULL, &log_xml_element_content_end author, XML_UNKNOWN_POS, NULL, &rss_author_end enclosure, XML_UNKNOWN_POS, &rss_enclosure_start, NULL category, XML_UNKNOWN_POS, NULL, &generic_category_end comments, XML_UNKNOWN_POS, NULL, &rss_comments_end ttl, XML_UNKNOWN_POS, NULL, &rss_ttl_end generator, XML_UNKNOWN_POS, NULL, &log_xml_element_content_end webMaster, XML_UNKNOWN_POS, NULL, &log_xml_element_content_end managingEditor, XML_UNKNOWN_POS, NULL, &rss_managingeditor_end source, XML_UNKNOWN_POS, &rss_source_start, &rss_source_end http://www.w3.org/2005/Atom feed, GENERIC_FEED, NULL, NULL http://www.w3.org/2005/Atom entry, GENERIC_ITEM, &generic_item_starter, &generic_item_ender http://www.w3.org/2005/Atom id, XML_UNKNOWN_POS, NULL, &generic_guid_end http://www.w3.org/2005/Atom title, XML_UNKNOWN_POS, NULL, &generic_title_end http://www.w3.org/2005/Atom link, XML_UNKNOWN_POS, &atom_link_start, NULL http://www.w3.org/2005/Atom summary, XML_UNKNOWN_POS, &atom_content_start, &atom_content_end http://www.w3.org/2005/Atom content, XML_UNKNOWN_POS, &atom_content_start, &atom_content_end http://www.w3.org/2005/Atom published, XML_UNKNOWN_POS, NULL, &published_end http://www.w3.org/2005/Atom updated, XML_UNKNOWN_POS, NULL, &update_date_end http://www.w3.org/2005/Atom author, ATOM_AUTHOR, &author_start, NULL http://www.w3.org/2005/Atom contributor, ATOM_AUTHOR, &contributor_start, NULL http://www.w3.org/2005/Atom name, XML_UNKNOWN_POS, NULL, &name_end http://www.w3.org/2005/Atom uri, XML_UNKNOWN_POS, NULL, &uri_end http://www.w3.org/2005/Atom email, XML_UNKNOWN_POS, NULL, &email_end http://www.w3.org/2005/Atom category, XML_UNKNOWN_POS, &atom_category_start, NULL http://www.w3.org/2005/Atom subtitle, XML_UNKNOWN_POS, &atom_subtitle_start, &atom_subtitle_end http://www.w3.org/2005/Atom generator, XML_UNKNOWN_POS, &atom_generator_start, &log_xml_element_content_end http://www.w3.org/1999/02/22-rdf-syntax-ns# RDF, GENERIC_FEED, NULL, NULL http://purl.org/rss/1.0/ item, GENERIC_ITEM, &generic_item_starter, &generic_item_ender http://purl.org/rss/1.0/ title, XML_UNKNOWN_POS, NULL, &generic_title_end http://purl.org/rss/1.0/ link, XML_UNKNOWN_POS, NULL, &rss_link_end http://purl.org/rss/1.0/ description, XML_UNKNOWN_POS, NULL, &generic_html_content_end http://purl.org/rss/1.0/modules/content/ encoded, XML_UNKNOWN_POS, NULL, &generic_html_content_end http://purl.org/dc/elements/1.1/ title, XML_UNKNOWN_POS, NULL, &dublincore_title_end http://purl.org/dc/elements/1.1/ description, XML_UNKNOWN_POS, NULL, &generic_plain_content_end http://purl.org/dc/elements/1.1/ date, XML_UNKNOWN_POS, NULL, &update_date_end http://purl.org/dc/elements/1.1/ creator, XML_UNKNOWN_POS, NULL, &dublincore_creator_end http://purl.org/dc/elements/1.1/ contributor, XML_UNKNOWN_POS, NULL, &dublincore_contributor_end http://purl.org/dc/elements/1.1/ subject, XML_UNKNOWN_POS, NULL, &generic_category_end http://search.yahoo.com/mrss/ content, MEDIARSS_CONTENT, &mediarss_content_start, NULL http://search.yahoo.com/mrss/ embed, XML_UNKNOWN_POS, &embed_or_player_start, NULL http://search.yahoo.com/mrss/ player, XML_UNKNOWN_POS, &embed_or_player_start, NULL http://search.yahoo.com/mrss/ peerLink, XML_UNKNOWN_POS, &peerlink_start, NULL http://search.yahoo.com/mrss/ description, XML_UNKNOWN_POS, &description_start, &description_end http://www.georss.org/georss point, XML_UNKNOWN_POS, NULL, &georss_point_end http://www.opengis.net/gml pos, XML_UNKNOWN_POS, NULL, &georss_point_end http://news.yandex.ru full-text, XML_UNKNOWN_POS, NULL, &generic_html_content_end http://turbo.yandex.ru content, XML_UNKNOWN_POS, NULL, &generic_html_content_end http://www.rbc.ru full-text, XML_UNKNOWN_POS, NULL, &generic_plain_content_end newsraft/src/parse_xml/parse_xml_feed.h000066400000000000000000000006771516312403600206310ustar00rootroot00000000000000#ifndef PARSE_XML_FEED_H #define PARSE_XML_FEED_H #include "newsraft.h" // Unknown position must have 0 value! enum xml_position { XML_UNKNOWN_POS = 0, GENERIC_FEED, GENERIC_ITEM, ATOM_AUTHOR, MEDIARSS_CONTENT, }; struct xml_element_handler { const char *name; uint8_t bitpos; void (*start_handle)(struct feed_update_state *data, const XML_Char **atts); void (*end_handle)(struct feed_update_state *data); }; #endif // PARSE_XML_FEED_H newsraft/src/parse_xml/setup-opml-parser.c000066400000000000000000000140761516312403600212440ustar00rootroot00000000000000#include #include "newsraft.h" struct opml_parser_context { XML_Parser parser; struct string *current_section_name; int outline_depth; }; static void opml_start_element_handler(void *userData, const XML_Char *name, const XML_Char **atts) { if (strcmp(name, "outline") != 0) return; struct opml_parser_context *ctx = userData; struct feed_entry feed = {0}; const char *title = get_value_of_attribute_key(atts, "title"); if (title == NULL) { title = get_value_of_attribute_key(atts, "text"); } const char *xml_url = get_value_of_attribute_key(atts, "xmlUrl"); if (xml_url && strlen(xml_url) > 0) { // Treat elements with a xmlUrl as feeds. feed.url = crtas(xml_url, strlen(xml_url)); if (title && strlen(title) > 0) { feed.name = crtas(title, strlen(title)); } int64_t section_index = 0; if (ctx->current_section_name) { section_index = make_sure_section_exists(ctx->current_section_name); } INFO("Copying '%s' into section #%lld for import", feed.url->ptr, section_index); if (copy_feed_to_section(&feed, section_index) == NULL) { FAIL("Error copying OPML element '%s' to section", feed.url->ptr); XML_StopParser(ctx->parser, XML_TRUE); goto cleanup; } } else if (ctx->current_section_name == NULL && title != NULL && strlen(title) > 0) { // Treat top level elements where xmlUrl is missing as a section category. INFO("Set current section for import '%s'", title); ctx->current_section_name = crtas(title, strlen(title)); } else { // Ignore elements without a xmlUrl when we already have a section set. INFO("Ignoring outline element for import '%s'", title); } ctx->outline_depth++; cleanup: free_string(feed.url); free_string(feed.name); } static void opml_end_element_handler(void *userData, const XML_Char *name) { if (strcmp(name, "outline") != 0) return; struct opml_parser_context *ctx = userData; if (ctx->outline_depth == 1 && ctx->current_section_name != NULL) { // Unset the current section when an end element at depth 1 is found. INFO("Unset current section for import '%s'", ctx->current_section_name->ptr); free_string(ctx->current_section_name); ctx->current_section_name = NULL; } ctx->outline_depth--; } static bool has_feed_in_section(struct feed_entry **feeds, size_t feeds_count, int64_t section_index) { bool has_feed = false; for (size_t i = 0; i < feeds_count; ++i) { if (feeds[i]->section_index == section_index) { has_feed = true; break; } } return has_feed; } static bool write_feeds_file(void) { size_t feeds_count = 0; struct feed_entry **feeds = get_all_feeds(&feeds_count); struct string *output = crtes(20000); if (feeds_count == 0 || feeds == NULL || output == NULL) { free_string(output); return false; } bool has_global_feed = has_feed_in_section(feeds, feeds_count, 0); for (int64_t section_index = 0; true; ++section_index) { char *section_name = get_section_name(section_index); if (!section_name) { break; } if (section_index > 0) { bool need_newline = (section_index == 1 && has_global_feed) || section_index > 1; str_appendf(output, "%s@ %s\n", need_newline ? "\n" : "", section_name); } for (size_t i = 0; i < feeds_count; ++i) { if (feeds[i]->section_index != section_index) { continue; } if (STRING_IS_EMPTY(feeds[i]->name)) { str_appendf(output, "%s\n", feeds[i]->url->ptr); } else { str_appendf( output, "%s \"%s\"\n", feeds[i]->url->ptr, feeds[i]->name->ptr ); } } } fputs(output->ptr, stdout); fflush(stdout); free_string(output); return true; } bool convert_opml_to_feeds(void) { bool ok = false; struct string *opml = crtes(10000); struct opml_parser_context ctx = { .parser = XML_ParserCreateNS(NULL, ' '), }; FILE *f = fopen("/dev/stdin", "r"); if (!f || !ctx.parser || !opml) { goto cleanup; } for (int c = fgetc(f); c != EOF; c = fgetc(f)) { catcs(opml, c); } if (opml->len == 0) { goto cleanup; } const struct string* section_name = get_cfg_string(NULL, CFG_GLOBAL_SECTION_NAME); if (make_sure_section_exists(section_name) < 0) { goto cleanup; } XML_SetUserData(ctx.parser, &ctx); XML_SetElementHandler(ctx.parser, &opml_start_element_handler, &opml_end_element_handler); if (XML_Parse(ctx.parser, opml->ptr, opml->len, XML_TRUE) != XML_STATUS_OK) { write_error("XML parser failed: %s\n", XML_ErrorString(XML_GetErrorCode(ctx.parser))); goto cleanup; } ok = write_feeds_file(); cleanup: XML_ParserFree(ctx.parser); free_string(opml); if (f) fclose(f); return ok; } bool convert_feeds_to_opml(void) { size_t feeds_count = 0; struct feed_entry **feeds = get_all_feeds(&feeds_count); if (feeds_count == 0 || feeds == NULL) { return false; } struct string *opml = crtes(50000); const char *header = "\n" "\n" "\t\n\t\tNewsraft - Exported Feeds\n\t\n" "\t\n"; const char *footer = "\t\n\n"; catas(opml, header, strlen(header)); char *indent; int64_t section_index = 0; char *section_name = NULL; while ((section_name = get_section_name(section_index)) != NULL) { if (section_index != 0) { str_appendf(opml, "\t\t\n", section_name); } for (size_t i = 0; i < feeds_count; ++i) { if (feeds[i]->section_index != section_index) { continue; // skip feeds from other sections } if (feeds[i]->url->ptr[0] == '$') { continue; // skip command feeds } indent = section_index == 0 ? "\t\t" : "\t\t\t"; if (STRING_IS_EMPTY(feeds[i]->name)) { str_appendf(opml, "%s\n", indent, feeds[i]->url->ptr); } else { str_appendf( opml, "%s\n", indent, feeds[i]->url->ptr, feeds[i]->name->ptr, feeds[i]->name->ptr ); } } if (section_index != 0) { str_appendf(opml, "\t\t\n", section_name); } section_index++; } catas(opml, footer, strlen(footer)); fputs(opml->ptr, stdout); fflush(stdout); free_string(opml); return true; } newsraft/src/parse_xml/setup-xml-parser.c000066400000000000000000000043371516312403600210740ustar00rootroot00000000000000#include #include "parse_xml/parse_xml_feed.h" #define XML_NAMESPACE_SEPARATOR ' ' static void start_element_handler(void *userData, const XML_Char *name, const XML_Char **atts) { struct feed_update_state *data = userData; empty_string(data->emptying_target); data->depth += 1; data->path[data->depth] = XML_UNKNOWN_POS; struct xml_element_handler *handler = in_word_set(name, strlen(name)); if (handler != NULL) { INFO("Known start: %s", name); data->path[data->depth] = handler->bitpos; if (handler->start_handle != NULL) { data->depth -= 1; handler->start_handle(data, atts); data->depth += 1; } } else if (strncmp(name, "http://www.w3.org/1999/xhtml", 28) == 0) { XML_DefaultCurrent(data->xml_parser); } else { WARN("Unknown start: %s", name); } } static void end_element_handler(void *userData, const XML_Char *name) { struct feed_update_state *data = userData; trim_whitespace_from_string(data->text); if (data->depth > 0) { data->depth -= 1; } struct xml_element_handler *handler = in_word_set(name, strlen(name)); if (handler != NULL) { INFO("Known end: %s", name); if (handler->end_handle != NULL) { handler->end_handle(data); } } else if (strncmp(name, "http://www.w3.org/1999/xhtml", 28) == 0) { XML_DefaultCurrent(data->xml_parser); } else { WARN("Unknown end: %s", name); } } static void character_data_handler(void *userData, const XML_Char *s, int len) { catas(((struct feed_update_state *)userData)->text, s, len); } static void xml_default_handler(void *userData, const XML_Char *s, int len) { struct feed_update_state *data = userData; if (data->emptying_target == &data->decoy) { catas(data->text, s, len); } } bool setup_xml_parser(struct feed_update_state *data) { data->xml_parser = XML_ParserCreateNS(NULL, XML_NAMESPACE_SEPARATOR); if (data->xml_parser == NULL) { return false; } static char ptr_for_decoy[1]; data->decoy.ptr = ptr_for_decoy; data->emptying_target = data->text; XML_SetUserData(data->xml_parser, data); XML_SetElementHandler(data->xml_parser, &start_element_handler, &end_element_handler); XML_SetCharacterDataHandler(data->xml_parser, &character_data_handler); XML_SetDefaultHandler(data->xml_parser, &xml_default_handler); return true; } newsraft/src/path.c000066400000000000000000000135541516312403600146070ustar00rootroot00000000000000#include #include // opendir #include #include // PATH_MAX #include // mkdir #include "newsraft.h" // Note to the future. // Do not read Newsraft-specific file pathes from environment variables (like // NEWSRAFT_CONFIG_DIR), because environment is intended for settings that are // valueable to many programs, not just a single one. #ifndef PATH_MAX #define PATH_MAX 4096 #endif static char feeds_file_path[PATH_MAX] = ""; static char config_file_path[PATH_MAX] = ""; static char db_file_path[PATH_MAX] = ""; static inline bool set_file_path(char *dest, const char *name, const char *src) { if (strlen(src) >= PATH_MAX) { write_error("Path to the %s file is too long!\n", name); return false; } strcpy(dest, src); return true; } bool set_feeds_path(const char *path) { return set_file_path(feeds_file_path, "feeds", path); } bool set_config_path(const char *path) { return set_file_path(config_file_path, "config", path); } bool set_db_path(const char *path) { return set_file_path(db_file_path, "database", path); } const char * get_feeds_path(void) { if (strlen(feeds_file_path) > 0) { return feeds_file_path; } FILE *f; char *env_var = getenv("XDG_CONFIG_HOME"); // Order in which to look up a feeds file: // 1. $XDG_CONFIG_HOME/newsraft/feeds // 2. $HOME/.config/newsraft/feeds // 3. $HOME/.newsraft/feeds if (env_var != NULL && strlen(env_var) > 0) { snprintf(feeds_file_path, PATH_MAX, "%s/newsraft/feeds", env_var); f = fopen(feeds_file_path, "r"); if (f != NULL) { fclose(f); return feeds_file_path; // 1 } } env_var = getenv("HOME"); if (env_var != NULL && strlen(env_var) > 0) { snprintf(feeds_file_path, PATH_MAX, "%s/.config/newsraft/feeds", env_var); f = fopen(feeds_file_path, "r"); if (f != NULL) { fclose(f); return feeds_file_path; // 2 } snprintf(feeds_file_path, PATH_MAX, "%s/.newsraft/feeds", env_var); f = fopen(feeds_file_path, "r"); if (f != NULL) { fclose(f); return feeds_file_path; // 3 } } write_error("Can't find feeds file! Newsraft requires it to function.\n"); write_error("A detailed description of the format of this file is provided in newsraft(1) man page.\n"); return NULL; } const char * get_config_path(void) { if (strlen(config_file_path) > 0) { return config_file_path; } FILE *f; char *env_var = getenv("XDG_CONFIG_HOME"); // Order in which to look up a config file: // 1. $XDG_CONFIG_HOME/newsraft/config // 2. $HOME/.config/newsraft/config // 3. $HOME/.newsraft/config if (env_var != NULL && strlen(env_var) > 0) { snprintf(config_file_path, PATH_MAX, "%s/newsraft/config", env_var); f = fopen(config_file_path, "r"); if (f != NULL) { fclose(f); return config_file_path; // 1 } } env_var = getenv("HOME"); if (env_var != NULL && strlen(env_var) > 0) { snprintf(config_file_path, PATH_MAX, "%s/.config/newsraft/config", env_var); f = fopen(config_file_path, "r"); if (f != NULL) { fclose(f); return config_file_path; // 2 } snprintf(config_file_path, PATH_MAX, "%s/.newsraft/config", env_var); f = fopen(config_file_path, "r"); if (f != NULL) { fclose(f); return config_file_path; // 3 } } // Do not write error message because config file is optional. return NULL; } const char * get_db_path(void) { if (strlen(db_file_path) > 0) { return db_file_path; } // Order in which to look up a database file: // 1. $XDG_DATA_HOME/newsraft/newsraft.sqlite3 // 2. $HOME/.local/share/newsraft/newsraft.sqlite3 // 3. $HOME/.newsraft/newsraft.sqlite3 FILE *f; char *xdg_data_home_var = getenv("XDG_DATA_HOME"); size_t xdg_data_home_var_len = xdg_data_home_var != NULL ? strlen(xdg_data_home_var) : 0; if (xdg_data_home_var != NULL && xdg_data_home_var_len > 0) { snprintf(db_file_path, PATH_MAX, "%s/newsraft/newsraft.sqlite3", xdg_data_home_var); f = fopen(db_file_path, "r"); if (f != NULL) { fclose(f); return db_file_path; // 1 } } char *home_var = getenv("HOME"); size_t home_var_len = home_var != NULL ? strlen(home_var) : 0; if (home_var != NULL && home_var_len > 0) { snprintf(db_file_path, PATH_MAX, "%s/.local/share/newsraft/newsraft.sqlite3", home_var); f = fopen(db_file_path, "r"); if (f != NULL) { fclose(f); return db_file_path; // 2 } snprintf(db_file_path, PATH_MAX, "%s/.newsraft/newsraft.sqlite3", home_var); f = fopen(db_file_path, "r"); if (f != NULL) { fclose(f); return db_file_path; // 3 } } // If we got to this point then database file does not exist. // We have to create a new one! DIR *d; if (xdg_data_home_var != NULL && xdg_data_home_var_len > 0) { snprintf(db_file_path, PATH_MAX, "%s", xdg_data_home_var); mkdir(db_file_path, 0777); snprintf(db_file_path, PATH_MAX, "%s/newsraft", xdg_data_home_var); mkdir(db_file_path, 0777); d = opendir(db_file_path); if (d != NULL) { closedir(d); snprintf(db_file_path, PATH_MAX, "%s/newsraft/newsraft.sqlite3", xdg_data_home_var); return db_file_path; // 1 } else { write_error("Failed to create \"%s\" directory!\n", db_file_path); } } else if (home_var != NULL && home_var_len > 0) { snprintf(db_file_path, PATH_MAX, "%s", home_var); mkdir(db_file_path, 0777); snprintf(db_file_path, PATH_MAX, "%s/.local", home_var); mkdir(db_file_path, 0777); snprintf(db_file_path, PATH_MAX, "%s/.local/share", home_var); mkdir(db_file_path, 0777); snprintf(db_file_path, PATH_MAX, "%s/.local/share/newsraft", home_var); mkdir(db_file_path, 0777); d = opendir(db_file_path); if (d != NULL) { closedir(d); snprintf(db_file_path, PATH_MAX, "%s/.local/share/newsraft/newsraft.sqlite3", home_var); return db_file_path; // 2 } else { write_error("Failed to create \"%s\" directory!\n", db_file_path); } } else { write_error("Neither XDG_DATA_HOME nor HOME environment variables are set!\n"); } write_error("Failed to get database file path!\n"); return NULL; } newsraft/src/queue.c000066400000000000000000000121141516312403600147660ustar00rootroot00000000000000#include "newsraft.h" struct queue_notification_category { const struct wstring *notify_cmd; size_t new_items_count; }; static struct feed_update_state *update_queue = NULL; static pthread_mutex_t update_queue_lock = PTHREAD_MUTEX_INITIALIZER; struct feed_update_state * queue_pull(bool (*condition)(struct feed_update_state *)) { pthread_mutex_lock(&update_queue_lock); for (struct feed_update_state *j = update_queue; j != NULL; j = j->next) { if (condition(j)) { pthread_mutex_unlock(&update_queue_lock); return j; } } pthread_mutex_unlock(&update_queue_lock); return NULL; } static void queue_destroy_unprotected(void) { struct feed_update_state *k = NULL; for (struct feed_update_state *j = update_queue; j != NULL; j = j->next, free(k)) { free_string(j->new_errors); remove_downloader_handle(j); curl_easy_cleanup(j->curl); curl_slist_free_all(j->download_headers); XML_ParserFree(j->xml_parser); free_string(j->feed.title); free_string(j->feed.link); free_string(j->feed.content); free_string(j->feed.attachments); free_string(j->feed.persons); free_string(j->feed.extras); free_string(j->feed.http_header_etag); free_string(j->text); free_item(j->feed.item); k = j; } update_queue = NULL; } void queue_destroy(void) { pthread_mutex_lock(&update_queue_lock); queue_destroy_unprotected(); pthread_mutex_unlock(&update_queue_lock); } static void queue_execute_update_notifications_unprotected(size_t max_units_count) { struct queue_notification_category *units = newsraft_calloc(max_units_count, sizeof(struct queue_notification_category)); for (struct feed_update_state *i = update_queue; i != NULL; i = i->next) { const struct wstring *notify_cmd = get_cfg_wstring(&i->feed_entry->cfg, CFG_NOTIFICATION_COMMAND); if (notify_cmd && notify_cmd->len > 0 && i->new_items_count > 0) { struct queue_notification_category *unit = units; while (unit->notify_cmd != NULL && wcscmp(unit->notify_cmd->ptr, notify_cmd->ptr) != 0) { unit += 1; } unit->notify_cmd = notify_cmd; unit->new_items_count += i->new_items_count; } } for (size_t i = 0; i < max_units_count && units[i].notify_cmd != NULL; ++i) { struct format_arg notification_cmd_args[] = { {L'q', L'd', {.i = units[i].new_items_count}}, {L'\0', L'\0', {.i = 0 /* terminator */}}, }; run_formatted_command(units[i].notify_cmd, notification_cmd_args); } free(units); } void queue_examine(void) { pthread_mutex_lock(&update_queue_lock); size_t update_queue_len = 0; size_t update_queue_failures = 0; size_t update_queue_cancelations = 0; size_t update_queue_finished_len = 0; for (struct feed_update_state *j = update_queue; j != NULL; j = j->next) { update_queue_len += 1; if (j->is_finished) { update_queue_finished_len += 1; } if (j->is_canceled) { update_queue_cancelations += 1; } if (j->is_failed && !get_cfg_bool(&j->feed_entry->cfg, CFG_SUPPRESS_ERRORS)) { update_queue_failures += 1; } } info_status("Feed updates completed: %zu/%zu", update_queue_finished_len, update_queue_len); if (update_queue_finished_len == update_queue_len) { raise_menu_age(); allow_status_cleaning(); if (update_queue_failures > 0) { fail_status("%zu feeds failed (select failed feed and press v for more info)", update_queue_failures); } else if (update_queue_cancelations > 0) { info_status("%zu feeds are already up-to-date", update_queue_cancelations); } else { status_clean(); } queue_execute_update_notifications_unprotected(update_queue_len); db_transaction_commit(); queue_destroy_unprotected(); } else { prevent_status_cleaning(); } pthread_mutex_unlock(&update_queue_lock); } void queue_updates(struct feed_entry **feeds, size_t feeds_count) { time_t current_time = time(NULL); if (current_time <= 0) { FAIL("Failed to get system time!"); return; } pthread_mutex_lock(&update_queue_lock); bool this_is_new_queue = update_queue == NULL ? true : false; for (size_t i = 0; i < feeds_count; ++i) { bool already_present_in_queue = false; for (struct feed_update_state *j = update_queue; j != NULL; j = j->next) { if (feeds[i] == j->feed_entry) { already_present_in_queue = true; break; } } if (already_present_in_queue == true) { continue; } feeds[i]->update_date = current_time; INFO("Feed %s update attempt date: %" PRId64, feeds[i]->url->ptr, feeds[i]->update_date); struct feed_update_state *item = newsraft_calloc(1, sizeof(struct feed_update_state)); item->feed_entry = feeds[i]; item->new_errors = crtes(1); item->text = crtes(50000); item->next = update_queue; update_queue = item; } if (this_is_new_queue) { db_transaction_begin(); } pthread_mutex_unlock(&update_queue_lock); queue_examine(); threads_wake_up(NEWSRAFT_THREAD_DOWNLOAD); threads_wake_up(NEWSRAFT_THREAD_SHRUNNER); } void queue_wait_finish(void) { bool complete = false; struct timespec check_period = {0, 100000000}; // 0.1 seconds do { nanosleep(&check_period, NULL); pthread_mutex_lock(&update_queue_lock); complete = update_queue == NULL ? true : false; pthread_mutex_unlock(&update_queue_lock); } while (complete == false); } newsraft/src/render-block.c000066400000000000000000000033221516312403600162120ustar00rootroot00000000000000#include #include "newsraft.h" bool add_render_block(struct render_blocks_list *blocks, const char *content, size_t content_len, render_block_format content_type, bool needs_trimming) { if (content == NULL || content_len == 0) { return true; // Ignore empty render blocks. } blocks->ptr = newsraft_realloc(blocks->ptr, sizeof(struct render_block) * (blocks->len + 1)); blocks->ptr[blocks->len].content = convert_array_to_wstring(content, content_len); if (blocks->ptr[blocks->len].content == NULL) { return false; } blocks->ptr[blocks->len].content_type = content_type; blocks->ptr[blocks->len].needs_trimming = needs_trimming; blocks->len += 1; return true; } void apply_links_render_blocks(struct render_blocks_list *blocks, const struct wstring *data) { if (data != NULL && data->ptr != NULL && data->len > 0) { for (size_t i = 0; i < blocks->len; ++i) { if (blocks->ptr[i].content_type == TEXT_LINKS) { wstr_set(&blocks->ptr[i].content, data->ptr, data->len, data->len); blocks->ptr[i].content_type = TEXT_HTML; } } } } void free_render_blocks(struct render_blocks_list *blocks) { for (size_t i = 0; i < blocks->len; ++i) { free_wstring(blocks->ptr[i].content); } free(blocks->ptr); if (blocks->links.ptr != NULL) { for (size_t i = 0; i < blocks->links.len; ++i) { free_contents_of_link(&blocks->links.ptr[i]); } free(blocks->links.ptr); blocks->links.ptr = NULL; blocks->links.len = 0; } } void free_render_result(struct render_result *render) { if (render != NULL) { for (size_t i = 0; i < render->lines_len; ++i) { free_wstring(render->lines[i].ws); free(render->lines[i].hints); } free(render->lines); render->lines = NULL; render->lines_len = 0; } } newsraft/src/render_data/000077500000000000000000000000001516312403600157475ustar00rootroot00000000000000newsraft/src/render_data/line.c000066400000000000000000000116651516312403600170530ustar00rootroot00000000000000#include #include "render_data.h" #define RENDER_TAB_DISPLAY L" " bool line_bump(struct line *line) { line->target->lines = newsraft_realloc(line->target->lines, sizeof(struct render_line) * (line->target->lines_len + 1)); line->target->lines_len += 1; line->head = line->target->lines + line->target->lines_len - 1; line->head->ws = wcrtes(line->lim); line->head->hints = NULL; line->head->hints_len = 0; line->head->indent = line->indent; line->end = SIZE_MAX; line->end_is_hyphenated = false; return true; } static bool line_tab(struct line *line) { if (line->target->lines_len < 2) { return line_string(line, RENDER_TAB_DISPLAY); } struct render_line *prev = &line->target->lines[line->target->lines_len - 2]; size_t whitespace_len = 0; for (size_t i = line->head->ws->len; i < prev->ws->len && ISWIDEWHITESPACE(prev->ws->ptr[i]); ++i) { whitespace_len += 1; } if (whitespace_len == 0) { return line_string(line, RENDER_TAB_DISPLAY); } bool status = true; for (size_t i = 0; i < whitespace_len && status == true; ++i) { status = line_char(line, L' '); } return status; } static bool line_soft_hyphen(struct line *line) { if (line->head->ws->len > 0 && line->head->ws->ptr[line->head->ws->len - 1] != ' ' && (size_t)wcswidth(line->head->ws->ptr, line->head->ws->len) < line->lim - line->head->indent) { line->end = line->head->ws->len - 1; line->end_is_hyphenated = true; } return true; } static inline void line_split_at_end(struct line *line) { // Have to use pointers to individual members because calling line_char can // realloc lines array which would make using a pointer to the render_line // element prone to the use-after-free bugs. struct wstring *prev_ws = line->head->ws; newsraft_video_t *prev_hints = line->head->hints; size_t prev_end = line->end; bool prev_end_is_hyphenated = line->end_is_hyphenated; line_bump(line); // Now line->head points to a new empty line size_t i = prev_end + 1; while (i < prev_ws->len && ISWIDEWHITESPACE(prev_ws->ptr[i])) { i += 1; } while (i < prev_ws->len) { line->style = prev_hints[i]; line_char(line, prev_ws->ptr[i++]); } if (prev_end_is_hyphenated){ prev_ws->ptr[prev_end + 1] = L'-'; prev_ws->len = prev_end + 1; prev_end += 1; } prev_ws->ptr[prev_end + 1] = L'\0'; prev_ws->len = prev_end + 1; } // This function writes a character and its style to a render target. // Writing to a render target must be done only through this function! static bool line_append(struct line *line, wchar_t c) { wcatcs(line->head->ws, c); if (line->head->hints_len < line->head->ws->lim) { line->head->hints = newsraft_realloc(line->head->hints, sizeof(newsraft_video_t) * (line->head->ws->lim + 1)); line->head->hints_len = line->head->ws->lim; } line->head->hints[line->head->ws->len - 1] = line->style; return true; } bool line_char(struct line *line, wchar_t c) { if (c == L'\n') { return line_bump(line); // Finish current line and create a new empty one } if (c == L'\t') { return line_tab(line); // Add missing whitespace to align with previous line } if (c == 0xAD){ // 0xAD is a soft hyphen return line_soft_hyphen(line); // Special kind of line end marker with a hyphen } int c_width = wcwidth(c); if (c_width < 1) { return true; // Ignore invalid characters } // Forcefully add characters to line if: // 1. line is infinite // 2. line doesn't fit this character // 3. current indentation is too big if (line->lim == 0 || line->lim <= (size_t)c_width || line->lim <= line->head->indent) { return line_append(line, c); } if (c == L' ' && line->head->ws->len > 0 && line->head->ws->ptr[line->head->ws->len - 1] != ' ') { line->end = line->head->ws->len - 1; line->end_is_hyphenated = false; } if ((size_t)wcswidth(line->head->ws->ptr, line->head->ws->len) + c_width <= line->lim - line->head->indent) { line_append(line, c); } else if (c == L' ') { return true; // Ignore spaces when we are in the end of line } else if (line->end == SIZE_MAX) { line_bump(line); line_char(line, c); } else { line_append(line, c); line_split_at_end(line); } return true; // TODO: check for errors? } bool line_string(struct line *line, const wchar_t *str) { bool status = true; if (str != NULL) { for (const wchar_t *i = str; *i != L'\0' && status == true; ++i) { status = line_char(line, *i); } } return status; } static inline void line_style_update(struct line *line) { line->style = TB_DEFAULT; for (size_t i = 0; i < line->style_stack_len; ++i) { line->style |= line->style_stack[i]; } } void line_style(struct line *line, newsraft_video_t attrs) { line->style_stack = newsraft_realloc(line->style_stack, sizeof(newsraft_video_t) * (line->style_stack_len + 1)); line->style_stack[line->style_stack_len] = attrs; line->style_stack_len += 1; line_style_update(line); } void line_unstyle(struct line *line) { if (line->style_stack_len > 0) { line->style_stack_len -= 1; } line_style_update(line); } newsraft/src/render_data/render-text-html.c000066400000000000000000000713431516312403600213260ustar00rootroot00000000000000#include #include #include "render_data.h" #define MAX_NESTED_LISTS_DEPTH 10 #define SPACES_PER_INDENTATION_LEVEL 4 #define HTML_TABLE_COLUMN_SPACING 2 struct html_table_cell { struct render_result text; struct line line; int8_t colspan; int8_t rowspan; int64_t index; }; struct html_table_row { struct html_table_cell *cells; int64_t cells_count; size_t max_cell_height; }; struct html_table { struct html_table_row *rows; int64_t rows_count; int64_t columns_count; size_t *column_widths; struct wstring *unmapped_text; // text outside of cells }; struct list_level { bool is_ordered; unsigned length; }; struct html_render { struct line *line; uint8_t list_depth; // We are keeping first element of the list_levels for the list items // which were placed without the beginning of the listing (ul, ol). struct list_level list_levels[MAX_NESTED_LISTS_DEPTH + 1]; struct links_list *links; bool in_table; // Nested tables are not supported size_t in_pre_depth; // Shows how many pre elements we are in struct string **abbrs; size_t abbrs_len; struct html_table table; }; struct html_element_renderer { GumboTag tag_id; bool descentable; uint8_t newlines_before; uint8_t newlines_after; wchar_t *prefix; wchar_t *suffix; void (*start_handler)(struct html_render *, GumboVector *); void (*end_handler)(struct html_render *, GumboVector *); newsraft_video_t video_attrs; }; static void provide_newlines(struct html_render *ctx, size_t count, bool force) { if (force == false) { size_t digits_in_the_beginning = 0; if (ctx->line->target->lines_len > 0) { for (size_t i = 0; i < ctx->line->target->lines[ctx->line->target->lines_len - 1].ws->len; ++i) { if (iswdigit(ctx->line->target->lines[ctx->line->target->lines_len - 1].ws->ptr[i])) { digits_in_the_beginning += 1; } else { break; } } } if ((digits_in_the_beginning > 0 && ctx->line->target->lines[ctx->line->target->lines_len - 1].ws->ptr[digits_in_the_beginning] == L'.') || (ctx->line->target->lines_len > 0 && ctx->line->target->lines[ctx->line->target->lines_len - 1].ws->ptr[0] == L'*')) { return; // Ignore beginning of lists } } size_t empty_lines = 0; for (size_t i = ctx->line->target->lines_len; true; --i) { if (i == 0) { return; } else if (ctx->line->target->lines[i - 1].ws->len == 0) { empty_lines += 1; } else { break; } } if (count > empty_lines) { for (int i = count - empty_lines; i > 0; --i) { line_bump(ctx->line); } } } static const char * get_value_of_xml_attribute(GumboVector *attrs, const char *attr_name) { GumboAttribute *attr = gumbo_get_attribute(attrs, attr_name); return attr != NULL && attr->value != NULL ? attr->value : ""; } static void url_mark_handler(struct html_render *ctx, GumboVector *attrs) { const char *links[] = {"href", "src", "data"}; const char *names[] = {"title", "name", "alt"}; const char *types[] = {"type"}; const char *link = NULL; const char *type = NULL; const char *name = NULL; for (size_t l = 0; l < LENGTH(links) && link == NULL; ++l) { const char *a = get_value_of_xml_attribute(attrs, links[l]); if (strlen(a) > 0) link = a; } for (size_t t = 0; t < LENGTH(types) && type == NULL; ++t) { const char *a = get_value_of_xml_attribute(attrs, types[t]); if (strlen(a) > 0) type = a; } for (size_t n = 0; n < LENGTH(names) && name == NULL; ++n) { const char *a = get_value_of_xml_attribute(attrs, names[n]); if (strlen(a) > 0) name = a; } if (link == NULL) return; // Ignore empty links if (link[0] == '#') return; // Ignore anchors to elements size_t link_len = strlen(link); if (link_len == 0) return; // Ignore empty links int64_t link_index = add_url_to_links_list(ctx->links, link, link_len); if (link_index < 0) return; // Ignore invalid links wchar_t index[100]; int index_len = swprintf(index, 100, L"[%" PRId64, link_index + 1); if (index_len < 2 || index_len > 99) return; // Should never happen if (ctx->line->head->ws->len > 0) { line_string(ctx->line, L"\u00A0"); // non-breaking space } line_style(ctx->line, TB_BOLD); line_string(ctx->line, index); if (type != NULL || name != NULL) { line_string(ctx->line, L", "); if (type != NULL) { struct wstring *w = convert_array_to_wstring(type, strlen(type)); if (w != NULL) { line_string(ctx->line, w->ptr); free_wstring(w); } } if (name != NULL) { line_string(ctx->line, type == NULL ? L"\"" : L" \""); struct wstring *w = convert_array_to_wstring(name, strlen(name)); if (w != NULL) { line_string(ctx->line, w->ptr); free_wstring(w); } line_char(ctx->line, L'"'); } } line_char(ctx->line, L']'); line_unstyle(ctx->line); } static void abbr_handler(struct html_render *ctx, GumboVector *attrs) { const char *title = get_value_of_xml_attribute(attrs, "title"); if (title == NULL) { return; } const size_t title_len = strlen(title); if (title_len == 0) { return; } for (size_t i = 0; i < ctx->abbrs_len; ++i) { if (strcasecmp(title, ctx->abbrs[i]->ptr) == 0) { return; // It's a duplicate } } line_string(ctx->line, L"\u00A0"); // non-breaking space line_char(ctx->line, L'('); struct wstring *w = convert_array_to_wstring(title, title_len); if (w != NULL) { line_string(ctx->line, w->ptr); free_wstring(w); } line_char(ctx->line, L')'); ctx->abbrs = newsraft_realloc(ctx->abbrs, sizeof(struct string *) * (ctx->abbrs_len + 1)); ctx->abbrs[ctx->abbrs_len] = crtas(title, title_len); ctx->abbrs_len += 1; } static void br_handler(struct html_render *ctx, GumboVector *attrs) { (void)attrs; line_char(ctx->line, L'\n'); } static void hr_handler(struct html_render *ctx, GumboVector *attrs) { (void)attrs; ctx->line->head->indent = 0; for (size_t i = 0; i < ctx->line->lim; ++i) { line_char(ctx->line, L'─'); } } static void input_handler(struct html_render *ctx, GumboVector *attrs) { if (strcmp(get_value_of_xml_attribute(attrs, "type"), "hidden") != 0) { const char *value = get_value_of_xml_attribute(attrs, "value"); size_t value_len = strlen(value); if (value_len > 0) { struct wstring *w = convert_array_to_wstring(value, value_len); if (w != NULL) { line_char(ctx->line, L'['); line_string(ctx->line, w->ptr); line_char(ctx->line, L']'); free_wstring(w); } } } } static void li_handler(struct html_render *ctx, GumboVector *attrs) { (void)attrs; if (ctx->list_depth > 0) { ctx->line->head->indent = SPACES_PER_INDENTATION_LEVEL * (ctx->list_depth * 2 - 1); ctx->line->indent = SPACES_PER_INDENTATION_LEVEL * (ctx->list_depth * 2); } if (ctx->list_levels[ctx->list_depth].is_ordered == true) { ctx->list_levels[ctx->list_depth].length += 1; wchar_t num[100]; swprintf(num, 100, L"%u. ", ctx->list_levels[ctx->list_depth].length); line_string(ctx->line, num); } else { line_string(ctx->line, L"* "); } } static void ul_start_handler(struct html_render *ctx, GumboVector *attrs) { (void)attrs; if (ctx->list_depth < MAX_NESTED_LISTS_DEPTH) { ctx->list_depth += 1; ctx->list_levels[ctx->list_depth].is_ordered = false; } } static void ul_end_handler(struct html_render *ctx, GumboVector *attrs) { (void)attrs; if (ctx->list_depth > 0) { ctx->list_depth -= 1; } ctx->line->indent = SPACES_PER_INDENTATION_LEVEL * (ctx->list_depth * 2); ctx->line->head->indent = ctx->line->indent; } static void ol_start_handler(struct html_render *ctx, GumboVector *attrs) { long list_start = 0; GumboAttribute *start = gumbo_get_attribute(attrs, "start"); if (start != NULL && start->value != NULL && strlen(start->value) > 0) { list_start = strtol(start->value, NULL, 10) - 1; } if (ctx->list_depth < MAX_NESTED_LISTS_DEPTH) { ctx->list_depth += 1; ctx->list_levels[ctx->list_depth].is_ordered = true; ctx->list_levels[ctx->list_depth].length = list_start; } } static void indent_start_handler(struct html_render *ctx, GumboVector *attrs) { (void)attrs; ctx->line->head->indent += SPACES_PER_INDENTATION_LEVEL; ctx->line->indent = ctx->line->head->indent; } static void indent_end_handler(struct html_render *ctx, GumboVector *attrs) { (void)attrs; if (ctx->line->head->indent >= SPACES_PER_INDENTATION_LEVEL) { ctx->line->indent = ctx->line->head->indent - SPACES_PER_INDENTATION_LEVEL; } if (ctx->line->head->ws->len == 0) { ctx->line->head->indent = ctx->line->indent; } } static void html_table_add_row(struct html_render *ctx, GumboVector *attrs) { (void)attrs; ctx->table.rows = newsraft_realloc(ctx->table.rows, sizeof(struct html_table_row) * (ctx->table.rows_count + 1)); ctx->table.rows[ctx->table.rows_count].cells = NULL; ctx->table.rows[ctx->table.rows_count].cells_count = 0; ctx->table.rows[ctx->table.rows_count].max_cell_height = 1; ctx->table.rows_count += 1; } static int8_t html_table_get_cells_count_from_tag_attribute(GumboVector *attrs, const char *attr_name) { GumboAttribute *attr = gumbo_get_attribute(attrs, attr_name); if (attr != NULL && attr->value != NULL) { int8_t result = 0; if (sscanf(attr->value, "%" SCNd8, &result) == 1) { return result > 0 ? result : 1; } } return 1; } static void html_table_add_cell(struct html_render *ctx, GumboVector *attrs) { if (ctx->table.rows_count == 0) { html_table_add_row(ctx, attrs); if (ctx->table.rows_count == 0) { return; } } int64_t i; struct html_table_row *last_row = &ctx->table.rows[ctx->table.rows_count - 1]; int64_t old_count = last_row->cells_count; // Before adding another cell we have to check if the place where we write // the cell to is not reserved by the cells with the rowspan attribute // above. If it is reserved, we have to shift write target to another cell. for (i = 0; i < ctx->table.rows_count - 1; ++i) { if (last_row->cells_count < ctx->table.rows[i].cells_count) { if (ctx->table.rows[i].cells[last_row->cells_count].rowspan + i >= ctx->table.rows_count) { last_row->cells_count += 1; i = -1; // And again. } } } int8_t colspan = html_table_get_cells_count_from_tag_attribute(attrs, "colspan"); int8_t rowspan = html_table_get_cells_count_from_tag_attribute(attrs, "rowspan"); last_row->cells_count += colspan; last_row->cells = newsraft_realloc(last_row->cells, sizeof(struct html_table_cell) * last_row->cells_count); for (i = old_count; i < last_row->cells_count; ++i) { memset(&last_row->cells[i], 0, sizeof(struct html_table_cell)); last_row->cells[i].line.target = &last_row->cells[i].text; if (i < last_row->cells_count - colspan) { last_row->cells[i].colspan = 1; // TODO borrow colspan from cells above last_row->cells[i].rowspan = -1; } else if (i > last_row->cells_count - colspan) { last_row->cells[i].colspan = -1; last_row->cells[i].rowspan = rowspan; } else { last_row->cells[i].colspan = colspan; last_row->cells[i].rowspan = rowspan; } last_row->cells[i].index = i; line_bump(&last_row->cells[i].line); } ctx->line = &last_row->cells[last_row->cells_count - colspan].line; } static inline void adjust_column_widths_to_contain_cells_with_colspan_attribute(struct html_table *table) { size_t sum_width; size_t *min_width; for (int64_t i = 0; i < table->rows_count; ++i) { for (int64_t j = 0; j < table->rows[i].cells_count; ++j) { if (table->rows[i].cells[j].colspan > 1) { while (true) { sum_width = table->column_widths[j]; min_width = &table->column_widths[j]; for (int64_t k = 1; (k < table->rows[i].cells[j].colspan) && ((j + k) < table->columns_count); ++k) { sum_width += table->column_widths[j + k]; if (table->column_widths[j + k] < *min_width) { min_width = &table->column_widths[j + k]; } } // table->rows[i].cells[j].lines is not NULL because // all cells with (colspan > 1) have at least one line. if (sum_width < table->rows[i].cells[j].text.lines[0].ws->len) { *min_width += 1; } else { break; } } } } } } static inline void print_html_table(struct line *line, struct html_table *table) { // Get maximum columns count int max_columns_count = 0; for (int i = 0; i < table->rows_count; ++i) { if (table->rows[i].cells_count > max_columns_count) { max_columns_count = table->rows[i].cells_count; } } table->columns_count = max_columns_count; table->column_widths = newsraft_calloc(table->columns_count, sizeof(size_t)); // Get row heights for (int i = 0; i < table->rows_count; ++i) { for (int j = 0; j < table->rows[i].cells_count; ++j) { if (table->rows[i].max_cell_height < table->rows[i].cells[j].text.lines_len) { table->rows[i].max_cell_height = table->rows[i].cells[j].text.lines_len; } } } // Get column widths for (int i = 0; i < table->rows_count; ++i) { for (int j = 0; j < table->rows[i].cells_count; ++j) { if (table->rows[i].cells[j].colspan > 1) { // Don't expand width of an entire column just because of the content // of a cell that spans multiple columns. The required width will be // calculated later to evenly space the content of cells that are not // spread across multiple columns. continue; } for (size_t k = 0; k < table->rows[i].cells[j].text.lines_len; ++k) { if (table->column_widths[j] < table->rows[i].cells[j].text.lines[k].ws->len) { table->column_widths[j] = table->rows[i].cells[j].text.lines[k].ws->len; } } } } adjust_column_widths_to_contain_cells_with_colspan_attribute(table); for (int64_t i = 0; i < table->rows_count; ++i) { // fprintf(stderr, "--> ROW\n"); for (size_t height = 0; height < table->rows[i].max_cell_height; ++height) { for (int64_t j = 0; j < table->rows[i].cells_count; ++j) { // fprintf(stderr, "----> CEL (%zu)\n", height); struct html_table_cell *cells = &table->rows[i].cells[j]; if (cells->colspan > 0) { long w = table->column_widths[j]; for (int64_t k = j + 1; (k < table->rows[i].cells_count) && (table->rows[i].cells[k].colspan == -1); ++k) { w += HTML_TABLE_COLUMN_SPACING + table->column_widths[k]; } if (cells->rowspan >= 0 && height < cells->text.lines_len) { // fprintf(stderr, "------> %ls\n", cells->text.lines[height].ws->ptr); for (size_t g = 0; g < cells->text.lines[height].ws->len; ++g) { line_style(line, cells->text.lines[height].hints[g]); line_char(line, cells->text.lines[height].ws->ptr[g]); line_unstyle(line); } w -= cells->text.lines[height].ws->len; } // Be careful not to add whitespace for trailing column! if (j + 1 < table->rows[i].cells_count) { for (long l = 0; l < w + HTML_TABLE_COLUMN_SPACING; ++l) { line_string(line, L"\u00A0"); // non-breaking space } } } // fprintf(stderr, "<---- CEL (%zu)\n", height); } line_char(line, L'\n'); } // fprintf(stderr, "<-- ROW\n"); } } static inline void free_abbrs(struct html_render *ctx) { for (size_t i = 0; i < ctx->abbrs_len; ++i) { free_string(ctx->abbrs[i]); } free(ctx->abbrs); } static inline void free_html_table(struct html_table *table) { for (int i = 0; i < table->rows_count; ++i) { for (int j = 0; j < table->rows[i].cells_count; ++j) { free(table->rows[i].cells[j].line.style_stack); free_render_result(&table->rows[i].cells[j].text); } free(table->rows[i].cells); } free(table->rows); free(table->column_widths); memset(table, 0, sizeof(struct html_table)); } static const struct html_element_renderer renderers[] = { {GUMBO_TAG_SPAN, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_P, true, 2, 2, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_BR, true, 0, 0, NULL, NULL, &br_handler, NULL, TB_DEFAULT}, {GUMBO_TAG_HR, true, 1, 1, NULL, NULL, &hr_handler, NULL, TB_DEFAULT}, {GUMBO_TAG_A, true, 0, 0, NULL, NULL, NULL, &url_mark_handler, TB_UNDERLINE}, {GUMBO_TAG_IMG, true, 0, 0, NULL, L"[image]", NULL, &url_mark_handler, TB_UNDERLINE}, {GUMBO_TAG_IFRAME, true, 0, 0, NULL, L"[iframe]", NULL, &url_mark_handler, TB_UNDERLINE}, {GUMBO_TAG_EMBED, true, 0, 0, NULL, L"[embed]", NULL, &url_mark_handler, TB_UNDERLINE}, {GUMBO_TAG_SOURCE, true, 0, 0, NULL, L"[source]", NULL, &url_mark_handler, TB_UNDERLINE}, {GUMBO_TAG_OBJECT, true, 0, 0, NULL, L"[object]", NULL, &url_mark_handler, TB_UNDERLINE}, {GUMBO_TAG_AUDIO, true, 0, 0, NULL, L"[audio]", NULL, &url_mark_handler, TB_UNDERLINE}, {GUMBO_TAG_VIDEO, true, 0, 0, NULL, L"[video]", NULL, &url_mark_handler, TB_UNDERLINE}, {GUMBO_TAG_U, true, 0, 0, NULL, NULL, NULL, NULL, TB_UNDERLINE}, {GUMBO_TAG_B, true, 0, 0, NULL, NULL, NULL, NULL, TB_BOLD}, {GUMBO_TAG_BIG, true, 0, 0, NULL, NULL, NULL, NULL, TB_BOLD}, {GUMBO_TAG_I, true, 0, 0, NULL, NULL, NULL, NULL, TB_ITALIC}, {GUMBO_TAG_EM, true, 0, 0, NULL, NULL, NULL, NULL, TB_ITALIC}, {GUMBO_TAG_VAR, true, 0, 0, NULL, NULL, NULL, NULL, TB_ITALIC}, {GUMBO_TAG_SMALL, true, 0, 0, NULL, NULL, NULL, NULL, TB_ITALIC}, {GUMBO_TAG_DFN, true, 0, 0, NULL, NULL, NULL, NULL, TB_ITALIC}, {GUMBO_TAG_INS, true, 0, 0, NULL, NULL, NULL, NULL, TB_ITALIC}, {GUMBO_TAG_ADDRESS, true, 1, 1, NULL, NULL, NULL, NULL, TB_ITALIC}, {GUMBO_TAG_MARK, true, 0, 0, NULL, NULL, NULL, NULL, TB_BOLD|TB_ITALIC}, {GUMBO_TAG_STRONG, true, 0, 0, NULL, NULL, NULL, NULL, TB_BOLD}, {GUMBO_TAG_HEADER, true, 3, 2, NULL, NULL, NULL, NULL, TB_BOLD}, {GUMBO_TAG_H1, true, 3, 2, NULL, NULL, NULL, NULL, TB_BOLD}, {GUMBO_TAG_H2, true, 3, 2, NULL, NULL, NULL, NULL, TB_BOLD}, {GUMBO_TAG_H3, true, 3, 2, NULL, NULL, NULL, NULL, TB_BOLD}, {GUMBO_TAG_H4, true, 3, 2, NULL, NULL, NULL, NULL, TB_BOLD}, {GUMBO_TAG_H5, true, 3, 2, NULL, NULL, NULL, NULL, TB_BOLD}, {GUMBO_TAG_H6, true, 3, 2, NULL, NULL, NULL, NULL, TB_BOLD}, {GUMBO_TAG_PRE, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_LI, true, 1, 1, NULL, NULL, &li_handler, NULL, TB_DEFAULT}, {GUMBO_TAG_UL, true, 0, 0, NULL, NULL, &ul_start_handler, &ul_end_handler, TB_DEFAULT}, {GUMBO_TAG_OL, true, 0, 0, NULL, NULL, &ol_start_handler, &ul_end_handler, TB_DEFAULT}, {GUMBO_TAG_FIGURE, true, 2, 2, NULL, NULL, &indent_start_handler, &indent_end_handler, TB_DEFAULT}, {GUMBO_TAG_BLOCKQUOTE, true, 2, 2, NULL, NULL, &indent_start_handler, &indent_end_handler, TB_DEFAULT}, {GUMBO_TAG_DD, true, 1, 1, NULL, NULL, &indent_start_handler, &indent_end_handler, TB_DEFAULT}, {GUMBO_TAG_TD, true, 0, 0, NULL, NULL, &html_table_add_cell, NULL, TB_DEFAULT}, {GUMBO_TAG_TH, true, 0, 0, NULL, NULL, &html_table_add_cell, NULL, TB_DEFAULT}, {GUMBO_TAG_TR, true, 0, 0, NULL, NULL, &html_table_add_row, NULL, TB_DEFAULT}, {GUMBO_TAG_ABBR, true, 0, 0, NULL, NULL, NULL, &abbr_handler, TB_DEFAULT}, {GUMBO_TAG_INPUT, true, 0, 0, NULL, NULL, &input_handler, NULL, TB_DEFAULT}, {GUMBO_TAG_DIV, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_CENTER, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_MAIN, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_ARTICLE, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_SUMMARY, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_FIGCAPTION, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_SECTION, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_FOOTER, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_OPTION, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_FORM, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_ASIDE, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_NAV, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_HGROUP, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_DT, true, 1, 1, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_DL, true, 2, 2, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_DETAILS, true, 2, 2, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_LABEL, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_TEXTAREA, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_CODE, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_TT, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_SAMP, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_KBD, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_CITE, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_TIME, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_FONT, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_BASEFONT, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_THEAD, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_TBODY, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_TFOOT, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_WBR, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_NOSCRIPT, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_DATA, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_APPLET, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_PARAM, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_LINK, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_CANVAS, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_META, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_BODY, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_HTML, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_SUP, true, 0, 0, L"^", NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_Q, true, 0, 0, L"\"", L"\"", NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_BUTTON, true, 0, 0, L"[", L"]", NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_SVG, false, 0, 0, NULL, L" [svg image]", NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_HEAD, false, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_STYLE, false, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_SCRIPT, false, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, // {GUMBO_TAG_PICTURE, true, 0, 0, NULL, NULL, NULL, /* not gumbo */ NULL, TB_DEFAULT}, // Gumbo can assign thead, tbody and tfoot tags to table element under // some circumstances, so we need to ignore these to avoid tag display. {GUMBO_TAG_THEAD, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_TBODY, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_TFOOT, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, {GUMBO_TAG_UNKNOWN, true, 0, 0, NULL, NULL, NULL, NULL, TB_DEFAULT}, }; static void render_html(GumboNode *node, struct html_render *ctx) { if (node->type == GUMBO_NODE_TEXT || node->type == GUMBO_NODE_CDATA || node->type == GUMBO_NODE_WHITESPACE) { struct wstring *wstr = convert_array_to_wstring(node->v.text.text, strlen(node->v.text.text)); if (wstr == NULL) { return; } for (const wchar_t *i = wstr->ptr; *i != L'\0'; ++i) { if (ctx->in_pre_depth > 0) { line_char(ctx->line, *i); } else if (!ISWIDEWHITESPACE(*i)) { line_char(ctx->line, *i); } else if (ctx->line->head->ws->len > 0) { if (!ISWIDEWHITESPACE(ctx->line->head->ws->ptr[ctx->line->head->ws->len - 1])) { line_char(ctx->line, L' '); } } } free_wstring(wstr); } else if (node->type == GUMBO_NODE_ELEMENT) { size_t i = 0; while ((renderers[i].tag_id != GUMBO_TAG_UNKNOWN) && (renderers[i].tag_id != node->v.element.tag)) { i += 1; } if (node->v.element.tag == GUMBO_TAG_TABLE && ctx->in_table == false) { ctx->in_table = true; struct line *origin = ctx->line; for (size_t j = 0; j < node->v.element.children.length; ++j) { render_html(node->v.element.children.data[j], ctx); } print_html_table(origin, &ctx->table); free_html_table(&ctx->table); ctx->line = origin; ctx->in_table = false; } else if (renderers[i].tag_id == GUMBO_TAG_UNKNOWN) { struct wstring *tag = convert_array_to_wstring(node->v.element.original_tag.data, node->v.element.original_tag.length); if (tag != NULL) { line_string(ctx->line, tag->ptr); free_wstring(tag); } for (size_t j = 0; j < node->v.element.children.length; ++j) { render_html(node->v.element.children.data[j], ctx); } tag = convert_array_to_wstring(node->v.element.original_end_tag.data, node->v.element.original_end_tag.length); if (tag != NULL) { line_string(ctx->line, tag->ptr); free_wstring(tag); } } else { if (renderers[i].tag_id == GUMBO_TAG_UL || renderers[i].tag_id == GUMBO_TAG_OL) { provide_newlines(ctx, ctx->list_depth == 0 ? 2 : 1, true); } else { provide_newlines(ctx, renderers[i].newlines_before, false); } if (renderers[i].tag_id == GUMBO_TAG_PRE) { ctx->in_pre_depth += 1; } if (renderers[i].start_handler != NULL) { renderers[i].start_handler(ctx, &node->v.element.attributes); } if (renderers[i].video_attrs != TB_DEFAULT) { line_style(ctx->line, renderers[i].video_attrs); } line_string(ctx->line, renderers[i].prefix); if (renderers[i].descentable == true) { for (size_t j = 0; j < node->v.element.children.length; ++j) { render_html(node->v.element.children.data[j], ctx); } } line_string(ctx->line, renderers[i].suffix); if (renderers[i].video_attrs != TB_DEFAULT) { line_unstyle(ctx->line); } if (renderers[i].end_handler != NULL) { renderers[i].end_handler(ctx, &node->v.element.attributes); } if (renderers[i].tag_id == GUMBO_TAG_PRE) { ctx->in_pre_depth -= 1; } if (renderers[i].tag_id == GUMBO_TAG_UL || renderers[i].tag_id == GUMBO_TAG_OL) { provide_newlines(ctx, ctx->list_depth == 0 ? 2 : 1, true); } else { provide_newlines(ctx, renderers[i].newlines_after, true); } } } } bool render_text_html(struct line *line, const struct wstring *source, struct links_list *links) { struct html_render html = { .line = line, .links = links, }; struct string *str = convert_wstring_to_string(source); if (str == NULL) { return false; } GumboOutput *output = gumbo_parse(str->ptr); if (output == NULL) { FAIL("Couldn't parse HTML successfully!"); free_string(str); return false; } render_html(output->root, &html); gumbo_destroy_output(&kGumboDefaultOptions, output); free_string(str); free_abbrs(&html); return true; } newsraft/src/render_data/render-text-plain.c000066400000000000000000000030201516312403600214500ustar00rootroot00000000000000#include #include "render_data.h" bool render_text_plain(struct line *line, const struct wstring *source, struct links_list *links) { for (const wchar_t *i = source->ptr, *j = i; *i != L'\0'; i = j) { // find end of token while (!ISWIDEWHITESPACE(*j) && *j != L'\0') { j += 1; } // render token characters for (const wchar_t *k = i; k < j; ++k) { line_char(line, *k); } // render link mark if token is URI const wchar_t *scheme_delimiter = wcsstr(i, L"://"); if (scheme_delimiter && scheme_delimiter > i && scheme_delimiter < j) { // token contains scheme delimiter, so it's a good candidate for URI const wchar_t *uri_start = i; // RFC 3986 says URI scheme can only begin with ASCII letter while (uri_start < scheme_delimiter && (*uri_start < L'A' || *uri_start > L'Z') && (*uri_start < L'a' || *uri_start > L'z')) { uri_start += 1; } if (uri_start < scheme_delimiter) { struct string *link = convert_warray_to_string(uri_start, j - uri_start); if (link && link->len) { wchar_t url_mark[100]; size_t url_index = add_url_to_links_list(links, link->ptr, link->len); // U+00A0 is non-breaking space if (swprintf(url_mark, 100, L"\u00A0[%zu]", url_index + 1) > 0) { line_style(line, TB_BOLD); line_string(line, url_mark); line_unstyle(line); } } free_string(link); } } // spew out whitespace character which concluded current token if (*j != L'\0') { line_char(line, *j); j += 1; } } return true; } newsraft/src/render_data/render_data.c000066400000000000000000000036441516312403600203720ustar00rootroot00000000000000#include #include "render_data.h" static inline void remove_trailing_empty_lines_except_for_first_one(struct line *line) { // Line processor expects there to be at least one line! // That's why we always keep the first line. for (size_t i = line->target->lines_len - 1; i > 0; --i) { for (size_t j = 0; j < line->target->lines[i].ws->len; ++j) { if (!ISWIDEWHITESPACE(line->target->lines[i].ws->ptr[j])) { return; } } free_wstring(line->target->lines[i].ws); free(line->target->lines[i].hints); line->target->lines_len -= 1; line->head = line->target->lines + line->target->lines_len - 1; } } bool render_data(struct config_context **ctx, struct render_result *result, struct render_blocks_list *blocks, size_t content_width) { size_t pager_width = get_cfg_uint(ctx, CFG_PAGER_WIDTH); struct line line = {.target = result}; line.lim = pager_width > 0 && pager_width < content_width ? pager_width : content_width; line_char(&line, L'\n'); // Add first line to line processor for (size_t i = 0; i < blocks->len; ++i) { line.indent = 0; line.style = TB_DEFAULT; line.style_stack_len = 0; if (blocks->ptr[i].content_type == TEXT_HTML) { render_text_html(&line, blocks->ptr[i].content, &blocks->links); } else if (blocks->ptr[i].content_type == TEXT_PLAIN) { render_text_plain(&line, blocks->ptr[i].content, &blocks->links); } else { // TEXT_RAW line_string(&line, blocks->ptr[i].content->ptr); } if (blocks->ptr[i].needs_trimming == true) { remove_trailing_empty_lines_except_for_first_one(&line); } } remove_trailing_empty_lines_except_for_first_one(&line); if (get_cfg_bool(ctx, CFG_PAGER_CENTERING) && pager_width > 0 && pager_width < content_width) { for (size_t i = 0; i < result->lines_len; ++i) { result->lines[i].indent += (content_width - pager_width) / 2; } } free(line.style_stack); return result->lines_len > 1 || result->lines[0].ws->len > 0 ? true : false; } newsraft/src/render_data/render_data.h000066400000000000000000000021261516312403600203710ustar00rootroot00000000000000#ifndef RENDER_DATA_H #define RENDER_DATA_H #include #include "newsraft.h" struct line { struct render_result *target; // Render output context struct render_line *head; // Line where text is currently added size_t lim; // Capacity of one text line size_t end; // Index of character suitable for line ending bool end_is_hyphenated; // Means we have to put - at the end of line size_t indent; // Indentation for subsequent line bumps newsraft_video_t style; // Cumulative style value for added text newsraft_video_t *style_stack; // Stack of all currently applied styles size_t style_stack_len; }; bool render_text_plain(struct line *line, const struct wstring *source, struct links_list *links); bool render_text_html(struct line *line, const struct wstring *source, struct links_list *links); bool line_char(struct line *line, wchar_t c); bool line_string(struct line *line, const wchar_t *str); void line_style(struct line *line, newsraft_video_t attrs); void line_unstyle(struct line *line); #endif // RENDER_DATA_H newsraft/src/sections.c000066400000000000000000000350601516312403600154760ustar00rootroot00000000000000#include #include #include "newsraft.h" struct feed_section { struct string *name; struct feed_entry **feeds; // Array of pointers to feeds belonging to this section. size_t feeds_count; size_t unread_count; bool has_errors; }; static struct feed_section *sections = NULL; static struct feed_section **sections_view = NULL; static size_t sections_count = 0; static sorting_method_t sections_sort = SORT_BY_INITIAL_ASC; static size_t get_sections_view_len(void) { if (get_cfg_bool(NULL, CFG_GLOBAL_SECTION_HIDE)) { return sections_count - 1; } else { return sections_count; } } static bool is_section_valid(struct menu_state *ctx, size_t index) { (void)ctx; return index < get_sections_view_len() ? true : false; } static const struct format_arg * get_section_args(struct menu_state *ctx, size_t index) { (void)ctx; static struct format_arg section_fmt[] = { {L'i', L'd', {.i = 0 }}, {L'u', L'd', {.i = 0 }}, {L't', L's', {.s = NULL}}, {L'\0', L'\0', {.i = 0 }}, // terminator }; section_fmt[0].value.i = index + 1; section_fmt[1].value.i = sections_view[index]->unread_count; section_fmt[2].value.s = sections_view[index]->name->ptr; return section_fmt; } static struct config_color paint_section(struct menu_state *ctx, size_t index, bool is_selected) { (void)ctx; struct config_color color; if (sections_view[index]->has_errors) { color = get_cfg_color(NULL, CFG_COLOR_LIST_SECTION_FAILED); } else if (sections_view[index]->unread_count > 0) { color = get_cfg_color(NULL, CFG_COLOR_LIST_SECTION_UNREAD); } else { color = get_cfg_color(NULL, CFG_COLOR_LIST_SECTION); } if (is_selected) { if (is_cfg_color_set(NULL, CFG_COLOR_LIST_SECTION_SELECTED)) { color = get_cfg_color(NULL, CFG_COLOR_LIST_SECTION_SELECTED); } else { color.attributes |= TB_REVERSE; } } return color; } static bool is_section_unread(struct menu_state *ctx, size_t index) { (void)ctx; return sections_view[index]->unread_count > 0; } static bool is_section_failed(struct menu_state *ctx, size_t index) { (void)ctx; return sections_view[index]->has_errors; } struct feed_entry ** get_all_feeds(size_t *feeds_count) { *feeds_count = sections[0].feeds_count; return sections[0].feeds; } char * get_section_name(size_t section_index) { if (section_index >= sections_count) return NULL; return sections[section_index].name->ptr; } void mark_feeds_read(struct feed_entry **feeds, size_t feeds_count, bool status) { if (db_change_unread_status_of_all_items_in_feeds(feeds, feeds_count, !status) == true) { for (size_t i = 0; i < feeds_count; ++i) { feeds[i]->unread_count = db_count_items(&feeds[i], 1, true); } refresh_sections_statistics_about_underlying_feeds(); expose_all_visible_entries_of_the_list_menu(); raise_menu_age(); } } int64_t make_sure_section_exists(const struct string *section_name) { for (size_t i = 0; i < sections_count; ++i) { if (strcmp(section_name->ptr, sections[i].name->ptr) == 0) { return i; } } sections = newsraft_realloc(sections, sizeof(struct feed_section) * (sections_count + 1)); sections_view = newsraft_realloc(sections_view, sizeof(struct feed_section *) * (sections_count + 1)); memset(§ions[sections_count], 0, sizeof(struct feed_section)); sections[sections_count].name = crtss(section_name); if (sections[sections_count].name == NULL) { write_error("Not enough memory for section name string!\n"); return -1; } INFO("Created section \"%s\".", sections[sections_count].name->ptr); sections_count += 1; // Have to update it every time because primary array is realloc'ed just above. if (get_cfg_bool(NULL, CFG_GLOBAL_SECTION_HIDE)) { for (size_t i = 1; i < sections_count; ++i) { sections_view[i - 1] = §ions[i]; } } else { for (size_t i = 0; i < sections_count; ++i) { sections_view[i] = §ions[i]; } } return sections_count - 1; } static inline struct feed_entry * find_feed_in_section(const struct string *link, const struct feed_section *section) { for (size_t i = 0; i < section->feeds_count; ++i) { if ((link->len == section->feeds[i]->url->len) && (strcmp(link->ptr, section->feeds[i]->url->ptr) == 0)) { return section->feeds[i]; } } return NULL; } static inline struct feed_entry * copy_feed_to_global_section(const struct feed_entry *feed) { struct feed_entry *duplicate = find_feed_in_section(feed->url, §ions[0]); if (duplicate != NULL) { return duplicate; } INFO("Adding to global section: %s", feed->url->ptr); size_t feed_index = (sections[0].feeds_count)++; sections[0].feeds = newsraft_realloc(sections[0].feeds, sizeof(struct feed_entry *) * sections[0].feeds_count); sections[0].feeds[feed_index] = newsraft_calloc(1, sizeof(struct feed_entry)); if (STRING_IS_EMPTY(feed->name)) { sections[0].feeds[feed_index]->name = db_get_string_from_feed_table(feed->url, "title", 5); } else { sections[0].feeds[feed_index]->name = crtss(feed->name); } if (sections[0].feeds[feed_index]->name != NULL) { inlinefy_string(sections[0].feeds[feed_index]->name); } sections[0].feeds[feed_index]->url = crtss(feed->url); sections[0].feeds[feed_index]->link = db_get_string_from_feed_table(feed->url, "link", 4); sections[0].feeds[feed_index]->errors = crtes(1); sections[0].feeds[feed_index]->update_date = db_get_date_from_feeds_table(feed->url, "update_date", 11); return sections[0].feeds[feed_index]; } struct feed_entry * copy_feed_to_section(const struct feed_entry *feed_data, int64_t section_index) { // All feeds without exception are stored in the global section struct feed_entry *feed = copy_feed_to_global_section(feed_data); if (feed == NULL) { write_error("Not enough memory!\n"); return NULL; } feed->section_index = section_index; // User sections contain only pointers to feeds in the global section struct feed_section *section = sections + section_index; if (find_feed_in_section(feed->url, section) == NULL) { section->feeds = newsraft_realloc(section->feeds, sizeof(struct feed_entry *) * (section->feeds_count + 1)); section->feeds[section->feeds_count] = feed; section->feeds_count += 1; } return feed; } void refresh_sections_statistics_about_underlying_feeds(void) { pthread_mutex_lock(&interface_lock); for (size_t i = 0; i < sections_count; ++i) { bool has_errors = false; sections[i].unread_count = 0; for (size_t j = 0; j < sections[i].feeds_count; ++j) { if (sections[i].feeds[j]->errors->len > 0 && !get_cfg_bool(§ions[i].feeds[j]->cfg, CFG_SUPPRESS_ERRORS)) { has_errors = true; } sections[i].unread_count += sections[i].feeds[j]->unread_count; } sections[i].has_errors = has_errors; } pthread_mutex_unlock(&interface_lock); } bool purge_abandoned_feeds(void) { if (sections[0].feeds_count == 0) { return true; } char *query = newsraft_malloc(sizeof(char) * (42 + sections[0].feeds_count * 2 + 100)); strcpy(query, "DELETE FROM items WHERE feed_url NOT IN (?"); for (size_t i = 1; i < sections[0].feeds_count; ++i) { strcat(query, ",?"); } strcat(query, ")"); sqlite3_stmt *res = db_prepare(query, strlen(query) + 1, NULL); if (res == NULL) { free(query); return false; } for (size_t i = 0; i < sections[0].feeds_count; ++i) { db_bind_feed_url(res, i + 1, sections[0].feeds[i]->url); } bool status = sqlite3_step(res) == SQLITE_DONE ? true : false; sqlite3_finalize(res); free(query); return status; } void free_sections(void) { size_t i; for (i = 0; i < sections[0].feeds_count; ++i) { if (sections[0].feeds[i] != NULL) { free_string(sections[0].feeds[i]->url); free_string(sections[0].feeds[i]->name); free_string(sections[0].feeds[i]->link); free_string(sections[0].feeds[i]->errors); free_config_context(sections[0].feeds[i]->cfg); if (sections[0].feeds[i]->binds != NULL) { free_binds(sections[0].feeds[i]->binds); } newsraft_free(sections[0].feeds[i]); } } for (i = 0; i < sections_count; ++i) { free_string(sections[i].name); newsraft_free(sections[i].feeds); } newsraft_free(sections_view); newsraft_free(sections); } bool start_updating_all_feeds_and_wait_finish(void) { queue_updates(sections[0].feeds, sections[0].feeds_count); queue_wait_finish(); return true; } bool print_unread_items_count(void) { int64_t count = db_count_items(sections[0].feeds, sections[0].feeds_count, true); fprintf(stdout, "%" PRId64 "\n", count); fflush(stdout); return true; } void process_auto_updating_feeds(void) { time_t now = time(NULL); if (now > 0) { for (size_t i = 0; i < sections[0].feeds_count; ++i) { size_t last_try = sections[0].feeds[i]->update_date; size_t age = (size_t)now > last_try ? now - last_try : 0; size_t reload_period = get_cfg_uint(§ions[0].feeds[i]->cfg, CFG_RELOAD_PERIOD) * 60; if (reload_period > 0 && age > reload_period) { queue_updates(sections[0].feeds + i, 1); } } } } static int compare_sections_initial(const void *data1, const void *data2) { struct feed_section **section1 = (struct feed_section **)data1; struct feed_section **section2 = (struct feed_section **)data2; size_t index1 = 0, index2 = 0; for (size_t i = 0; i < sections_count; ++i) { if (*section1 == §ions[i]) index1 = i; if (*section2 == §ions[i]) index2 = i; } if (index1 > index2) return sections_sort & 1 ? -1 : 1; if (index1 < index2) return sections_sort & 1 ? 1 : -1; return 0; } static int compare_sections_unread(const void *data1, const void *data2) { struct feed_section *section1 = *(struct feed_section **)data1; struct feed_section *section2 = *(struct feed_section **)data2; if (section1->unread_count > section2->unread_count) return sections_sort & 1 ? -1 : 1; if (section1->unread_count < section2->unread_count) return sections_sort & 1 ? 1 : -1; return compare_sections_initial(data1, data2) * (sections_sort & 1 ? -1 : 1); } static int compare_sections_alphabet(const void *data1, const void *data2) { struct feed_section *section1 = *(struct feed_section **)data1; struct feed_section *section2 = *(struct feed_section **)data2; return strcmp(section1->name->ptr, section2->name->ptr) * (sections_sort & 1 ? -1 : 1); } static inline void sort_sections(sorting_method_t method, bool we_are_already_in_sections_menu) { pthread_mutex_lock(&interface_lock); bool need_status_message = method != sections_sort; sections_sort = method; switch (sections_sort & ~1) { case SORT_BY_UNREAD_ASC: qsort(sections_view, get_sections_view_len(), sizeof(struct feed_section *), &compare_sections_unread); break; case SORT_BY_INITIAL_ASC: qsort(sections_view, get_sections_view_len(), sizeof(struct feed_section *), &compare_sections_initial); break; case SORT_BY_ALPHABET_ASC: qsort(sections_view, get_sections_view_len(), sizeof(struct feed_section *), &compare_sections_alphabet); break; } pthread_mutex_unlock(&interface_lock); if (we_are_already_in_sections_menu == true) { expose_all_visible_entries_of_the_list_menu(); if (need_status_message) { info_status(get_sorting_message(sections_sort), "sections"); } } } struct menu_state * sections_menu_loop(struct menu_state *m) { m->enumerator = &is_section_valid; m->printer = &list_menu_writer; m->get_args = &get_section_args; m->paint_action = &paint_section; m->unread_state = &is_section_unread; m->failed_state = &is_section_failed; m->entry_format = get_cfg_wstring(NULL, CFG_MENU_SECTION_ENTRY_FORMAT); if (!(m->flags & MENU_DISABLE_SETTINGS)) { // Don't set the menu names here because it's redundant! if (get_cfg_bool(NULL, CFG_SECTIONS_MENU_PARAMOUNT_EXPLORE) && db_count_items(sections[0].feeds, sections[0].feeds_count, false)) { return setup_menu(&items_menu_loop, NULL, sections[0].feeds, sections[0].feeds_count, MENU_IS_EXPLORE, NULL); } else if (sections_count == 1) { return setup_menu(&feeds_menu_loop, NULL, sections[0].feeds, sections[0].feeds_count, MENU_SWALLOW, NULL); } } refresh_sections_statistics_about_underlying_feeds(); if (m->is_initialized == false) { m->age = fetch_menu_age(); sort_sections(get_sorting_id(get_cfg_string(NULL, CFG_MENU_SECTION_SORTING)->ptr), false); } start_menu(); const struct wstring *arg; while (true) { if (get_cfg_bool(NULL, CFG_MENU_RESPONSIVENESS) && m->age != fetch_menu_age()) { m->age = fetch_menu_age(); sort_sections(sections_sort, true); } input_id cmd = get_input(NULL, NULL, &arg); if (handle_list_menu_control(m, cmd, arg) == true) { continue; } switch (cmd) { case INPUT_MARK_READ: mark_feeds_read(sections_view[m->view_sel]->feeds, sections_view[m->view_sel]->feeds_count, true); break; case INPUT_MARK_UNREAD: mark_feeds_read(sections_view[m->view_sel]->feeds, sections_view[m->view_sel]->feeds_count, false); break; case INPUT_MARK_READ_ALL: mark_feeds_read(sections[0].feeds, sections[0].feeds_count, true); break; case INPUT_MARK_UNREAD_ALL: mark_feeds_read(sections[0].feeds, sections[0].feeds_count, false); break; case INPUT_RELOAD: queue_updates(sections_view[m->view_sel]->feeds, sections_view[m->view_sel]->feeds_count); break; case INPUT_RELOAD_ALL: queue_updates(sections[0].feeds, sections[0].feeds_count); break; case INPUT_ENTER: return setup_menu(&feeds_menu_loop, sections_view[m->view_sel]->name, sections_view[m->view_sel]->feeds, sections_view[m->view_sel]->feeds_count, MENU_NORMAL, NULL); case INPUT_TOGGLE_EXPLORE_MODE: return setup_menu(&items_menu_loop, NULL, sections[0].feeds, sections[0].feeds_count, MENU_IS_EXPLORE, NULL); case INPUT_APPLY_SEARCH_MODE_FILTER: return setup_menu(&items_menu_loop, NULL, sections[0].feeds, sections[0].feeds_count, MENU_IS_SEARCH | MENU_IS_EXPLORE, NULL); case INPUT_VIEW_ERRORS: return setup_menu(&errors_pager_loop, NULL, sections_view[m->view_sel]->feeds, sections_view[m->view_sel]->feeds_count, MENU_NORMAL, NULL); case INPUT_SORT_BY_UNREAD: sort_sections(sections_sort == SORT_BY_UNREAD_DESC ? SORT_BY_UNREAD_ASC : SORT_BY_UNREAD_DESC, true); break; case INPUT_SORT_BY_INITIAL: sort_sections(sections_sort == SORT_BY_INITIAL_ASC ? SORT_BY_INITIAL_DESC : SORT_BY_INITIAL_ASC, true); break; case INPUT_SORT_BY_ALPHABET: sort_sections(sections_sort == SORT_BY_ALPHABET_ASC ? SORT_BY_ALPHABET_DESC : SORT_BY_ALPHABET_ASC, true); break; case INPUT_DATABASE_COMMAND: db_perform_user_edit(arg, sections_view[m->view_sel]->feeds, sections_view[m->view_sel]->feeds_count, NULL); break; case INPUT_FIND_COMMAND: return setup_menu(&items_menu_loop, NULL, sections[0].feeds, sections[0].feeds_count, MENU_IS_EXPLORE, arg); case INPUT_QUIT_SOFT: case INPUT_QUIT_HARD: return NULL; } } return NULL; } newsraft/src/signal.c000066400000000000000000000007371516312403600151270ustar00rootroot00000000000000#include #include "newsraft.h" static void tell_program_to_terminate_safely_and_quickly(int dummy) { (void)dummy; they_want_us_to_stop = true; } bool register_signal_handlers(void) { struct sigaction s = {0}; s.sa_handler = &tell_program_to_terminate_safely_and_quickly; if (sigaction(SIGQUIT, &s, NULL) || sigaction(SIGINT, &s, NULL) || sigaction(SIGTERM, &s, NULL)) { write_error("Failed to register signal handlers!\n"); return false; } return true; } newsraft/src/sorting.c000066400000000000000000000044661516312403600153420ustar00rootroot00000000000000#include #include "newsraft.h" struct sorting_bundle { const char *setting; const char *message; }; static struct sorting_bundle sorting_methods[] = { [SORT_BY_INITIAL_ASC] = {"initial-asc", "Sorted %s according to initial order (ascending)"}, [SORT_BY_INITIAL_DESC] = {"initial-desc", "Sorted %s according to initial order (descending)"}, [SORT_BY_TIME_ASC] = {"time-asc", "Sorted %s by time (ascending)"}, [SORT_BY_TIME_DESC] = {"time-desc", "Sorted %s by time (descending)"}, [SORT_BY_TIME_DOWNLOAD_ASC] = {"time-download-asc", "Sorted %s by download time (ascending)"}, [SORT_BY_TIME_DOWNLOAD_DESC] = {"time-download-desc", "Sorted %s by download time (descending)"}, [SORT_BY_TIME_PUBLICATION_ASC] = {"time-publication-asc", "Sorted %s by publication time (ascending)"}, [SORT_BY_TIME_PUBLICATION_DESC] = {"time-publication-desc", "Sorted %s by publication time (descending)"}, [SORT_BY_TIME_UPDATE_ASC] = {"time-update-asc", "Sorted %s by update time (ascending)"}, [SORT_BY_TIME_UPDATE_DESC] = {"time-update-desc", "Sorted %s by update time (descending)"}, [SORT_BY_ROWID_ASC] = {"rowid-asc", "Sorted %s by rowid (ascending)"}, [SORT_BY_ROWID_DESC] = {"rowid-desc", "Sorted %s by rowid (descending)"}, [SORT_BY_UNREAD_ASC] = {"unread-asc", "Sorted %s by unread (ascending)"}, [SORT_BY_UNREAD_DESC] = {"unread-desc", "Sorted %s by unread (descending)"}, [SORT_BY_ALPHABET_ASC] = {"alphabet-asc", "Sorted %s in alphabetical order (ascending)"}, [SORT_BY_ALPHABET_DESC] = {"alphabet-desc", "Sorted %s in alphabetical order (descending)"}, [SORT_BY_IMPORTANT_ASC] = {"important-asc", "Sorted %s by importance (ascending)"}, [SORT_BY_IMPORTANT_DESC] = {"important-desc", "Sorted %s by importance (descending)"}, }; sorting_method_t get_sorting_id(const char *sorting_name) { for (size_t i = 0; i < LENGTH(sorting_methods); ++i) { if (strcmp(sorting_name, sorting_methods[i].setting) == 0) { return i; } } return SORT_BY_INITIAL_ASC; } const char * get_sorting_message(int sorting_id) { return sorting_methods[sorting_id].message; } newsraft/src/string-serialize.c000066400000000000000000000070251516312403600171420ustar00rootroot00000000000000#include #include #include "newsraft.h" struct deserialize_stream { struct string *entry; char delimiter; const char *prev_delimiter_pos; const char *delimiter_pos; }; // You can change it without breaking compatibility with existing databases, // because first character of serialized data is always a DELIMITER. And it // is easy for the stream handler (see below) to figure out which character // to take as a delimiter. #define DELIMITER 31 // ASCII Unit Separator (control character) static const char caret[2] = {DELIMITER, '^'}; void serialize_caret(struct string **target) { if (*target == NULL) { *target = crtes(100); } catas(*target, caret, 2); } void serialize_array(register struct string **target, register const char *key, register size_t key_len, register const char *value, register size_t value_len) { if (value == NULL || value_len == 0) { return; // Ignore empty entries. } make_string_fit_more(target, 1 + key_len + value_len); // Check tests/serialize_array.c to better understand what this function does. // Just append key and value to the target with DELIMITER at the beginning. // It's kind of messy here because we need to squeeze out all available performance // since this function does a lot of heavy lifting in the feed parsers code. (*target)->ptr[(*target)->len] = DELIMITER; memcpy((*target)->ptr + (*target)->len + 1, key, key_len); memcpy((*target)->ptr + (*target)->len + 1 + key_len, value, value_len); (*target)->len += 1 + key_len + value_len; (*target)->ptr[(*target)->len] = '\0'; // Make sure there's no delimiters in the serialized data! for (size_t i = 0, end = (*target)->len; i < value_len; ++i) { if ((*target)->ptr[end - i - 1] == DELIMITER) { for (char *j = (*target)->ptr + end - i - 1; *j != '\0'; ++j) { *j = *(j + 1); } (*target)->len -= 1; (*target)->ptr[(*target)->len] = '\0'; if ((*target)->len + value_len == end) { (*target)->len -= 1 + key_len; (*target)->ptr[(*target)->len] = '\0'; return; // Value consists only of delimiters! } } } return; } void serialize_string(struct string **target, const char *key, size_t key_len, const struct string *value) { if (!STRING_IS_EMPTY(value)) { serialize_array(target, key, key_len, value->ptr, value->len); } } struct deserialize_stream * open_deserialize_stream(const char *serialized_data) { struct deserialize_stream *stream = newsraft_malloc(sizeof(struct deserialize_stream)); stream->entry = crtes(10000); stream->delimiter = *serialized_data; if (stream->delimiter == '\0') { stream->prev_delimiter_pos = NULL; stream->delimiter_pos = NULL; } else { stream->prev_delimiter_pos = serialized_data; stream->delimiter_pos = strchr(serialized_data + 1, stream->delimiter); } return stream; } const struct string * get_next_entry_from_deserialize_stream(struct deserialize_stream *stream) { if (stream->delimiter_pos == NULL && stream->prev_delimiter_pos == NULL) { return NULL; } if (stream->delimiter_pos == NULL) { cpyas(&stream->entry, stream->prev_delimiter_pos + 1, strlen(stream->prev_delimiter_pos + 1)); stream->prev_delimiter_pos = NULL; } else { cpyas(&stream->entry, stream->prev_delimiter_pos + 1, stream->delimiter_pos - stream->prev_delimiter_pos - 1); stream->prev_delimiter_pos = stream->delimiter_pos; stream->delimiter_pos = strchr(stream->delimiter_pos + 1, stream->delimiter); } return stream->entry; } void close_deserialize_stream(struct deserialize_stream *stream) { if (stream != NULL) { free_string(stream->entry); free(stream); } } newsraft/src/string.c000066400000000000000000000151341516312403600151550ustar00rootroot00000000000000#include #include #include "newsraft.h" // Note to the future. // When allocating memory, we request more resources than necessary to reduce // the number of further realloc calls to expand string buffer. static inline void str_set(struct string **dest, const char *src_ptr, size_t src_len, size_t src_lim) { if (*dest == NULL) { struct string *str = newsraft_malloc(sizeof(struct string)); str->ptr = newsraft_malloc(sizeof(char) * (src_lim + 1)); if (src_ptr != NULL && src_len > 0) { memcpy(str->ptr, src_ptr, sizeof(char) * src_len); *(str->ptr + src_len) = '\0'; str->len = src_len; } else { *(str->ptr) = '\0'; str->len = 0; } str->lim = src_lim; *dest = str; } else { if (src_lim > (*dest)->lim) { (*dest)->ptr = newsraft_realloc((*dest)->ptr, sizeof(char) * (src_lim + 1)); (*dest)->lim = src_lim; } if (src_ptr != NULL && src_len > 0) { memcpy((*dest)->ptr, src_ptr, sizeof(char) * src_len); } *((*dest)->ptr + src_len) = '\0'; (*dest)->len = src_len; } } struct string * crtes(size_t desired_capacity) { struct string *str = NULL; str_set(&str, NULL, 0, desired_capacity); return str; } struct string * crtas(const char *src_ptr, size_t src_len) { struct string *str = NULL; str_set(&str, src_ptr, src_len, src_len); return str; } struct string * crtss(const struct string *src) { return crtas(src->ptr, src->len); } void cpyas(struct string **dest, const char *src_ptr, size_t src_len) { str_set(dest, src_ptr, src_len, src_len); } void cpyss(struct string **dest, const struct string *src) { str_set(dest, src->ptr, src->len, src->len); } void catas(struct string *dest, const char *src_ptr, size_t src_len) { size_t new_len = dest->len + src_len; if (new_len > dest->lim) { size_t new_lim = new_len * 2 + 67; dest->ptr = newsraft_realloc(dest->ptr, sizeof(char) * (new_lim + 1)); dest->lim = new_lim; } if (src_ptr != NULL && src_len > 0) { memcpy(dest->ptr + dest->len, src_ptr, sizeof(char) * src_len); } *(dest->ptr + new_len) = '\0'; dest->len = new_len; } void catss(struct string *dest, const struct string *src) { catas(dest, src->ptr, src->len); } void catcs(struct string *dest, char c) { catas(dest, &c, 1); } void make_string_fit_more(struct string **dest, size_t n) { if (*dest == NULL) { str_set(dest, NULL, 0, n); } else if ((*dest)->len + n > (*dest)->lim) { size_t new_lim = ((*dest)->len + n) * 2 + 67; (*dest)->ptr = newsraft_realloc((*dest)->ptr, sizeof(char) * (new_lim + 1)); (*dest)->lim = new_lim; } } void str_vappendf(struct string *dest, const char *fmt, va_list args) { va_list args_copy; va_copy(args_copy, args); char dummy; const int required_len = vsnprintf(&dummy, 0, fmt, args_copy); va_end(args_copy); if (required_len <= 0) { return; } char *buf = newsraft_malloc(required_len + 1); const int actual_len = vsnprintf(buf, required_len + 1, fmt, args); if (actual_len != required_len) { FAIL("vsnprintf() yielded invalid length!"); newsraft_free(buf); return; } catas(dest, buf, actual_len); newsraft_free(buf); } void str_appendf(struct string *dest, const char *fmt, ...) { va_list args; va_start(args, fmt); str_vappendf(dest, fmt, args); va_end(args); } void empty_string(struct string *dest) { dest->len = 0; dest->ptr[0] = '\0'; } void free_string(struct string *str) { if (str != NULL) { newsraft_free(str->ptr); newsraft_free(str); } } void trim_whitespace_from_string(struct string *str) { if (str != NULL && str->len > 0) { size_t left_edge = 0; while (left_edge < str->len && ISWHITESPACE(str->ptr[left_edge])) { left_edge += 1; } while (left_edge < str->len && ISWHITESPACE(str->ptr[str->len - 1])) { str->len -= 1; } if (left_edge > 0) { str->len -= left_edge; for (size_t i = 0; i < str->len; ++i) { str->ptr[i] = str->ptr[i + left_edge]; } } str->ptr[str->len] = '\0'; } } struct wstring * convert_string_to_wstring(const struct string *src) { struct wstring *wstr = wcrtes(src->len); if (wstr == NULL) { return NULL; } wstr->len = mbstowcs(wstr->ptr, src->ptr, wstr->lim + 1); if (wstr->len == (size_t)-1) { free_wstring(wstr); return NULL; } wstr->ptr[wstr->len] = L'\0'; return wstr; } struct wstring * convert_array_to_wstring(const char *src_ptr, size_t src_len) { struct string *str = crtas(src_ptr, src_len); if (str == NULL) { return NULL; } struct wstring *wstr = convert_string_to_wstring(str); free_string(str); return wstr; } void remove_start_of_string(struct string *str, size_t size) { if (size >= str->len) { empty_string(str); } else { for (size_t i = 0; (i + size) < str->len; ++i) { str->ptr[i] = str->ptr[i + size]; } str->len -= size; str->ptr[str->len] = '\0'; } } void inlinefy_string(struct string *str) { // Replace multiple whitespace with a single space. char *dest = str->ptr; char c = '\0'; for (const char *s = str->ptr; *s != '\0'; ++s) { if (ISWHITESPACE(*s)) { if (c == ' ') // previous character was whitespace continue; c = ' '; } else { c = *s; } *dest = c; ++dest; } *dest = '\0'; str->len = dest - str->ptr; } void newsraft_simple_hash(struct string **dest, const char *src) { uint64_t hash = 14695981039346656037LLU; for (const char *i = src; *i != '\0'; ++i) { hash = (hash ^ *i) * 1099511628211LLU; } char out[64]; for (size_t i = 0; i < sizeof(out); ++i) { out[i] = 32 + hash % 95; hash = (hash ^ ((hash << 39) | (hash >> 25))) * 1099511628211LLU; } cpyas(dest, out, sizeof(out)); } struct string * newsraft_base64_encode(const uint8_t *data, size_t size) { static char base64_encoding_table[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' }; struct string *out = crtes(size * 4); for (size_t i = 0; i < size;) { uint32_t octet_a = i < size ? data[i++] : 0; uint32_t octet_b = i < size ? data[i++] : 0; uint32_t octet_c = i < size ? data[i++] : 0; uint32_t triple = (octet_a << 16) + (octet_b << 8) + octet_c; catcs(out, base64_encoding_table[(triple >> 3 * 6) & 0x3F]); catcs(out, base64_encoding_table[(triple >> 2 * 6) & 0x3F]); catcs(out, base64_encoding_table[(triple >> 1 * 6) & 0x3F]); catcs(out, base64_encoding_table[(triple >> 0 * 6) & 0x3F]); } switch (size % 3) { case 1: if (out->len >= 2) out->ptr[out->len - 2] = '='; // fall through case 2: if (out->len >= 1) out->ptr[out->len - 1] = '='; // fall through } return out; } newsraft/src/struct-item.c000066400000000000000000000011051516312403600161200ustar00rootroot00000000000000#include #include "newsraft.h" void prepend_item(struct getfeed_item **head_item_ptr) { struct getfeed_item *item = newsraft_calloc(1, sizeof(struct getfeed_item)); item->next = *head_item_ptr; *head_item_ptr = item; } void free_item(struct getfeed_item *item) { for (struct getfeed_item *i = item; i != NULL; item = i) { free_string(item->guid); free_string(item->title); free_string(item->link); free_string(item->content); free_string(item->attachments); free_string(item->persons); free_string(item->extras); i = item->next; free(item); } } newsraft/src/termbox2.h000066400000000000000000003737541516312403600154350ustar00rootroot00000000000000/* MIT License Copyright (c) 2010-2020 nsf 2015-2025 Adam Saponara Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef TERMBOX_H_INCL #define TERMBOX_H_INCL #ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE #endif #ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef PATH_MAX #define TB_PATH_MAX PATH_MAX #else #define TB_PATH_MAX 4096 #endif #ifdef __cplusplus extern "C" { #endif // __ffi_start #define TB_VERSION_STR "2.7.0-dev" /* The following compile-time options are supported: * * TB_OPT_ATTR_W: Integer width of `fg` and `bg` attributes. Valid values * (assuming system support) are 16, 32, and 64. (See * `uintattr_t`). 32 or 64 enables output mode * `TB_OUTPUT_TRUECOLOR`. 64 enables additional style * attributes. (See `tb_set_output_mode`.) Larger values * consume more memory in exchange for more features. * Defaults to 16. * * TB_OPT_EGC: If set, enable extended grapheme cluster support * (`tb_extend_cell`, `tb_set_cell_ex`). Consumes more * memory. Defaults off. * * TB_OPT_PRINTF_BUF: Write buffer size for printf operations. Represents the * largest string that can be sent in one call to * `tb_print*` and `tb_send*` functions. Defaults to 4096. * * TB_OPT_READ_BUF: Read buffer size for tty reads. Defaults to 64. * * TB_OPT_TRUECOLOR: Deprecated. Sets TB_OPT_ATTR_W to 32 if not already set. */ #if defined(TB_LIB_OPTS) || 0 // __tb_lib_opts /* Ensure consistent compile-time options when using as a shared library */ #undef TB_OPT_ATTR_W #undef TB_OPT_EGC #undef TB_OPT_PRINTF_BUF #undef TB_OPT_READ_BUF #define TB_OPT_ATTR_W 64 #define TB_OPT_EGC #endif /* Ensure sane `TB_OPT_ATTR_W` (16, 32, or 64) */ #if defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 16 #elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 32 #elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 64 #else #undef TB_OPT_ATTR_W #if defined TB_OPT_TRUECOLOR // Deprecated. Back-compat for old flag. #define TB_OPT_ATTR_W 32 #else #define TB_OPT_ATTR_W 16 #endif #endif /* ASCII key constants (`tb_event.key`) */ #define TB_KEY_CTRL_TILDE 0x00 #define TB_KEY_CTRL_2 0x00 // clash with `CTRL_TILDE` #define TB_KEY_CTRL_A 0x01 #define TB_KEY_CTRL_B 0x02 #define TB_KEY_CTRL_C 0x03 #define TB_KEY_CTRL_D 0x04 #define TB_KEY_CTRL_E 0x05 #define TB_KEY_CTRL_F 0x06 #define TB_KEY_CTRL_G 0x07 #define TB_KEY_BACKSPACE 0x08 #define TB_KEY_CTRL_H 0x08 // clash with `CTRL_BACKSPACE` #define TB_KEY_TAB 0x09 #define TB_KEY_CTRL_I 0x09 // clash with `TAB` #define TB_KEY_CTRL_J 0x0a #define TB_KEY_CTRL_K 0x0b #define TB_KEY_CTRL_L 0x0c #define TB_KEY_ENTER 0x0d #define TB_KEY_CTRL_M 0x0d // clash with `ENTER` #define TB_KEY_CTRL_N 0x0e #define TB_KEY_CTRL_O 0x0f #define TB_KEY_CTRL_P 0x10 #define TB_KEY_CTRL_Q 0x11 #define TB_KEY_CTRL_R 0x12 #define TB_KEY_CTRL_S 0x13 #define TB_KEY_CTRL_T 0x14 #define TB_KEY_CTRL_U 0x15 #define TB_KEY_CTRL_V 0x16 #define TB_KEY_CTRL_W 0x17 #define TB_KEY_CTRL_X 0x18 #define TB_KEY_CTRL_Y 0x19 #define TB_KEY_CTRL_Z 0x1a #define TB_KEY_ESC 0x1b #define TB_KEY_CTRL_LSQ_BRACKET 0x1b // clash with 'ESC' #define TB_KEY_CTRL_3 0x1b // clash with 'ESC' #define TB_KEY_CTRL_4 0x1c #define TB_KEY_CTRL_BACKSLASH 0x1c // clash with 'CTRL_4' #define TB_KEY_CTRL_5 0x1d #define TB_KEY_CTRL_RSQ_BRACKET 0x1d // clash with 'CTRL_5' #define TB_KEY_CTRL_6 0x1e #define TB_KEY_CTRL_7 0x1f #define TB_KEY_CTRL_SLASH 0x1f // clash with 'CTRL_7' #define TB_KEY_CTRL_UNDERSCORE 0x1f // clash with 'CTRL_7' #define TB_KEY_SPACE 0x20 #define TB_KEY_BACKSPACE2 0x7f #define TB_KEY_CTRL_8 0x7f // clash with 'BACKSPACE2' #define tb_key_i(i) 0xffff - (i) /* Terminal-dependent key constants (`tb_event.key`) and terminfo caps */ /* BEGIN codegen h */ /* Produced by ./codegen.sh on Tue, 03 Sep 2024 04:17:47 +0000 */ #define TB_KEY_F1 (0xffff - 0) #define TB_KEY_F2 (0xffff - 1) #define TB_KEY_F3 (0xffff - 2) #define TB_KEY_F4 (0xffff - 3) #define TB_KEY_F5 (0xffff - 4) #define TB_KEY_F6 (0xffff - 5) #define TB_KEY_F7 (0xffff - 6) #define TB_KEY_F8 (0xffff - 7) #define TB_KEY_F9 (0xffff - 8) #define TB_KEY_F10 (0xffff - 9) #define TB_KEY_F11 (0xffff - 10) #define TB_KEY_F12 (0xffff - 11) #define TB_KEY_INSERT (0xffff - 12) #define TB_KEY_DELETE (0xffff - 13) #define TB_KEY_HOME (0xffff - 14) #define TB_KEY_END (0xffff - 15) #define TB_KEY_PGUP (0xffff - 16) #define TB_KEY_PGDN (0xffff - 17) #define TB_KEY_ARROW_UP (0xffff - 18) #define TB_KEY_ARROW_DOWN (0xffff - 19) #define TB_KEY_ARROW_LEFT (0xffff - 20) #define TB_KEY_ARROW_RIGHT (0xffff - 21) #define TB_KEY_BACK_TAB (0xffff - 22) #define TB_KEY_MOUSE_LEFT (0xffff - 23) #define TB_KEY_MOUSE_RIGHT (0xffff - 24) #define TB_KEY_MOUSE_MIDDLE (0xffff - 25) #define TB_KEY_MOUSE_RELEASE (0xffff - 26) #define TB_KEY_MOUSE_WHEEL_UP (0xffff - 27) #define TB_KEY_MOUSE_WHEEL_DOWN (0xffff - 28) #define TB_CAP_F1 0 #define TB_CAP_F2 1 #define TB_CAP_F3 2 #define TB_CAP_F4 3 #define TB_CAP_F5 4 #define TB_CAP_F6 5 #define TB_CAP_F7 6 #define TB_CAP_F8 7 #define TB_CAP_F9 8 #define TB_CAP_F10 9 #define TB_CAP_F11 10 #define TB_CAP_F12 11 #define TB_CAP_INSERT 12 #define TB_CAP_DELETE 13 #define TB_CAP_HOME 14 #define TB_CAP_END 15 #define TB_CAP_PGUP 16 #define TB_CAP_PGDN 17 #define TB_CAP_ARROW_UP 18 #define TB_CAP_ARROW_DOWN 19 #define TB_CAP_ARROW_LEFT 20 #define TB_CAP_ARROW_RIGHT 21 #define TB_CAP_BACK_TAB 22 #define TB_CAP__COUNT_KEYS 23 #define TB_CAP_ENTER_CA 23 #define TB_CAP_EXIT_CA 24 #define TB_CAP_SHOW_CURSOR 25 #define TB_CAP_HIDE_CURSOR 26 #define TB_CAP_CLEAR_SCREEN 27 #define TB_CAP_SGR0 28 #define TB_CAP_UNDERLINE 29 #define TB_CAP_BOLD 30 #define TB_CAP_BLINK 31 #define TB_CAP_ITALIC 32 #define TB_CAP_REVERSE 33 #define TB_CAP_ENTER_KEYPAD 34 #define TB_CAP_EXIT_KEYPAD 35 #define TB_CAP_DIM 36 #define TB_CAP_INVISIBLE 37 #define TB_CAP__COUNT 38 /* END codegen h */ /* Some hard-coded caps */ #define TB_HARDCAP_ENTER_MOUSE "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h" #define TB_HARDCAP_EXIT_MOUSE "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l" #define TB_HARDCAP_STRIKEOUT "\x1b[9m" #define TB_HARDCAP_UNDERLINE_2 "\x1b[21m" #define TB_HARDCAP_OVERLINE "\x1b[53m" /* Colors (numeric) and attributes (bitwise) (`tb_cell.fg`, `tb_cell.bg`) */ #define TB_DEFAULT 0x0000 #define TB_BLACK 0x0001 #define TB_RED 0x0002 #define TB_GREEN 0x0003 #define TB_YELLOW 0x0004 #define TB_BLUE 0x0005 #define TB_MAGENTA 0x0006 #define TB_CYAN 0x0007 #define TB_WHITE 0x0008 #if TB_OPT_ATTR_W == 16 #define TB_BOLD 0x0100 #define TB_UNDERLINE 0x0200 #define TB_REVERSE 0x0400 #define TB_ITALIC 0x0800 #define TB_BLINK 0x1000 #define TB_HI_BLACK 0x2000 #define TB_BRIGHT 0x4000 #define TB_DIM 0x8000 #define TB_256_BLACK TB_HI_BLACK // `TB_256_BLACK` is deprecated #else // `TB_OPT_ATTR_W` is 32 or 64 #define TB_BOLD 0x01000000 #define TB_UNDERLINE 0x02000000 #define TB_REVERSE 0x04000000 #define TB_ITALIC 0x08000000 #define TB_BLINK 0x10000000 #define TB_HI_BLACK 0x20000000 #define TB_BRIGHT 0x40000000 #define TB_DIM 0x80000000 #define TB_TRUECOLOR_BOLD TB_BOLD // `TB_TRUECOLOR_*` is deprecated #define TB_TRUECOLOR_UNDERLINE TB_UNDERLINE #define TB_TRUECOLOR_REVERSE TB_REVERSE #define TB_TRUECOLOR_ITALIC TB_ITALIC #define TB_TRUECOLOR_BLINK TB_BLINK #define TB_TRUECOLOR_BLACK TB_HI_BLACK #endif #if TB_OPT_ATTR_W == 64 #define TB_STRIKEOUT 0x0000000100000000 #define TB_UNDERLINE_2 0x0000000200000000 #define TB_OVERLINE 0x0000000400000000 #define TB_INVISIBLE 0x0000000800000000 #endif /* Event types (`tb_event.type`) */ #define TB_EVENT_KEY 1 #define TB_EVENT_RESIZE 2 #define TB_EVENT_MOUSE 3 /* Key modifiers (bitwise) (`tb_event.mod`) */ #define TB_MOD_ALT 1 #define TB_MOD_CTRL 2 #define TB_MOD_SHIFT 4 #define TB_MOD_MOTION 8 /* Input modes (bitwise) (`tb_set_input_mode`) */ #define TB_INPUT_CURRENT 0 #define TB_INPUT_ESC 1 #define TB_INPUT_ALT 2 #define TB_INPUT_MOUSE 4 /* Output modes (`tb_set_output_mode`) */ #define TB_OUTPUT_CURRENT 0 #define TB_OUTPUT_NORMAL 1 #define TB_OUTPUT_256 2 #define TB_OUTPUT_216 3 #define TB_OUTPUT_GRAYSCALE 4 #if TB_OPT_ATTR_W >= 32 #define TB_OUTPUT_TRUECOLOR 5 #endif /* Common function return values unless otherwise noted. * * Library behavior is undefined after receiving `TB_ERR_MEM`. Callers may * attempt reinitializing by freeing memory, invoking `tb_shutdown`, then * `tb_init`. */ #define TB_OK 0 #define TB_ERR -1 #define TB_ERR_NEED_MORE -2 #define TB_ERR_INIT_ALREADY -3 #define TB_ERR_INIT_OPEN -4 #define TB_ERR_MEM -5 #define TB_ERR_NO_EVENT -6 #define TB_ERR_NO_TERM -7 #define TB_ERR_NOT_INIT -8 #define TB_ERR_OUT_OF_BOUNDS -9 #define TB_ERR_READ -10 #define TB_ERR_RESIZE_IOCTL -11 #define TB_ERR_RESIZE_PIPE -12 #define TB_ERR_RESIZE_SIGACTION -13 #define TB_ERR_POLL -14 #define TB_ERR_TCGETATTR -15 #define TB_ERR_TCSETATTR -16 #define TB_ERR_UNSUPPORTED_TERM -17 #define TB_ERR_RESIZE_WRITE -18 #define TB_ERR_RESIZE_POLL -19 #define TB_ERR_RESIZE_READ -20 #define TB_ERR_RESIZE_SSCANF -21 #define TB_ERR_CAP_COLLISION -22 #define TB_ERR_SELECT TB_ERR_POLL #define TB_ERR_RESIZE_SELECT TB_ERR_RESIZE_POLL /* Deprecated. Function types to be used with `tb_set_func`. */ #define TB_FUNC_EXTRACT_PRE 0 #define TB_FUNC_EXTRACT_POST 1 /* Define this to set the size of the buffer used in `tb_printf` * and `tb_sendf` */ #ifndef TB_OPT_PRINTF_BUF #define TB_OPT_PRINTF_BUF 4096 #endif /* Define this to set the size of the read buffer used when reading * from the tty */ #ifndef TB_OPT_READ_BUF #define TB_OPT_READ_BUF 64 #endif /* Define this for limited back compat with termbox v1 */ #ifdef TB_OPT_V1_COMPAT #define tb_change_cell tb_set_cell #define tb_put_cell(x, y, c) tb_set_cell((x), (y), (c)->ch, (c)->fg, (c)->bg) #define tb_set_clear_attributes tb_set_clear_attrs #define tb_select_input_mode tb_set_input_mode #define tb_select_output_mode tb_set_output_mode #endif /* Define these to swap in a different allocator */ #ifndef tb_malloc #define tb_malloc malloc #define tb_realloc realloc #define tb_free free #endif #define tb_log(...) if (global.fn_print_log_msg) global.fn_print_log_msg(__VA_ARGS__) #if TB_OPT_ATTR_W == 64 typedef uint64_t uintattr_t; #elif TB_OPT_ATTR_W == 32 typedef uint32_t uintattr_t; #else // 16 typedef uint16_t uintattr_t; #endif /* A cell in a 2d grid representing the terminal screen. * * The terminal screen is represented as 2d array of cells. The structure is * optimized for dealing with single-width (`wcwidth==1`) Unicode codepoints, * however some support for grapheme clusters (e.g., combining diacritical * marks) and wide codepoints (e.g., Hiragana) is provided through `ech`, * `nech`, and `cech` via `tb_set_cell_ex`. `ech` is only valid when `nech>0`, * otherwise `ch` is used. * * For non-single-width codepoints, given `N=wcwidth(ch)/wcswidth(ech)`: * * when `N==0`: termbox forces a single-width cell. Callers should avoid this * if aiming to render text accurately. Callers may use * `tb_set_cell_ex` or `tb_print*` to render `N==0` combining * characters. * * when `N>1`: termbox zeroes out the following `N-1` cells and skips sending * them to the tty. So, e.g., if the caller sets `x=0,y=0` to an * `N==2` codepoint, the caller's next set should be at `x=2,y=0`. * Anything set at `x=1,y=0` will be ignored. If there are not * enough columns remaining on the line to render `N` width, spaces * are sent instead. * * See `tb_present` for implementation. */ struct tb_cell { uint32_t ch; // a Unicode codepoint uintattr_t fg; // bitwise foreground attributes uintattr_t bg; // bitwise background attributes #ifdef TB_OPT_EGC uint32_t *ech; // a grapheme cluster of Unicode codepoints, 0-terminated size_t nech; // num elements in ech, 0 means use ch instead of ech size_t cech; // num elements allocated for ech #endif }; /* An incoming event from the tty. * * Given the event type, the following fields are relevant: * * when `TB_EVENT_KEY`: `key` xor `ch` (one will be zero) and `mod`. Note * there is overlap between `TB_MOD_CTRL` and * `TB_KEY_CTRL_*`. `TB_MOD_CTRL` and `TB_MOD_SHIFT` are * only set as modifiers to `TB_KEY_ARROW_*`. * * when `TB_EVENT_RESIZE`: `w` and `h` * * when `TB_EVENT_MOUSE`: `key` (`TB_KEY_MOUSE_*`), `x`, and `y` */ struct tb_event { uint8_t type; // one of `TB_EVENT_*` constants uint8_t mod; // bitwise `TB_MOD_*` constants uint16_t key; // one of `TB_KEY_*` constants uint32_t ch; // a Unicode codepoint int32_t w; // resize width int32_t h; // resize height int32_t x; // mouse x int32_t y; // mouse y }; /* Initialize the termbox library. This function should be called before any * other functions. `tb_init` is equivalent to `tb_init_file("/dev/tty")`. After * successful initialization, the library must be finalized using `tb_shutdown`. */ int tb_init(void); int tb_init_file(const char *path); int tb_init_fd(int ttyfd); int tb_init_rwfd(int rfd, int wfd); int tb_shutdown(void); /* Return the size of the internal back buffer (which is the same as terminal's * window size in rows and columns). The internal buffer can be resized after * `tb_clear` or `tb_present` calls. Both dimensions have an unspecified * negative value when called before `tb_init` or after `tb_shutdown`. */ int tb_width(void); int tb_height(void); /* Clear the internal back buffer using `TB_DEFAULT` or the attributes set by * `tb_set_clear_attrs`. */ int tb_clear(void); int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg); /* Synchronize the internal back buffer with the terminal by writing to tty. */ int tb_present(void); /* Clear the internal front buffer effectively forcing a complete re-render of * the back buffer to the tty. It is not necessary to call this under normal * circumstances. */ int tb_invalidate(void); /* Set the position of the cursor. Upper-left cell is (0, 0). */ int tb_set_cursor(int cx, int cy); int tb_hide_cursor(void); /* Set cell contents in the internal back buffer at the specified position. * * Use `tb_set_cell_ex` for rendering grapheme clusters (e.g., combining * diacritical marks). * * Calling `tb_set_cell(x, y, ch, fg, bg)` is equivalent to * `tb_set_cell_ex(x, y, &ch, 1, fg, bg)`. * * `tb_extend_cell` is a shortcut for appending 1 codepoint to `tb_cell.ech`. * * Non-printable (`iswprint(3)`) codepoints are replaced with `U+FFFD` at render * time. */ int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg); int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg); int tb_extend_cell(int x, int y, uint32_t ch); /* Return a pointer to the cell at the specified position. * * Cell memory may be invalid or freed after subsequent library calls, so * callers must copy any data that they need to persist across calls. Modifying * cell memory results in undefined behavior. * * Callers may use pointer math to access cells relative to the requested one. * The cell grid memory layout is a contiguous array indexable by the expression * `(y * width) + x`. * * If `back` is non-zero, return cell from the internal back buffer. Otherwise, * return cell from the front buffer. Note the front buffer is updated on each * call to `tb_present`, whereas the back buffer is updated immediately by * `tb_set_cell` and other functions that modify cell contents. * * If the position is invalid, `TB_ERR_OUT_OF_BOUNDS` is returned. */ int tb_get_cell(int x, int y, int back, struct tb_cell **cell); /* Set the input mode. Termbox has two input modes: * * 1. `TB_INPUT_ESC` * When escape (`\x1b`) is in the buffer and there's no match for an escape * sequence, a key event for `TB_KEY_ESC` is returned. * * 2. `TB_INPUT_ALT` * When escape (`\x1b`) is in the buffer and there's no match for an escape * sequence, the next keyboard event is returned with a `TB_MOD_ALT` * modifier. * * You can also apply `TB_INPUT_MOUSE` via bitwise OR operation to either of the * modes (e.g., `TB_INPUT_ESC | TB_INPUT_MOUSE`) to receive `TB_EVENT_MOUSE` * events. If none of the main two modes were set, but the mouse mode was, * `TB_INPUT_ESC` is used. If for some reason you've decided to use * `TB_INPUT_ESC | TB_INPUT_ALT`, it will behave as if only `TB_INPUT_ESC` was * selected. * * If mode is `TB_INPUT_CURRENT`, return the current input mode. * * The default input mode is `TB_INPUT_ESC`. */ int tb_set_input_mode(int mode); /* Set the output mode. Termbox has multiple output modes: * * 1. `TB_OUTPUT_NORMAL` => [0..8] * * This mode provides 8 different colors: * `TB_BLACK`, `TB_RED`, `TB_GREEN`, `TB_YELLOW`, * `TB_BLUE`, `TB_MAGENTA`, `TB_CYAN`, `TB_WHITE` * * Plus `TB_DEFAULT` which skips sending a color code (i.e., uses the * terminal's default color). * * Colors (including `TB_DEFAULT`) may be bitwise OR'd with attributes: * `TB_BOLD`, `TB_UNDERLINE`, `TB_REVERSE`, `TB_ITALIC`, `TB_BLINK`, * `TB_BRIGHT`, `TB_DIM` * * The following style attributes are also available if compiled with * `TB_OPT_ATTR_W` set to 64: * `TB_STRIKEOUT`, `TB_UNDERLINE_2`, `TB_OVERLINE`, `TB_INVISIBLE` * * As in all modes, the value 0 is interpreted as `TB_DEFAULT` for * convenience. * * Some notes: `TB_REVERSE` and `TB_BRIGHT` can be applied as either `fg` or * `bg` attributes for the same effect. The rest of the attributes apply to * `fg` only and are ignored as `bg` attributes. * * Example usage: `tb_set_cell(x, y, '@', TB_BLACK | TB_BOLD, TB_RED)` * * 2. `TB_OUTPUT_256` => [0..255] + `TB_HI_BLACK` * * In this mode you get 256 distinct colors (plus default): * 0x00 (1): `TB_DEFAULT` * `TB_HI_BLACK` (1): `TB_BLACK` in `TB_OUTPUT_NORMAL` * 0x01..0x07 (7): the next 7 colors as in `TB_OUTPUT_NORMAL` * 0x08..0x0f (8): bright versions of the above * 0x10..0xe7 (216): 216 different colors * 0xe8..0xff (24): 24 different shades of gray * * All `TB_*` style attributes except `TB_BRIGHT` may be bitwise OR'd as in * `TB_OUTPUT_NORMAL`. * * Note `TB_HI_BLACK` must be used for black, as 0x00 represents default. * * 3. `TB_OUTPUT_216` => [0..216] * * This mode supports the 216-color range of `TB_OUTPUT_256` only, but you * don't need to provide an offset: * 0x00 (1): `TB_DEFAULT` * 0x01..0xd8 (216): 216 different colors * * 4. `TB_OUTPUT_GRAYSCALE` => [0..24] * * This mode supports the 24-color range of `TB_OUTPUT_256` only, but you * don't need to provide an offset: * 0x00 (1): `TB_DEFAULT` * 0x01..0x18 (24): 24 different shades of gray * * 5. `TB_OUTPUT_TRUECOLOR` => [0x000000..0xffffff] + `TB_HI_BLACK` * * This mode provides 24-bit color on supported terminals. The format is * 0xRRGGBB. * * All `TB_*` style attributes except `TB_BRIGHT` may be bitwise OR'd as in * `TB_OUTPUT_NORMAL`. * * Note `TB_HI_BLACK` must be used for black, as 0x000000 represents default. * * To use the terminal default color (i.e., to not send an escape code), pass * `TB_DEFAULT`. For convenience, the value 0 is interpreted as `TB_DEFAULT` in * all modes. * * Note, cell attributes persist after switching output modes. Any translation * between, for example, `TB_OUTPUT_NORMAL`'s `TB_RED` and * `TB_OUTPUT_TRUECOLOR`'s 0xff0000 must be performed by the caller. Also note * that cells previously rendered in one mode may persist unchanged until the * front buffer is cleared (such as after a resize event) at which point it will * be re-interpreted and flushed according to the current mode. Callers may * invoke `tb_invalidate` if it is desirable to immediately re-interpret and * flush the entire screen according to the current mode. * * Note, not all terminals support all output modes, especially beyond * `TB_OUTPUT_NORMAL`. There is also no very reliable way to determine color * support dynamically. If portability is desired, callers are recommended to * use `TB_OUTPUT_NORMAL` or make output mode end-user configurable. The same * advice applies to style attributes. * * If mode is `TB_OUTPUT_CURRENT`, return the current output mode. * * The default output mode is `TB_OUTPUT_NORMAL`. */ int tb_set_output_mode(int mode); /* Wait for an event up to `timeout_ms` milliseconds and populate `event` with * it. If no event is available within the timeout period, `TB_ERR_NO_EVENT` * is returned. On a resize event, the underlying `select(2)` call may be * interrupted, yielding a return code of `TB_ERR_POLL`. In this case, you may * check `errno` via `tb_last_errno`. If it's `EINTR`, you may elect to ignore * that and call `tb_peek_event` again. */ int tb_peek_event(struct tb_event *event, int timeout_ms); /* Same as `tb_peek_event` except no timeout. */ int tb_poll_event(struct tb_event *event); /* Internal termbox fds that can be used with `poll(2)`, `select(2)`, etc. * externally. Callers must invoke `tb_poll_event` or `tb_peek_event` if * fds become readable. */ int tb_get_fds(int *ttyfd, int *resizefd); /* Print and printf functions. Specify param `out_w` to determine width of * printed string. Strings are interpreted as UTF-8. * * Non-printable characters (`iswprint(3)`) and truncated UTF-8 byte sequences * are replaced with U+FFFD. * * Newlines (`\n`) are supported with the caveat that `out_w` will return the * width of the string as if it were on a single line. * * If the starting coordinate is out of bounds, `TB_ERR_OUT_OF_BOUNDS` is * returned. If the starting coordinate is in bounds, but goes out of bounds, * then the out-of-bounds portions of the string are ignored. * * For finer control, use `tb_set_cell`. */ int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str); int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, ...); int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, const char *str); int tb_printf_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, const char *fmt, ...); /* Send raw bytes to terminal. */ int tb_send(const char *buf, size_t nbuf); int tb_sendf(const char *fmt, ...); /* Deprecated. Set custom callbacks. `fn_type` is one of `TB_FUNC_*` constants, * `fn` is a compatible function pointer, or NULL to clear. * * `TB_FUNC_EXTRACT_PRE`: * If specified, invoke this function BEFORE termbox tries to extract any * escape sequences from the input buffer. * * `TB_FUNC_EXTRACT_POST`: * If specified, invoke this function AFTER termbox tries (and fails) to * extract any escape sequences from the input buffer. */ int tb_set_func(int fn_type, int (*fn)(struct tb_event *, size_t *)); /* Return byte length of codepoint given first byte of UTF-8 sequence (1-6). */ int tb_utf8_char_length(char c); /* Convert UTF-8 null-terminated byte sequence to UTF-32 codepoint. * * If `c` is an empty C string, return 0. `out` is left unchanged. * * If a null byte is encountered in the middle of the codepoint, return a * negative number indicating how many bytes were processed. `out` is left * unchanged. * * Otherwise, return byte length of codepoint (1-6). */ int tb_utf8_char_to_unicode(uint32_t *out, const char *c); /* Convert UTF-32 codepoint to UTF-8 null-terminated byte sequence. * * `out` must be char[7] or greater. Return byte length of codepoint (1-6). */ int tb_utf8_unicode_to_char(char *out, uint32_t c); /* Library utility functions */ int tb_last_errno(void); const char *tb_strerror(int err); struct tb_cell *tb_cell_buffer(void); // Deprecated int tb_has_truecolor(void); int tb_has_egc(void); int tb_attr_width(void); const char *tb_version(void); int tb_iswprint(uint32_t ch); int tb_wcwidth(uint32_t ch); void tb_set_log_function(int (*fn)(const char *fmt, ...)); /* Deprecation notice! * * The following will be removed in version 3.x (ABI version 3): * * TB_256_BLACK (use TB_HI_BLACK) * TB_OPT_TRUECOLOR (use TB_OPT_ATTR_W) * TB_TRUECOLOR_BOLD (use TB_BOLD) * TB_TRUECOLOR_UNDERLINE (use TB_UNDERLINE) * TB_TRUECOLOR_REVERSE (use TB_REVERSE) * TB_TRUECOLOR_ITALIC (use TB_ITALIC) * TB_TRUECOLOR_BLINK (use TB_BLINK) * TB_TRUECOLOR_BLACK (use TB_HI_BLACK) * tb_cell_buffer * tb_set_func * TB_FUNC_EXTRACT_PRE * TB_FUNC_EXTRACT_POST */ #ifdef __cplusplus } #endif #endif // TERMBOX_H_INCL #ifdef TB_IMPL #define if_err_return(rv, expr) \ if (((rv) = (expr)) != TB_OK) return (rv) #define if_err_break(rv, expr) \ if (((rv) = (expr)) != TB_OK) break #define if_ok_return(rv, expr) \ if (((rv) = (expr)) == TB_OK) return (rv) #define if_ok_or_need_more_return(rv, expr) \ if (((rv) = (expr)) == TB_OK || (rv) == TB_ERR_NEED_MORE) return (rv) #define send_literal(rv, a) \ if_err_return((rv), bytebuf_nputs(&global.out, (a), sizeof(a) - 1)) #define send_num(rv, nbuf, n) \ if_err_return((rv), \ bytebuf_nputs(&global.out, (nbuf), convert_num((n), (nbuf)))) #define snprintf_or_return(rv, str, sz, fmt, ...) \ do { \ (rv) = snprintf((str), (sz), (fmt), __VA_ARGS__); \ if ((rv) < 0 || (rv) >= (int)(sz)) return TB_ERR; \ } while (0) #define if_not_init_return() \ if (!global.initialized) return TB_ERR_NOT_INIT struct bytebuf { char *buf; size_t len; size_t cap; }; struct cellbuf { int width; int height; struct tb_cell *cells; }; struct cap_trie { char c; struct cap_trie *children; size_t nchildren; int is_leaf; uint16_t key; uint8_t mod; }; struct tb_global { int ttyfd; int rfd; int wfd; int ttyfd_open; int resize_pipefd[2]; int width; int height; int cursor_x; int cursor_y; int last_x; int last_y; uintattr_t fg; uintattr_t bg; uintattr_t last_fg; uintattr_t last_bg; int input_mode; int output_mode; char *terminfo; size_t nterminfo; const char *caps[TB_CAP__COUNT]; struct cap_trie cap_trie; struct bytebuf in; struct bytebuf out; struct cellbuf back; struct cellbuf front; struct termios orig_tios; int has_orig_tios; int last_errno; int initialized; int (*fn_extract_esc_pre)(struct tb_event *, size_t *); int (*fn_extract_esc_post)(struct tb_event *, size_t *); int (*fn_print_log_msg)(const char *fmt, ...); char errbuf[1024]; }; static struct tb_global global = {0}; /* BEGIN codegen c */ /* Produced by ./codegen.sh on Tue, 03 Sep 2024 04:17:48 +0000 */ static const int16_t terminfo_cap_indexes[] = { 66, // kf1 (TB_CAP_F1) 68, // kf2 (TB_CAP_F2) 69, // kf3 (TB_CAP_F3) 70, // kf4 (TB_CAP_F4) 71, // kf5 (TB_CAP_F5) 72, // kf6 (TB_CAP_F6) 73, // kf7 (TB_CAP_F7) 74, // kf8 (TB_CAP_F8) 75, // kf9 (TB_CAP_F9) 67, // kf10 (TB_CAP_F10) 216, // kf11 (TB_CAP_F11) 217, // kf12 (TB_CAP_F12) 77, // kich1 (TB_CAP_INSERT) 59, // kdch1 (TB_CAP_DELETE) 76, // khome (TB_CAP_HOME) 164, // kend (TB_CAP_END) 82, // kpp (TB_CAP_PGUP) 81, // knp (TB_CAP_PGDN) 87, // kcuu1 (TB_CAP_ARROW_UP) 61, // kcud1 (TB_CAP_ARROW_DOWN) 79, // kcub1 (TB_CAP_ARROW_LEFT) 83, // kcuf1 (TB_CAP_ARROW_RIGHT) 148, // kcbt (TB_CAP_BACK_TAB) 28, // smcup (TB_CAP_ENTER_CA) 40, // rmcup (TB_CAP_EXIT_CA) 16, // cnorm (TB_CAP_SHOW_CURSOR) 13, // civis (TB_CAP_HIDE_CURSOR) 5, // clear (TB_CAP_CLEAR_SCREEN) 39, // sgr0 (TB_CAP_SGR0) 36, // smul (TB_CAP_UNDERLINE) 27, // bold (TB_CAP_BOLD) 26, // blink (TB_CAP_BLINK) 311, // sitm (TB_CAP_ITALIC) 34, // rev (TB_CAP_REVERSE) 89, // smkx (TB_CAP_ENTER_KEYPAD) 88, // rmkx (TB_CAP_EXIT_KEYPAD) 30, // dim (TB_CAP_DIM) 32, // invis (TB_CAP_INVISIBLE) }; // xterm static const char *xterm_caps[] = { "\033OP", // kf1 (TB_CAP_F1) "\033OQ", // kf2 (TB_CAP_F2) "\033OR", // kf3 (TB_CAP_F3) "\033OS", // kf4 (TB_CAP_F4) "\033[15~", // kf5 (TB_CAP_F5) "\033[17~", // kf6 (TB_CAP_F6) "\033[18~", // kf7 (TB_CAP_F7) "\033[19~", // kf8 (TB_CAP_F8) "\033[20~", // kf9 (TB_CAP_F9) "\033[21~", // kf10 (TB_CAP_F10) "\033[23~", // kf11 (TB_CAP_F11) "\033[24~", // kf12 (TB_CAP_F12) "\033[2~", // kich1 (TB_CAP_INSERT) "\033[3~", // kdch1 (TB_CAP_DELETE) "\033OH", // khome (TB_CAP_HOME) "\033OF", // kend (TB_CAP_END) "\033[5~", // kpp (TB_CAP_PGUP) "\033[6~", // knp (TB_CAP_PGDN) "\033OA", // kcuu1 (TB_CAP_ARROW_UP) "\033OB", // kcud1 (TB_CAP_ARROW_DOWN) "\033OD", // kcub1 (TB_CAP_ARROW_LEFT) "\033OC", // kcuf1 (TB_CAP_ARROW_RIGHT) "\033[Z", // kcbt (TB_CAP_BACK_TAB) "\033[?1049h\033[22;0;0t", // smcup (TB_CAP_ENTER_CA) "\033[?1049l\033[23;0;0t", // rmcup (TB_CAP_EXIT_CA) "\033[?12l\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) "\033(B\033[m", // sgr0 (TB_CAP_SGR0) "\033[4m", // smul (TB_CAP_UNDERLINE) "\033[1m", // bold (TB_CAP_BOLD) "\033[5m", // blink (TB_CAP_BLINK) "\033[3m", // sitm (TB_CAP_ITALIC) "\033[7m", // rev (TB_CAP_REVERSE) "\033[?1h\033=", // smkx (TB_CAP_ENTER_KEYPAD) "\033[?1l\033>", // rmkx (TB_CAP_EXIT_KEYPAD) "\033[2m", // dim (TB_CAP_DIM) "\033[8m", // invis (TB_CAP_INVISIBLE) }; // linux static const char *linux_caps[] = { "\033[[A", // kf1 (TB_CAP_F1) "\033[[B", // kf2 (TB_CAP_F2) "\033[[C", // kf3 (TB_CAP_F3) "\033[[D", // kf4 (TB_CAP_F4) "\033[[E", // kf5 (TB_CAP_F5) "\033[17~", // kf6 (TB_CAP_F6) "\033[18~", // kf7 (TB_CAP_F7) "\033[19~", // kf8 (TB_CAP_F8) "\033[20~", // kf9 (TB_CAP_F9) "\033[21~", // kf10 (TB_CAP_F10) "\033[23~", // kf11 (TB_CAP_F11) "\033[24~", // kf12 (TB_CAP_F12) "\033[2~", // kich1 (TB_CAP_INSERT) "\033[3~", // kdch1 (TB_CAP_DELETE) "\033[1~", // khome (TB_CAP_HOME) "\033[4~", // kend (TB_CAP_END) "\033[5~", // kpp (TB_CAP_PGUP) "\033[6~", // knp (TB_CAP_PGDN) "\033[A", // kcuu1 (TB_CAP_ARROW_UP) "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) "\033\011", // kcbt (TB_CAP_BACK_TAB) "", // smcup (TB_CAP_ENTER_CA) "", // rmcup (TB_CAP_EXIT_CA) "\033[?25h\033[?0c", // cnorm (TB_CAP_SHOW_CURSOR) "\033[?25l\033[?1c", // civis (TB_CAP_HIDE_CURSOR) "\033[H\033[J", // clear (TB_CAP_CLEAR_SCREEN) "\033[m\017", // sgr0 (TB_CAP_SGR0) "\033[4m", // smul (TB_CAP_UNDERLINE) "\033[1m", // bold (TB_CAP_BOLD) "\033[5m", // blink (TB_CAP_BLINK) "", // sitm (TB_CAP_ITALIC) "\033[7m", // rev (TB_CAP_REVERSE) "", // smkx (TB_CAP_ENTER_KEYPAD) "", // rmkx (TB_CAP_EXIT_KEYPAD) "\033[2m", // dim (TB_CAP_DIM) "", // invis (TB_CAP_INVISIBLE) }; // screen static const char *screen_caps[] = { "\033OP", // kf1 (TB_CAP_F1) "\033OQ", // kf2 (TB_CAP_F2) "\033OR", // kf3 (TB_CAP_F3) "\033OS", // kf4 (TB_CAP_F4) "\033[15~", // kf5 (TB_CAP_F5) "\033[17~", // kf6 (TB_CAP_F6) "\033[18~", // kf7 (TB_CAP_F7) "\033[19~", // kf8 (TB_CAP_F8) "\033[20~", // kf9 (TB_CAP_F9) "\033[21~", // kf10 (TB_CAP_F10) "\033[23~", // kf11 (TB_CAP_F11) "\033[24~", // kf12 (TB_CAP_F12) "\033[2~", // kich1 (TB_CAP_INSERT) "\033[3~", // kdch1 (TB_CAP_DELETE) "\033[1~", // khome (TB_CAP_HOME) "\033[4~", // kend (TB_CAP_END) "\033[5~", // kpp (TB_CAP_PGUP) "\033[6~", // knp (TB_CAP_PGDN) "\033OA", // kcuu1 (TB_CAP_ARROW_UP) "\033OB", // kcud1 (TB_CAP_ARROW_DOWN) "\033OD", // kcub1 (TB_CAP_ARROW_LEFT) "\033OC", // kcuf1 (TB_CAP_ARROW_RIGHT) "\033[Z", // kcbt (TB_CAP_BACK_TAB) "\033[?1049h", // smcup (TB_CAP_ENTER_CA) "\033[?1049l", // rmcup (TB_CAP_EXIT_CA) "\033[34h\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) "\033[H\033[J", // clear (TB_CAP_CLEAR_SCREEN) "\033[m\017", // sgr0 (TB_CAP_SGR0) "\033[4m", // smul (TB_CAP_UNDERLINE) "\033[1m", // bold (TB_CAP_BOLD) "\033[5m", // blink (TB_CAP_BLINK) "", // sitm (TB_CAP_ITALIC) "\033[7m", // rev (TB_CAP_REVERSE) "\033[?1h\033=", // smkx (TB_CAP_ENTER_KEYPAD) "\033[?1l\033>", // rmkx (TB_CAP_EXIT_KEYPAD) "\033[2m", // dim (TB_CAP_DIM) "", // invis (TB_CAP_INVISIBLE) }; // rxvt-256color static const char *rxvt_256color_caps[] = { "\033[11~", // kf1 (TB_CAP_F1) "\033[12~", // kf2 (TB_CAP_F2) "\033[13~", // kf3 (TB_CAP_F3) "\033[14~", // kf4 (TB_CAP_F4) "\033[15~", // kf5 (TB_CAP_F5) "\033[17~", // kf6 (TB_CAP_F6) "\033[18~", // kf7 (TB_CAP_F7) "\033[19~", // kf8 (TB_CAP_F8) "\033[20~", // kf9 (TB_CAP_F9) "\033[21~", // kf10 (TB_CAP_F10) "\033[23~", // kf11 (TB_CAP_F11) "\033[24~", // kf12 (TB_CAP_F12) "\033[2~", // kich1 (TB_CAP_INSERT) "\033[3~", // kdch1 (TB_CAP_DELETE) "\033[7~", // khome (TB_CAP_HOME) "\033[8~", // kend (TB_CAP_END) "\033[5~", // kpp (TB_CAP_PGUP) "\033[6~", // knp (TB_CAP_PGDN) "\033[A", // kcuu1 (TB_CAP_ARROW_UP) "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) "\033[Z", // kcbt (TB_CAP_BACK_TAB) "\0337\033[?47h", // smcup (TB_CAP_ENTER_CA) "\033[2J\033[?47l\0338", // rmcup (TB_CAP_EXIT_CA) "\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) "\033[m\017", // sgr0 (TB_CAP_SGR0) "\033[4m", // smul (TB_CAP_UNDERLINE) "\033[1m", // bold (TB_CAP_BOLD) "\033[5m", // blink (TB_CAP_BLINK) "", // sitm (TB_CAP_ITALIC) "\033[7m", // rev (TB_CAP_REVERSE) "\033=", // smkx (TB_CAP_ENTER_KEYPAD) "\033>", // rmkx (TB_CAP_EXIT_KEYPAD) "", // dim (TB_CAP_DIM) "", // invis (TB_CAP_INVISIBLE) }; // rxvt-unicode static const char *rxvt_unicode_caps[] = { "\033[11~", // kf1 (TB_CAP_F1) "\033[12~", // kf2 (TB_CAP_F2) "\033[13~", // kf3 (TB_CAP_F3) "\033[14~", // kf4 (TB_CAP_F4) "\033[15~", // kf5 (TB_CAP_F5) "\033[17~", // kf6 (TB_CAP_F6) "\033[18~", // kf7 (TB_CAP_F7) "\033[19~", // kf8 (TB_CAP_F8) "\033[20~", // kf9 (TB_CAP_F9) "\033[21~", // kf10 (TB_CAP_F10) "\033[23~", // kf11 (TB_CAP_F11) "\033[24~", // kf12 (TB_CAP_F12) "\033[2~", // kich1 (TB_CAP_INSERT) "\033[3~", // kdch1 (TB_CAP_DELETE) "\033[7~", // khome (TB_CAP_HOME) "\033[8~", // kend (TB_CAP_END) "\033[5~", // kpp (TB_CAP_PGUP) "\033[6~", // knp (TB_CAP_PGDN) "\033[A", // kcuu1 (TB_CAP_ARROW_UP) "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) "\033[Z", // kcbt (TB_CAP_BACK_TAB) "\033[?1049h", // smcup (TB_CAP_ENTER_CA) "\033[r\033[?1049l", // rmcup (TB_CAP_EXIT_CA) "\033[?12l\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) "\033[m\033(B", // sgr0 (TB_CAP_SGR0) "\033[4m", // smul (TB_CAP_UNDERLINE) "\033[1m", // bold (TB_CAP_BOLD) "\033[5m", // blink (TB_CAP_BLINK) "\033[3m", // sitm (TB_CAP_ITALIC) "\033[7m", // rev (TB_CAP_REVERSE) "\033=", // smkx (TB_CAP_ENTER_KEYPAD) "\033>", // rmkx (TB_CAP_EXIT_KEYPAD) "", // dim (TB_CAP_DIM) "", // invis (TB_CAP_INVISIBLE) }; // Eterm static const char *eterm_caps[] = { "\033[11~", // kf1 (TB_CAP_F1) "\033[12~", // kf2 (TB_CAP_F2) "\033[13~", // kf3 (TB_CAP_F3) "\033[14~", // kf4 (TB_CAP_F4) "\033[15~", // kf5 (TB_CAP_F5) "\033[17~", // kf6 (TB_CAP_F6) "\033[18~", // kf7 (TB_CAP_F7) "\033[19~", // kf8 (TB_CAP_F8) "\033[20~", // kf9 (TB_CAP_F9) "\033[21~", // kf10 (TB_CAP_F10) "\033[23~", // kf11 (TB_CAP_F11) "\033[24~", // kf12 (TB_CAP_F12) "\033[2~", // kich1 (TB_CAP_INSERT) "\033[3~", // kdch1 (TB_CAP_DELETE) "\033[7~", // khome (TB_CAP_HOME) "\033[8~", // kend (TB_CAP_END) "\033[5~", // kpp (TB_CAP_PGUP) "\033[6~", // knp (TB_CAP_PGDN) "\033[A", // kcuu1 (TB_CAP_ARROW_UP) "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) "", // kcbt (TB_CAP_BACK_TAB) "\0337\033[?47h", // smcup (TB_CAP_ENTER_CA) "\033[2J\033[?47l\0338", // rmcup (TB_CAP_EXIT_CA) "\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) "\033[m\017", // sgr0 (TB_CAP_SGR0) "\033[4m", // smul (TB_CAP_UNDERLINE) "\033[1m", // bold (TB_CAP_BOLD) "\033[5m", // blink (TB_CAP_BLINK) "", // sitm (TB_CAP_ITALIC) "\033[7m", // rev (TB_CAP_REVERSE) "", // smkx (TB_CAP_ENTER_KEYPAD) "", // rmkx (TB_CAP_EXIT_KEYPAD) "", // dim (TB_CAP_DIM) "", // invis (TB_CAP_INVISIBLE) }; static struct { const char *name; const char **caps; const char *alias; } builtin_terms[] = { {"xterm", xterm_caps, "" }, {"linux", linux_caps, "" }, {"screen", screen_caps, "tmux"}, {"rxvt-256color", rxvt_256color_caps, "" }, {"rxvt-unicode", rxvt_unicode_caps, "rxvt"}, {"Eterm", eterm_caps, "" }, {NULL, NULL, NULL }, }; /* END codegen c */ static struct { const char *cap; const uint16_t key; const uint8_t mod; } builtin_mod_caps[] = { // xterm arrows {"\x1b[1;2A", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, {"\x1b[1;3A", TB_KEY_ARROW_UP, TB_MOD_ALT }, {"\x1b[1;4A", TB_KEY_ARROW_UP, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[1;5A", TB_KEY_ARROW_UP, TB_MOD_CTRL }, {"\x1b[1;6A", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[1;7A", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8A", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[1;2B", TB_KEY_ARROW_DOWN, TB_MOD_SHIFT }, {"\x1b[1;3B", TB_KEY_ARROW_DOWN, TB_MOD_ALT }, {"\x1b[1;4B", TB_KEY_ARROW_DOWN, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[1;5B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL }, {"\x1b[1;6B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[1;7B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[1;2C", TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT }, {"\x1b[1;3C", TB_KEY_ARROW_RIGHT, TB_MOD_ALT }, {"\x1b[1;4C", TB_KEY_ARROW_RIGHT, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[1;5C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL }, {"\x1b[1;6C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[1;7C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[1;2D", TB_KEY_ARROW_LEFT, TB_MOD_SHIFT }, {"\x1b[1;3D", TB_KEY_ARROW_LEFT, TB_MOD_ALT }, {"\x1b[1;4D", TB_KEY_ARROW_LEFT, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[1;5D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL }, {"\x1b[1;6D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[1;7D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, // xterm keys {"\x1b[1;2H", TB_KEY_HOME, TB_MOD_SHIFT }, {"\x1b[1;3H", TB_KEY_HOME, TB_MOD_ALT }, {"\x1b[1;4H", TB_KEY_HOME, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[1;5H", TB_KEY_HOME, TB_MOD_CTRL }, {"\x1b[1;6H", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[1;7H", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8H", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[1;2F", TB_KEY_END, TB_MOD_SHIFT }, {"\x1b[1;3F", TB_KEY_END, TB_MOD_ALT }, {"\x1b[1;4F", TB_KEY_END, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[1;5F", TB_KEY_END, TB_MOD_CTRL }, {"\x1b[1;6F", TB_KEY_END, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[1;7F", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8F", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[2;2~", TB_KEY_INSERT, TB_MOD_SHIFT }, {"\x1b[2;3~", TB_KEY_INSERT, TB_MOD_ALT }, {"\x1b[2;4~", TB_KEY_INSERT, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[2;5~", TB_KEY_INSERT, TB_MOD_CTRL }, {"\x1b[2;6~", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[2;7~", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[2;8~", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[3;2~", TB_KEY_DELETE, TB_MOD_SHIFT }, {"\x1b[3;3~", TB_KEY_DELETE, TB_MOD_ALT }, {"\x1b[3;4~", TB_KEY_DELETE, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[3;5~", TB_KEY_DELETE, TB_MOD_CTRL }, {"\x1b[3;6~", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[3;7~", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[3;8~", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[5;2~", TB_KEY_PGUP, TB_MOD_SHIFT }, {"\x1b[5;3~", TB_KEY_PGUP, TB_MOD_ALT }, {"\x1b[5;4~", TB_KEY_PGUP, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[5;5~", TB_KEY_PGUP, TB_MOD_CTRL }, {"\x1b[5;6~", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[5;7~", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[5;8~", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[6;2~", TB_KEY_PGDN, TB_MOD_SHIFT }, {"\x1b[6;3~", TB_KEY_PGDN, TB_MOD_ALT }, {"\x1b[6;4~", TB_KEY_PGDN, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[6;5~", TB_KEY_PGDN, TB_MOD_CTRL }, {"\x1b[6;6~", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[6;7~", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[6;8~", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[1;2P", TB_KEY_F1, TB_MOD_SHIFT }, {"\x1b[1;3P", TB_KEY_F1, TB_MOD_ALT }, {"\x1b[1;4P", TB_KEY_F1, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[1;5P", TB_KEY_F1, TB_MOD_CTRL }, {"\x1b[1;6P", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[1;7P", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8P", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[1;2Q", TB_KEY_F2, TB_MOD_SHIFT }, {"\x1b[1;3Q", TB_KEY_F2, TB_MOD_ALT }, {"\x1b[1;4Q", TB_KEY_F2, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[1;5Q", TB_KEY_F2, TB_MOD_CTRL }, {"\x1b[1;6Q", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[1;7Q", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8Q", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[1;2R", TB_KEY_F3, TB_MOD_SHIFT }, {"\x1b[1;3R", TB_KEY_F3, TB_MOD_ALT }, {"\x1b[1;4R", TB_KEY_F3, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[1;5R", TB_KEY_F3, TB_MOD_CTRL }, {"\x1b[1;6R", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[1;7R", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8R", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[1;2S", TB_KEY_F4, TB_MOD_SHIFT }, {"\x1b[1;3S", TB_KEY_F4, TB_MOD_ALT }, {"\x1b[1;4S", TB_KEY_F4, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[1;5S", TB_KEY_F4, TB_MOD_CTRL }, {"\x1b[1;6S", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[1;7S", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8S", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[15;2~", TB_KEY_F5, TB_MOD_SHIFT }, {"\x1b[15;3~", TB_KEY_F5, TB_MOD_ALT }, {"\x1b[15;4~", TB_KEY_F5, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[15;5~", TB_KEY_F5, TB_MOD_CTRL }, {"\x1b[15;6~", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[15;7~", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[15;8~", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[17;2~", TB_KEY_F6, TB_MOD_SHIFT }, {"\x1b[17;3~", TB_KEY_F6, TB_MOD_ALT }, {"\x1b[17;4~", TB_KEY_F6, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[17;5~", TB_KEY_F6, TB_MOD_CTRL }, {"\x1b[17;6~", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[17;7~", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[17;8~", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[18;2~", TB_KEY_F7, TB_MOD_SHIFT }, {"\x1b[18;3~", TB_KEY_F7, TB_MOD_ALT }, {"\x1b[18;4~", TB_KEY_F7, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[18;5~", TB_KEY_F7, TB_MOD_CTRL }, {"\x1b[18;6~", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[18;7~", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[18;8~", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[19;2~", TB_KEY_F8, TB_MOD_SHIFT }, {"\x1b[19;3~", TB_KEY_F8, TB_MOD_ALT }, {"\x1b[19;4~", TB_KEY_F8, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[19;5~", TB_KEY_F8, TB_MOD_CTRL }, {"\x1b[19;6~", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[19;7~", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[19;8~", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[20;2~", TB_KEY_F9, TB_MOD_SHIFT }, {"\x1b[20;3~", TB_KEY_F9, TB_MOD_ALT }, {"\x1b[20;4~", TB_KEY_F9, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[20;5~", TB_KEY_F9, TB_MOD_CTRL }, {"\x1b[20;6~", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[20;7~", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[20;8~", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[21;2~", TB_KEY_F10, TB_MOD_SHIFT }, {"\x1b[21;3~", TB_KEY_F10, TB_MOD_ALT }, {"\x1b[21;4~", TB_KEY_F10, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[21;5~", TB_KEY_F10, TB_MOD_CTRL }, {"\x1b[21;6~", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[21;7~", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[21;8~", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[23;2~", TB_KEY_F11, TB_MOD_SHIFT }, {"\x1b[23;3~", TB_KEY_F11, TB_MOD_ALT }, {"\x1b[23;4~", TB_KEY_F11, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[23;5~", TB_KEY_F11, TB_MOD_CTRL }, {"\x1b[23;6~", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[23;7~", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[23;8~", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[24;2~", TB_KEY_F12, TB_MOD_SHIFT }, {"\x1b[24;3~", TB_KEY_F12, TB_MOD_ALT }, {"\x1b[24;4~", TB_KEY_F12, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[24;5~", TB_KEY_F12, TB_MOD_CTRL }, {"\x1b[24;6~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[24;7~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[24;8~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, // rxvt arrows {"\x1b[a", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, {"\x1b\x1b[A", TB_KEY_ARROW_UP, TB_MOD_ALT }, {"\x1b\x1b[a", TB_KEY_ARROW_UP, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1bOa", TB_KEY_ARROW_UP, TB_MOD_CTRL }, {"\x1b\x1bOa", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[b", TB_KEY_ARROW_DOWN, TB_MOD_SHIFT }, {"\x1b\x1b[B", TB_KEY_ARROW_DOWN, TB_MOD_ALT }, {"\x1b\x1b[b", TB_KEY_ARROW_DOWN, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1bOb", TB_KEY_ARROW_DOWN, TB_MOD_CTRL }, {"\x1b\x1bOb", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[c", TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT }, {"\x1b\x1b[C", TB_KEY_ARROW_RIGHT, TB_MOD_ALT }, {"\x1b\x1b[c", TB_KEY_ARROW_RIGHT, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1bOc", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL }, {"\x1b\x1bOc", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[d", TB_KEY_ARROW_LEFT, TB_MOD_SHIFT }, {"\x1b\x1b[D", TB_KEY_ARROW_LEFT, TB_MOD_ALT }, {"\x1b\x1b[d", TB_KEY_ARROW_LEFT, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1bOd", TB_KEY_ARROW_LEFT, TB_MOD_CTRL }, {"\x1b\x1bOd", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, // rxvt keys {"\x1b[7$", TB_KEY_HOME, TB_MOD_SHIFT }, {"\x1b\x1b[7~", TB_KEY_HOME, TB_MOD_ALT }, {"\x1b\x1b[7$", TB_KEY_HOME, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[7^", TB_KEY_HOME, TB_MOD_CTRL }, {"\x1b[7@", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b\x1b[7^", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[7@", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b\x1b[8~", TB_KEY_END, TB_MOD_ALT }, {"\x1b\x1b[8$", TB_KEY_END, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[8^", TB_KEY_END, TB_MOD_CTRL }, {"\x1b\x1b[8^", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[8@", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[8@", TB_KEY_END, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[8$", TB_KEY_END, TB_MOD_SHIFT }, {"\x1b\x1b[2~", TB_KEY_INSERT, TB_MOD_ALT }, {"\x1b\x1b[2$", TB_KEY_INSERT, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[2^", TB_KEY_INSERT, TB_MOD_CTRL }, {"\x1b\x1b[2^", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[2@", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[2@", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[2$", TB_KEY_INSERT, TB_MOD_SHIFT }, {"\x1b\x1b[3~", TB_KEY_DELETE, TB_MOD_ALT }, {"\x1b\x1b[3$", TB_KEY_DELETE, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[3^", TB_KEY_DELETE, TB_MOD_CTRL }, {"\x1b\x1b[3^", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[3@", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[3@", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[3$", TB_KEY_DELETE, TB_MOD_SHIFT }, {"\x1b\x1b[5~", TB_KEY_PGUP, TB_MOD_ALT }, {"\x1b\x1b[5$", TB_KEY_PGUP, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[5^", TB_KEY_PGUP, TB_MOD_CTRL }, {"\x1b\x1b[5^", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[5@", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[5@", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[5$", TB_KEY_PGUP, TB_MOD_SHIFT }, {"\x1b\x1b[6~", TB_KEY_PGDN, TB_MOD_ALT }, {"\x1b\x1b[6$", TB_KEY_PGDN, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[6^", TB_KEY_PGDN, TB_MOD_CTRL }, {"\x1b\x1b[6^", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[6@", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[6@", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[6$", TB_KEY_PGDN, TB_MOD_SHIFT }, {"\x1b\x1b[11~", TB_KEY_F1, TB_MOD_ALT }, {"\x1b\x1b[23~", TB_KEY_F1, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[11^", TB_KEY_F1, TB_MOD_CTRL }, {"\x1b\x1b[11^", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[23^", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[23^", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[23~", TB_KEY_F1, TB_MOD_SHIFT }, {"\x1b\x1b[12~", TB_KEY_F2, TB_MOD_ALT }, {"\x1b\x1b[24~", TB_KEY_F2, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[12^", TB_KEY_F2, TB_MOD_CTRL }, {"\x1b\x1b[12^", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[24^", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[24^", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[24~", TB_KEY_F2, TB_MOD_SHIFT }, {"\x1b\x1b[13~", TB_KEY_F3, TB_MOD_ALT }, {"\x1b\x1b[25~", TB_KEY_F3, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[13^", TB_KEY_F3, TB_MOD_CTRL }, {"\x1b\x1b[13^", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[25^", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[25^", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[25~", TB_KEY_F3, TB_MOD_SHIFT }, {"\x1b\x1b[14~", TB_KEY_F4, TB_MOD_ALT }, {"\x1b\x1b[26~", TB_KEY_F4, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[14^", TB_KEY_F4, TB_MOD_CTRL }, {"\x1b\x1b[14^", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[26^", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[26^", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[26~", TB_KEY_F4, TB_MOD_SHIFT }, {"\x1b\x1b[15~", TB_KEY_F5, TB_MOD_ALT }, {"\x1b\x1b[28~", TB_KEY_F5, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[15^", TB_KEY_F5, TB_MOD_CTRL }, {"\x1b\x1b[15^", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[28^", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[28^", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[28~", TB_KEY_F5, TB_MOD_SHIFT }, {"\x1b\x1b[17~", TB_KEY_F6, TB_MOD_ALT }, {"\x1b\x1b[29~", TB_KEY_F6, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[17^", TB_KEY_F6, TB_MOD_CTRL }, {"\x1b\x1b[17^", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[29^", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[29^", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[29~", TB_KEY_F6, TB_MOD_SHIFT }, {"\x1b\x1b[18~", TB_KEY_F7, TB_MOD_ALT }, {"\x1b\x1b[31~", TB_KEY_F7, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[18^", TB_KEY_F7, TB_MOD_CTRL }, {"\x1b\x1b[18^", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[31^", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[31^", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[31~", TB_KEY_F7, TB_MOD_SHIFT }, {"\x1b\x1b[19~", TB_KEY_F8, TB_MOD_ALT }, {"\x1b\x1b[32~", TB_KEY_F8, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[19^", TB_KEY_F8, TB_MOD_CTRL }, {"\x1b\x1b[19^", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[32^", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[32^", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[32~", TB_KEY_F8, TB_MOD_SHIFT }, {"\x1b\x1b[20~", TB_KEY_F9, TB_MOD_ALT }, {"\x1b\x1b[33~", TB_KEY_F9, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[20^", TB_KEY_F9, TB_MOD_CTRL }, {"\x1b\x1b[20^", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[33^", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[33^", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[33~", TB_KEY_F9, TB_MOD_SHIFT }, {"\x1b\x1b[21~", TB_KEY_F10, TB_MOD_ALT }, {"\x1b\x1b[34~", TB_KEY_F10, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[21^", TB_KEY_F10, TB_MOD_CTRL }, {"\x1b\x1b[21^", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[34^", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[34^", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[34~", TB_KEY_F10, TB_MOD_SHIFT }, {"\x1b\x1b[23~", TB_KEY_F11, TB_MOD_ALT }, {"\x1b\x1b[23$", TB_KEY_F11, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[23^", TB_KEY_F11, TB_MOD_CTRL }, {"\x1b\x1b[23^", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[23@", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[23@", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[23$", TB_KEY_F11, TB_MOD_SHIFT }, {"\x1b\x1b[24~", TB_KEY_F12, TB_MOD_ALT }, {"\x1b\x1b[24$", TB_KEY_F12, TB_MOD_ALT | TB_MOD_SHIFT }, {"\x1b[24^", TB_KEY_F12, TB_MOD_CTRL }, {"\x1b\x1b[24^", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b\x1b[24@", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, {"\x1b[24@", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[24$", TB_KEY_F12, TB_MOD_SHIFT }, // linux console/putty arrows {"\x1b[A", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, {"\x1b[B", TB_KEY_ARROW_DOWN, TB_MOD_SHIFT }, {"\x1b[C", TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT }, {"\x1b[D", TB_KEY_ARROW_LEFT, TB_MOD_SHIFT }, // more putty arrows {"\x1bOA", TB_KEY_ARROW_UP, TB_MOD_CTRL }, {"\x1b\x1bOA", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1bOB", TB_KEY_ARROW_DOWN, TB_MOD_CTRL }, {"\x1b\x1bOB", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1bOC", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL }, {"\x1b\x1bOC", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1bOD", TB_KEY_ARROW_LEFT, TB_MOD_CTRL }, {"\x1b\x1bOD", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, {NULL, 0, 0 }, }; static const unsigned char utf8_length[256] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 1, 1}; static const unsigned char utf8_mask[6] = {0x7f, 0x1f, 0x0f, 0x07, 0x03, 0x01}; static int tb_reset(void); static int tb_printf_inner(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, const char *fmt, va_list vl); static int init_term_attrs(void); static int init_term_caps(void); static int init_cap_trie(void); static int cap_trie_add(const char *cap, uint16_t key, uint8_t mod); static int cap_trie_find(const char *buf, size_t nbuf, struct cap_trie **last, size_t *depth); static int cap_trie_deinit(struct cap_trie *node); static int init_resize_handler(void); static int send_init_escape_codes(void); static int send_clear(void); static int update_term_size(void); static int update_term_size_via_esc(void); static int init_cellbuf(void); static int tb_deinit(void); static int load_terminfo(void); static int load_terminfo_from_path(const char *path, const char *term); static int read_terminfo_path(const char *path); static int parse_terminfo_caps(void); static int load_builtin_caps(void); static const char *get_terminfo_string(int16_t offsets_pos, int16_t offsets_len, int16_t table_pos, int16_t table_size, int16_t index); static int get_terminfo_int16(int offset, int16_t *val); static int wait_event(struct tb_event *event, int timeout); static int extract_event(struct tb_event *event); static int extract_esc(struct tb_event *event); static int extract_esc_user(struct tb_event *event, int is_post); static int extract_esc_cap(struct tb_event *event); static int extract_esc_mouse(struct tb_event *event); static int resize_cellbufs(void); static void handle_resize(int sig); static int send_attr(uintattr_t fg, uintattr_t bg); static int send_sgr(uint32_t fg, uint32_t bg, int fg_is_default, int bg_is_default); static int send_cursor_if(int x, int y); static int send_char(int x, int y, uint32_t ch); static int send_cluster(int x, int y, uint32_t *ch, size_t nch); static int convert_num(uint32_t num, char *buf); static int cell_cmp(struct tb_cell *a, struct tb_cell *b); static int cell_copy(struct tb_cell *dst, struct tb_cell *src); static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg); static int cell_reserve_ech(struct tb_cell *cell, size_t n); static int cell_free(struct tb_cell *cell); static int cellbuf_init(struct cellbuf *c, int w, int h); static int cellbuf_free(struct cellbuf *c); static int cellbuf_clear(struct cellbuf *c); static int cellbuf_get(struct cellbuf *c, int x, int y, struct tb_cell **out); static int cellbuf_in_bounds(struct cellbuf *c, int x, int y); static int cellbuf_resize(struct cellbuf *c, int w, int h); static int bytebuf_puts(struct bytebuf *b, const char *str); static int bytebuf_nputs(struct bytebuf *b, const char *str, size_t nstr); static int bytebuf_shift(struct bytebuf *b, size_t n); static int bytebuf_flush(struct bytebuf *b, int fd); static int bytebuf_reserve(struct bytebuf *b, size_t sz); static int bytebuf_free(struct bytebuf *b); static int tb_iswprint_ex(uint32_t ch, int *width); int tb_init(void) { return tb_init_file("/dev/tty"); } int tb_init_file(const char *path) { if (global.initialized) return TB_ERR_INIT_ALREADY; int ttyfd = open(path, O_RDWR); if (ttyfd < 0) { global.last_errno = errno; return TB_ERR_INIT_OPEN; } global.ttyfd_open = 1; return tb_init_fd(ttyfd); } int tb_init_fd(int ttyfd) { return tb_init_rwfd(ttyfd, ttyfd); } int tb_init_rwfd(int rfd, int wfd) { int rv; tb_reset(); global.ttyfd = isatty(rfd) ? rfd : (isatty(wfd) ? wfd : -1); global.rfd = rfd; global.wfd = wfd; do { if_err_break(rv, init_term_attrs()); if_err_break(rv, init_term_caps()); if_err_break(rv, init_cap_trie()); if_err_break(rv, init_resize_handler()); if_err_break(rv, send_init_escape_codes()); if_err_break(rv, send_clear()); if_err_break(rv, update_term_size()); if_err_break(rv, init_cellbuf()); global.initialized = 1; } while (0); if (rv != TB_OK) tb_deinit(); return rv; } int tb_shutdown(void) { if_not_init_return(); tb_deinit(); return TB_OK; } int tb_width(void) { if_not_init_return(); return global.width; } int tb_height(void) { if_not_init_return(); return global.height; } int tb_clear(void) { if_not_init_return(); return cellbuf_clear(&global.back); } int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg) { if_not_init_return(); global.fg = fg; global.bg = bg; return TB_OK; } int tb_present(void) { if_not_init_return(); int rv; // TODO: Assert global.back.(width,height) == global.front.(width,height) global.last_x = -1; global.last_y = -1; int x, y, i; for (y = 0; y < global.front.height; y++) { for (x = 0; x < global.front.width;) { struct tb_cell *back, *front; if_err_return(rv, cellbuf_get(&global.back, x, y, &back)); if_err_return(rv, cellbuf_get(&global.front, x, y, &front)); int w; { #ifdef TB_OPT_EGC if (back->nech > 0) w = wcswidth((wchar_t *)back->ech, back->nech); else #endif w = tb_wcwidth((wchar_t)back->ch); } if (w < 1) w = 1; // wcwidth qreturns -1 for invalid codepoints if (cell_cmp(back, front) != 0) { cell_copy(front, back); send_attr(back->fg, back->bg); if (w > 1 && x >= global.front.width - (w - 1)) { // Not enough room for wide char, send spaces for (i = x; i < global.front.width; i++) { send_char(i, y, ' '); } } else { { #ifdef TB_OPT_EGC if (back->nech > 0) send_cluster(x, y, back->ech, back->nech); else #endif send_char(x, y, back->ch); } // When wcwidth>1, we need to advance the cursor by more // than 1, thereby skipping some cells. Set these skipped // cells to an invalid codepoint in the front buffer, so // that if this cell is later replaced by a wcwidth==1 char, // we'll get a cell_cmp diff for the skipped cells and // properly re-render. for (i = 1; i < w; i++) { struct tb_cell *front_wide; uint32_t invalid = -1; if_err_return(rv, cellbuf_get(&global.front, x + i, y, &front_wide)); if_err_return(rv, cell_set(front_wide, &invalid, 1, -1, -1)); } } } x += w; } } if_err_return(rv, send_cursor_if(global.cursor_x, global.cursor_y)); if_err_return(rv, bytebuf_flush(&global.out, global.wfd)); return TB_OK; } int tb_invalidate(void) { int rv; if_not_init_return(); if_err_return(rv, resize_cellbufs()); return TB_OK; } int tb_set_cursor(int cx, int cy) { if_not_init_return(); int rv; if (cx < 0) cx = 0; if (cy < 0) cy = 0; if (global.cursor_x == -1) { if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR])); } if_err_return(rv, send_cursor_if(cx, cy)); global.cursor_x = cx; global.cursor_y = cy; return TB_OK; } int tb_hide_cursor(void) { if_not_init_return(); int rv; if (global.cursor_x >= 0) { if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_HIDE_CURSOR])); } global.cursor_x = -1; global.cursor_y = -1; return TB_OK; } int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg) { return tb_set_cell_ex(x, y, &ch, 1, fg, bg); } int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg) { if_not_init_return(); int rv; struct tb_cell *cell; if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); if_err_return(rv, cell_set(cell, ch, nch, fg, bg)); return TB_OK; } int tb_get_cell(int x, int y, int back, struct tb_cell **cell) { if_not_init_return(); return cellbuf_get(back ? &global.back : &global.front, x, y, cell); } int tb_extend_cell(int x, int y, uint32_t ch) { if_not_init_return(); #ifdef TB_OPT_EGC // TODO: iswprint ch? int rv; struct tb_cell *cell; size_t nech; if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); if (cell->nech > 0) { // append to ech nech = cell->nech + 1; if_err_return(rv, cell_reserve_ech(cell, nech + 1)); cell->ech[nech - 1] = ch; } else { // make new ech nech = 2; if_err_return(rv, cell_reserve_ech(cell, nech + 1)); cell->ech[0] = cell->ch; cell->ech[1] = ch; } cell->ech[nech] = '\0'; cell->nech = nech; return TB_OK; #else (void)x; (void)y; (void)ch; return TB_ERR; #endif } int tb_set_input_mode(int mode) { if_not_init_return(); if (mode == TB_INPUT_CURRENT) return global.input_mode; int esc_or_alt = TB_INPUT_ESC | TB_INPUT_ALT; if ((mode & esc_or_alt) == 0) { // neither specified; flip on ESC mode |= TB_INPUT_ESC; } else if ((mode & esc_or_alt) == esc_or_alt) { // both specified; flip off ALT mode &= ~TB_INPUT_ALT; } if (mode & TB_INPUT_MOUSE) { bytebuf_puts(&global.out, TB_HARDCAP_ENTER_MOUSE); bytebuf_flush(&global.out, global.wfd); } else { bytebuf_puts(&global.out, TB_HARDCAP_EXIT_MOUSE); bytebuf_flush(&global.out, global.wfd); } global.input_mode = mode; return TB_OK; } int tb_set_output_mode(int mode) { if_not_init_return(); switch (mode) { case TB_OUTPUT_CURRENT: return global.output_mode; case TB_OUTPUT_NORMAL: case TB_OUTPUT_256: case TB_OUTPUT_216: case TB_OUTPUT_GRAYSCALE: #if TB_OPT_ATTR_W >= 32 case TB_OUTPUT_TRUECOLOR: #endif global.last_fg = ~global.fg; global.last_bg = ~global.bg; global.output_mode = mode; return TB_OK; } return TB_ERR; } int tb_peek_event(struct tb_event *event, int timeout_ms) { if_not_init_return(); return wait_event(event, timeout_ms); } int tb_poll_event(struct tb_event *event) { if_not_init_return(); return wait_event(event, -1); } int tb_get_fds(int *ttyfd, int *resizefd) { if_not_init_return(); *ttyfd = global.rfd; *resizefd = global.resize_pipefd[0]; return TB_OK; } int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str) { return tb_print_ex(x, y, fg, bg, NULL, str); } int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, const char *str) { int rv, w, ix, x_prev; uint32_t uni; if_not_init_return(); if (!cellbuf_in_bounds(&global.back, x, y)) { return TB_ERR_OUT_OF_BOUNDS; } ix = x; x_prev = x; if (out_w) *out_w = 0; while (*str) { rv = tb_utf8_char_to_unicode(&uni, str); if (rv < 0) { uni = 0xfffd; // replace invalid UTF-8 char with U+FFFD str += rv * -1; } else if (rv > 0) { str += rv; } else { break; // shouldn't get here } if (uni == '\n') { // TODO: \r, \t, \v, \f, etc? x = ix; x_prev = x; y += 1; continue; } else if (!tb_iswprint_ex(uni, &w)) { uni = 0xfffd; // replace non-printable with U+FFFD w = 1; } if (w < 0) { return TB_ERR; // shouldn't happen if iswprint } else if (w == 0) { // combining character if (cellbuf_in_bounds(&global.back, x_prev, y)) { if_err_return(rv, tb_extend_cell(x_prev, y, uni)); } } else { if (cellbuf_in_bounds(&global.back, x, y)) { if_err_return(rv, tb_set_cell(x, y, uni, fg, bg)); } x_prev = x; x += w; if (out_w) *out_w += w; } } return TB_OK; } int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, ...) { int rv; va_list vl; va_start(vl, fmt); rv = tb_printf_inner(x, y, fg, bg, NULL, fmt, vl); va_end(vl); return rv; } int tb_printf_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, const char *fmt, ...) { int rv; va_list vl; va_start(vl, fmt); rv = tb_printf_inner(x, y, fg, bg, out_w, fmt, vl); va_end(vl); return rv; } int tb_send(const char *buf, size_t nbuf) { return bytebuf_nputs(&global.out, buf, nbuf); } int tb_sendf(const char *fmt, ...) { int rv; char buf[TB_OPT_PRINTF_BUF]; va_list vl; va_start(vl, fmt); rv = vsnprintf(buf, sizeof(buf), fmt, vl); va_end(vl); if (rv < 0 || rv >= (int)sizeof(buf)) { return TB_ERR; } return tb_send(buf, (size_t)rv); } int tb_set_func(int fn_type, int (*fn)(struct tb_event *, size_t *)) { switch (fn_type) { case TB_FUNC_EXTRACT_PRE: global.fn_extract_esc_pre = fn; return TB_OK; case TB_FUNC_EXTRACT_POST: global.fn_extract_esc_post = fn; return TB_OK; } return TB_ERR; } struct tb_cell *tb_cell_buffer(void) { if (!global.initialized) return NULL; return global.back.cells; } int tb_utf8_char_length(char c) { return utf8_length[(unsigned char)c]; } int tb_utf8_char_to_unicode(uint32_t *out, const char *c) { if (*c == '\0') return 0; int i; unsigned char len = tb_utf8_char_length(*c); unsigned char mask = utf8_mask[len - 1]; uint32_t result = c[0] & mask; for (i = 1; i < len && c[i] != '\0'; ++i) { result <<= 6; result |= c[i] & 0x3f; } if (i != len) return i * -1; *out = result; return (int)len; } int tb_utf8_unicode_to_char(char *out, uint32_t c) { int len = 0; int first; int i; if (c < 0x80) { first = 0; len = 1; } else if (c < 0x800) { first = 0xc0; len = 2; } else if (c < 0x10000) { first = 0xe0; len = 3; } else if (c < 0x200000) { first = 0xf0; len = 4; } else if (c < 0x4000000) { first = 0xf8; len = 5; } else { first = 0xfc; len = 6; } for (i = len - 1; i > 0; --i) { out[i] = (c & 0x3f) | 0x80; c >>= 6; } out[0] = c | first; out[len] = '\0'; return len; } int tb_last_errno(void) { return global.last_errno; } const char *tb_strerror(int err) { switch (err) { case TB_OK: return "Success"; case TB_ERR_NEED_MORE: return "Not enough input"; case TB_ERR_INIT_ALREADY: return "Termbox initialized already"; case TB_ERR_MEM: return "Out of memory"; case TB_ERR_NO_EVENT: return "No event"; case TB_ERR_NO_TERM: return "No TERM in environment"; case TB_ERR_NOT_INIT: return "Termbox not initialized"; case TB_ERR_OUT_OF_BOUNDS: return "Out of bounds"; case TB_ERR_UNSUPPORTED_TERM: return "Unsupported terminal"; case TB_ERR_CAP_COLLISION: return "Termcaps collision"; case TB_ERR_RESIZE_SSCANF: return "Terminal width/height not received by sscanf() after " "resize"; case TB_ERR: case TB_ERR_INIT_OPEN: case TB_ERR_READ: case TB_ERR_RESIZE_IOCTL: case TB_ERR_RESIZE_PIPE: case TB_ERR_RESIZE_SIGACTION: case TB_ERR_POLL: case TB_ERR_TCGETATTR: case TB_ERR_TCSETATTR: case TB_ERR_RESIZE_WRITE: case TB_ERR_RESIZE_POLL: case TB_ERR_RESIZE_READ: default: if (strerror_r(global.last_errno, global.errbuf, sizeof(global.errbuf)) != 0) { return "(strerror_r failed to store error description)"; } return (const char *)global.errbuf; } } int tb_has_truecolor(void) { #if TB_OPT_ATTR_W >= 32 return 1; #else return 0; #endif } int tb_has_egc(void) { #ifdef TB_OPT_EGC return 1; #else return 0; #endif } int tb_attr_width(void) { return TB_OPT_ATTR_W; } const char *tb_version(void) { return TB_VERSION_STR; } static int tb_reset(void) { int ttyfd_open = global.ttyfd_open; int (*fn_print_log_msg)(const char *fmt, ...) = global.fn_print_log_msg; memset(&global, 0, sizeof(global)); global.ttyfd = -1; global.rfd = -1; global.wfd = -1; global.ttyfd_open = ttyfd_open; global.resize_pipefd[0] = -1; global.resize_pipefd[1] = -1; global.width = -1; global.height = -1; global.cursor_x = -1; global.cursor_y = -1; global.last_x = -1; global.last_y = -1; global.fg = TB_DEFAULT; global.bg = TB_DEFAULT; global.last_fg = ~global.fg; global.last_bg = ~global.bg; global.input_mode = TB_INPUT_ESC; global.output_mode = TB_OUTPUT_NORMAL; global.fn_print_log_msg = fn_print_log_msg; return TB_OK; } static int init_term_attrs(void) { if (global.ttyfd < 0) return TB_OK; if (tcgetattr(global.ttyfd, &global.orig_tios) != 0) { global.last_errno = errno; return TB_ERR_TCGETATTR; } struct termios tios; memcpy(&tios, &global.orig_tios, sizeof(tios)); global.has_orig_tios = 1; cfmakeraw(&tios); tios.c_cc[VMIN] = 1; tios.c_cc[VTIME] = 0; if (tcsetattr(global.ttyfd, TCSAFLUSH, &tios) != 0) { global.last_errno = errno; return TB_ERR_TCSETATTR; } return TB_OK; } int tb_printf_inner(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, const char *fmt, va_list vl) { int rv; char buf[TB_OPT_PRINTF_BUF]; rv = vsnprintf(buf, sizeof(buf), fmt, vl); if (rv < 0 || rv >= (int)sizeof(buf)) { return TB_ERR; } return tb_print_ex(x, y, fg, bg, out_w, buf); } static int init_term_caps(void) { if (load_terminfo() == TB_OK) { return parse_terminfo_caps(); } return load_builtin_caps(); } static int init_cap_trie(void) { int rv, i; // Add caps from terminfo or built-in // // Collisions are expected as some terminfo entries have dupes. (For // example, att605-pc collides on TB_CAP_F4 and TB_CAP_DELETE.) First cap // in TB_CAP_* index order will win. // // TODO: Reorder TB_CAP_* so more critical caps come first. for (i = 0; i < TB_CAP__COUNT_KEYS; i++) { rv = cap_trie_add(global.caps[i], tb_key_i(i), 0); if (rv != TB_OK && rv != TB_ERR_CAP_COLLISION) return rv; } // Add built-in mod caps // // Collisions are OK here as well. This can happen if global.caps collides // with builtin_mod_caps. It is desirable to give precedence to global.caps // here. for (i = 0; builtin_mod_caps[i].cap != NULL; i++) { rv = cap_trie_add(builtin_mod_caps[i].cap, builtin_mod_caps[i].key, builtin_mod_caps[i].mod); if (rv != TB_OK && rv != TB_ERR_CAP_COLLISION) return rv; } return TB_OK; } static int cap_trie_add(const char *cap, uint16_t key, uint8_t mod) { struct cap_trie *next, *node = &global.cap_trie; size_t i, j; if (!cap || strlen(cap) <= 0) return TB_OK; // Nothing to do for empty caps for (i = 0; cap[i] != '\0'; i++) { char c = cap[i]; next = NULL; // Check if c is already a child of node for (j = 0; j < node->nchildren; j++) { if (node->children[j].c == c) { next = &node->children[j]; break; } } if (!next) { // We need to add a new child to node node->nchildren += 1; node->children = (struct cap_trie *)tb_realloc(node->children, sizeof(*node) * node->nchildren); if (!node->children) { return TB_ERR_MEM; } next = &node->children[node->nchildren - 1]; memset(next, 0, sizeof(*next)); next->c = c; } // Continue node = next; } if (node->is_leaf) { // Already a leaf here return TB_ERR_CAP_COLLISION; } node->is_leaf = 1; node->key = key; node->mod = mod; return TB_OK; } static int cap_trie_find(const char *buf, size_t nbuf, struct cap_trie **last, size_t *depth) { struct cap_trie *next, *node = &global.cap_trie; size_t i, j; *last = node; *depth = 0; for (i = 0; i < nbuf; i++) { char c = buf[i]; next = NULL; // Find c in node.children for (j = 0; j < node->nchildren; j++) { if (node->children[j].c == c) { next = &node->children[j]; break; } } if (!next) { // Not found return TB_OK; } node = next; *last = node; *depth += 1; if (node->is_leaf && node->nchildren < 1) { break; } } return TB_OK; } static int cap_trie_deinit(struct cap_trie *node) { size_t j; for (j = 0; j < node->nchildren; j++) { cap_trie_deinit(&node->children[j]); } if (node->children) tb_free(node->children); memset(node, 0, sizeof(*node)); return TB_OK; } static int init_resize_handler(void) { if (pipe(global.resize_pipefd) != 0) { global.last_errno = errno; return TB_ERR_RESIZE_PIPE; } struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = handle_resize; if (sigaction(SIGWINCH, &sa, NULL) != 0) { global.last_errno = errno; return TB_ERR_RESIZE_SIGACTION; } return TB_OK; } static int send_init_escape_codes(void) { int rv; if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_ENTER_CA])); if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_ENTER_KEYPAD])); if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_HIDE_CURSOR])); return TB_OK; } static int send_clear(void) { int rv; if_err_return(rv, send_attr(global.fg, global.bg)); if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_CLEAR_SCREEN])); if_err_return(rv, send_cursor_if(global.cursor_x, global.cursor_y)); if_err_return(rv, bytebuf_flush(&global.out, global.wfd)); global.last_x = -1; global.last_y = -1; return TB_OK; } static int update_term_size(void) { int rv, ioctl_errno; if (global.ttyfd < 0) return TB_OK; struct winsize sz; memset(&sz, 0, sizeof(sz)); // Try ioctl TIOCGWINSZ if (ioctl(global.ttyfd, TIOCGWINSZ, &sz) == 0) { tb_log("TIOCGWINSZ operation yielded %hu cols, %hu rows.", sz.ws_col, sz.ws_row); global.width = sz.ws_col; global.height = sz.ws_row; return TB_OK; } ioctl_errno = errno; // Try >cursor(9999,9999), >u7, = 0) { bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); bytebuf_puts(&global.out, global.caps[TB_CAP_SGR0]); bytebuf_puts(&global.out, global.caps[TB_CAP_CLEAR_SCREEN]); bytebuf_puts(&global.out, global.caps[TB_CAP_EXIT_CA]); bytebuf_puts(&global.out, global.caps[TB_CAP_EXIT_KEYPAD]); bytebuf_puts(&global.out, TB_HARDCAP_EXIT_MOUSE); bytebuf_flush(&global.out, global.wfd); } if (global.ttyfd >= 0) { if (global.has_orig_tios) { tcsetattr(global.ttyfd, TCSAFLUSH, &global.orig_tios); } if (global.ttyfd_open) { close(global.ttyfd); global.ttyfd_open = 0; } } struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_DFL; sigaction(SIGWINCH, &sa, NULL); if (global.resize_pipefd[0] >= 0) close(global.resize_pipefd[0]); if (global.resize_pipefd[1] >= 0) close(global.resize_pipefd[1]); cellbuf_free(&global.back); cellbuf_free(&global.front); bytebuf_free(&global.in); bytebuf_free(&global.out); if (global.terminfo) tb_free(global.terminfo); cap_trie_deinit(&global.cap_trie); tb_reset(); return TB_OK; } static int load_terminfo(void) { int rv; char tmp[TB_PATH_MAX]; // See terminfo(5) "Fetching Compiled Descriptions" for a description of // this behavior. Some of these paths are compile-time ncurses options, so // best guesses are used here. const char *term = getenv("TERM"); if (!term) return TB_ERR; // If TERMINFO is set, try that directory first const char *terminfo = getenv("TERMINFO"); if (terminfo) if_ok_return(rv, load_terminfo_from_path(terminfo, term)); // Next try ~/.terminfo const char *home = getenv("HOME"); if (home) { snprintf_or_return(rv, tmp, sizeof(tmp), "%s/.terminfo", home); if_ok_return(rv, load_terminfo_from_path(tmp, term)); } // Next try TERMINFO_DIRS // // Note, empty entries are supposed to be interpretted as the "compiled-in // default", which is of course system-dependent. Previously /etc/terminfo // was used here. Let's skip empty entries altogether rather than give // precedence to a guess, and check common paths after this loop. const char *dirs = getenv("TERMINFO_DIRS"); if (dirs) { snprintf_or_return(rv, tmp, sizeof(tmp), "%s", dirs); char *dir = strtok(tmp, ":"); while (dir) { const char *cdir = dir; if (*cdir != '\0') { if_ok_return(rv, load_terminfo_from_path(cdir, term)); } dir = strtok(NULL, ":"); } } #ifdef TB_TERMINFO_DIR if_ok_return(rv, load_terminfo_from_path(TB_TERMINFO_DIR, term)); #endif if_ok_return(rv, load_terminfo_from_path("/usr/local/etc/terminfo", term)); if_ok_return(rv, load_terminfo_from_path("/usr/local/share/terminfo", term)); if_ok_return(rv, load_terminfo_from_path("/usr/local/lib/terminfo", term)); if_ok_return(rv, load_terminfo_from_path("/etc/terminfo", term)); if_ok_return(rv, load_terminfo_from_path("/usr/share/terminfo", term)); if_ok_return(rv, load_terminfo_from_path("/usr/lib/terminfo", term)); if_ok_return(rv, load_terminfo_from_path("/usr/share/lib/terminfo", term)); if_ok_return(rv, load_terminfo_from_path("/lib/terminfo", term)); return TB_ERR; } static int load_terminfo_from_path(const char *path, const char *term) { int rv; char tmp[TB_PATH_MAX]; // Look for term at this terminfo location, e.g., /x/xterm snprintf_or_return(rv, tmp, sizeof(tmp), "%s/%c/%s", path, term[0], term); if_ok_return(rv, read_terminfo_path(tmp)); #ifdef __APPLE__ // Try the Darwin equivalent path, e.g., /78/xterm snprintf_or_return(rv, tmp, sizeof(tmp), "%s/%x/%s", path, term[0], term); if_ok_return(rv, read_terminfo_path(tmp)); #endif tb_log("Couldn't find term %s in terminfo database %s", term, path); return TB_ERR; } static int read_terminfo_path(const char *path) { FILE *fp = fopen(path, "rb"); if (!fp) return TB_ERR; struct stat st; if (fstat(fileno(fp), &st) != 0) { fclose(fp); return TB_ERR; } size_t fsize = st.st_size; char *data = (char *)tb_malloc(fsize); if (!data) { fclose(fp); return TB_ERR; } if (fread(data, 1, fsize, fp) != fsize) { fclose(fp); tb_free(data); return TB_ERR; } global.terminfo = data; global.nterminfo = fsize; tb_log("Successfully read terminfo from %s (%zu bytes)", path, fsize); fclose(fp); return TB_OK; } static int parse_terminfo_caps(void) { // See term(5) "LEGACY STORAGE FORMAT" and "EXTENDED STORAGE FORMAT" for a // description of this behavior. // Ensure there's at least a header's worth of data if (global.nterminfo < 6 * (int)sizeof(int16_t)) return TB_ERR; int16_t magic_number, nbytes_names, nbytes_bools, num_ints, num_offsets, nbytes_strings; size_t nbytes_header = 6 * sizeof(int16_t); // header[0] the magic number (octal 0432 or 01036) // header[1] the size, in bytes, of the names section // header[2] the number of bytes in the boolean section // header[3] the number of short integers in the numbers section // header[4] the number of offsets (short integers) in the strings section // header[5] the size, in bytes, of the string table get_terminfo_int16(0 * sizeof(int16_t), &magic_number); get_terminfo_int16(1 * sizeof(int16_t), &nbytes_names); get_terminfo_int16(2 * sizeof(int16_t), &nbytes_bools); get_terminfo_int16(3 * sizeof(int16_t), &num_ints); get_terminfo_int16(4 * sizeof(int16_t), &num_offsets); get_terminfo_int16(5 * sizeof(int16_t), &nbytes_strings); // Legacy ints are 16-bit, extended ints are 32-bit const int bytes_per_int = magic_number == 01036 ? 4 // 32-bit : 2; // 16-bit // > Between the boolean section and the number section, a null byte will be // > inserted, if necessary, to ensure that the number section begins on an // > even byte const int align_offset = (nbytes_names + nbytes_bools) % 2 != 0 ? 1 : 0; const int pos_str_offsets = nbytes_header // header (12 bytes) + nbytes_names // length of names section + nbytes_bools // length of boolean section + align_offset + (num_ints * bytes_per_int); // length of numbers section const int pos_str_table = pos_str_offsets + (num_offsets * sizeof(int16_t)); // length of string offsets table // Load caps int i; for (i = 0; i < TB_CAP__COUNT; i++) { const char *cap = get_terminfo_string(pos_str_offsets, num_offsets, pos_str_table, nbytes_strings, terminfo_cap_indexes[i]); if (!cap) { // Something is not right return TB_ERR; } global.caps[i] = cap; } return TB_OK; } static int load_builtin_caps(void) { int i, j; const char *term = getenv("TERM"); if (!term) return TB_ERR_NO_TERM; // Check for exact TERM match for (i = 0; builtin_terms[i].name != NULL; i++) { if (strcmp(term, builtin_terms[i].name) == 0) { for (j = 0; j < TB_CAP__COUNT; j++) { global.caps[j] = builtin_terms[i].caps[j]; } return TB_OK; } } // Check for partial TERM or alias match for (i = 0; builtin_terms[i].name != NULL; i++) { if (strstr(term, builtin_terms[i].name) != NULL || (*(builtin_terms[i].alias) != '\0' && strstr(term, builtin_terms[i].alias) != NULL)) { for (j = 0; j < TB_CAP__COUNT; j++) { global.caps[j] = builtin_terms[i].caps[j]; } return TB_OK; } } return TB_ERR_UNSUPPORTED_TERM; } static const char *get_terminfo_string(int16_t offsets_pos, int16_t offsets_len, int16_t table_pos, int16_t table_size, int16_t index) { if (index >= offsets_len) { // An index beyond the offset table indicates absent // See `convert_strings` in tinfo `read_entry.c` return ""; } int16_t table_offset; int table_offset_offset = (int)offsets_pos + (index * (int)sizeof(int16_t)); if (get_terminfo_int16(table_offset_offset, &table_offset) != TB_OK) { // offset beyond end of terminfo entry // Truncated/corrupt terminfo entry? return NULL; } if (table_offset < 0 || table_offset >= table_size) { // A negative offset indicates absent // An offset beyond the string table indicates absent // See `convert_strings` in tinfo `read_entry.c` return ""; } int str_offset = (int)table_pos + (int)table_offset; if (str_offset >= (int)global.nterminfo) { tb_log("Offset %d is beyond end of terminfo data. Is terminfo corrupted?", str_offset); return NULL; } return (const char *)(global.terminfo + str_offset); } static int get_terminfo_int16(int offset, int16_t *val) { if (offset < 0 || offset + sizeof(int16_t) > global.nterminfo) { tb_log("Invalid offset %d for reading int16 from terminfo", offset); *val = -1; return TB_ERR; } memcpy(val, global.terminfo + offset, sizeof(int16_t)); return TB_OK; } static int wait_event(struct tb_event *event, int timeout) { int rv; char buf[TB_OPT_READ_BUF]; memset(event, 0, sizeof(*event)); if_ok_return(rv, extract_event(event)); fd_set fds; struct timeval tv; tv.tv_sec = timeout / 1000; tv.tv_usec = (timeout - (tv.tv_sec * 1000)) * 1000; do { FD_ZERO(&fds); FD_SET(global.rfd, &fds); FD_SET(global.resize_pipefd[0], &fds); int maxfd = global.resize_pipefd[0] > global.rfd ? global.resize_pipefd[0] : global.rfd; int select_rv = select(maxfd + 1, &fds, NULL, NULL, (timeout < 0) ? NULL : &tv); if (select_rv < 0) { // Let EINTR/EAGAIN bubble up global.last_errno = errno; return TB_ERR_POLL; } else if (select_rv == 0) { return TB_ERR_NO_EVENT; } int tty_has_events = (FD_ISSET(global.rfd, &fds)); int resize_has_events = (FD_ISSET(global.resize_pipefd[0], &fds)); if (tty_has_events) { ssize_t read_rv = read(global.rfd, buf, sizeof(buf)); if (read_rv < 0) { global.last_errno = errno; return TB_ERR_READ; } else if (read_rv > 0) { bytebuf_nputs(&global.in, buf, read_rv); } } if (resize_has_events) { int ignore = 0; if (read(global.resize_pipefd[0], &ignore, sizeof(ignore)) < 0) { tb_log("Couldn't read data from resize pipe even though file descriptor was set!"); } // TODO: Harden against errors encountered mid-resize if_err_return(rv, update_term_size()); if_err_return(rv, resize_cellbufs()); event->type = TB_EVENT_RESIZE; event->w = global.width; event->h = global.height; return TB_OK; } memset(event, 0, sizeof(*event)); if_ok_return(rv, extract_event(event)); } while (timeout == -1); return rv; } static int extract_event(struct tb_event *event) { int rv; struct bytebuf *in = &global.in; if (in->len == 0) return TB_ERR; if (in->buf[0] == '\x1b') { // Escape sequence? // In TB_INPUT_ESC, skip if the buffer is a single escape char if (!((global.input_mode & TB_INPUT_ESC) && in->len == 1)) { if_ok_or_need_more_return(rv, extract_esc(event)); } // Escape key? if (global.input_mode & TB_INPUT_ESC) { event->type = TB_EVENT_KEY; event->ch = 0; event->key = TB_KEY_ESC; event->mod = 0; bytebuf_shift(in, 1); return TB_OK; } // Recurse for alt key event->mod |= TB_MOD_ALT; bytebuf_shift(in, 1); return extract_event(event); } // ASCII control key? int is_ctrl = (uint16_t)in->buf[0] < TB_KEY_SPACE || in->buf[0] == TB_KEY_BACKSPACE2; if (is_ctrl) { event->type = TB_EVENT_KEY; event->ch = 0; event->key = (uint16_t)in->buf[0]; event->mod |= TB_MOD_CTRL; bytebuf_shift(in, 1); return TB_OK; } // UTF-8? if (in->len >= (size_t)tb_utf8_char_length(in->buf[0])) { event->type = TB_EVENT_KEY; tb_utf8_char_to_unicode(&event->ch, in->buf); event->key = 0; bytebuf_shift(in, tb_utf8_char_length(in->buf[0])); return TB_OK; } // Need more input return TB_ERR; } static int extract_esc(struct tb_event *event) { int rv; if_ok_or_need_more_return(rv, extract_esc_user(event, 0)); if_ok_or_need_more_return(rv, extract_esc_cap(event)); if_ok_or_need_more_return(rv, extract_esc_mouse(event)); if_ok_or_need_more_return(rv, extract_esc_user(event, 1)); return TB_ERR; } static int extract_esc_user(struct tb_event *event, int is_post) { int rv; size_t consumed = 0; struct bytebuf *in = &global.in; int (*fn)(struct tb_event *, size_t *); fn = is_post ? global.fn_extract_esc_post : global.fn_extract_esc_pre; if (!fn) return TB_ERR; rv = fn(event, &consumed); if (rv == TB_OK) bytebuf_shift(in, consumed); if_ok_or_need_more_return(rv, rv); return TB_ERR; } static int extract_esc_cap(struct tb_event *event) { int rv; struct bytebuf *in = &global.in; struct cap_trie *node; size_t depth; if_err_return(rv, cap_trie_find(in->buf, in->len, &node, &depth)); if (node->is_leaf) { // Found a leaf node event->type = TB_EVENT_KEY; event->ch = 0; event->key = node->key; event->mod = node->mod; bytebuf_shift(in, depth); return TB_OK; } else if (node->nchildren > 0 && in->len <= depth) { // Found a branch node (not enough input) return TB_ERR_NEED_MORE; } return TB_ERR; } static int extract_esc_mouse(struct tb_event *event) { struct bytebuf *in = &global.in; size_t buf_shift = 0; // Bail if not enough to determine type if (in->len < 2) { return TB_ERR_NEED_MORE; } else if (in->buf[1] != '[') { return TB_ERR; } else if (in->len < 3) { return TB_ERR_NEED_MORE; } // Discern type of mouse event from 3rd byte int type = 0; enum { TYPE_VT200 = 0, TYPE_1006, TYPE_1015, TYPE_MAX }; if (in->buf[2] == 'M') { // X10 mouse encoding, the simplest one // \x1b [ M Cb Cx Cy type = TYPE_VT200; } else if (in->buf[2] == '<') { // xterm 1006 extended mode or urxvt 1015 extended mode // xterm: \x1b [ < Cb ; Cx ; Cy (M or m) type = TYPE_1006; } else { // urxvt: \x1b [ Cb ; Cx ; Cy M type = TYPE_1015; } switch (type) { case TYPE_VT200: { // In this mode, we need 6 bytes if (in->len < 6) return TB_ERR_NEED_MORE; int b = in->buf[3] - 0x20; switch (b & 3) { case 0: event->key = ((b & 64) != 0) ? TB_KEY_MOUSE_WHEEL_UP : TB_KEY_MOUSE_LEFT; break; case 1: event->key = ((b & 64) != 0) ? TB_KEY_MOUSE_WHEEL_DOWN : TB_KEY_MOUSE_MIDDLE; break; case 2: event->key = TB_KEY_MOUSE_RIGHT; break; case 3: event->key = TB_KEY_MOUSE_RELEASE; break; default: return TB_ERR; } if ((b & 32) != 0) event->mod |= TB_MOD_MOTION; // the coord is 1,1 for upper left event->x = ((uint8_t)in->buf[4]) - 0x21; event->y = ((uint8_t)in->buf[5]) - 0x21; // Eat 6 bytes buf_shift = 6; break; } case TYPE_1006: // fallthrough case TYPE_1015: { int num[3] = {-1, -1, -1}; int num_i = 0; int cur_num = -1; char trail = ' '; size_t i = 2; if (type == TYPE_1006) ++i; // skip '<' // Parse %d;%d;%d[mM] into `num` while (i < in->len && num_i < 3) { char c = in->buf[i]; if (c >= '0' && c <= '9') { // Digit if (cur_num == -1) cur_num = 0; cur_num *= 10; cur_num += (int)(c - '0'); } else if (cur_num != -1 && ((num_i < 2 && c == ';') || (num_i == 2 && (c == 'm' || c == 'M')))) { // We're at a semi-colon, 'm', or 'M' // and we have a number num[num_i] = cur_num; ++num_i; cur_num = -1; trail = c; } else { // Something else; not a mouse event return TB_ERR; } ++i; } // If we didn't get to the 3rd number, we need more if (num[2] == -1) return TB_ERR_NEED_MORE; // We have a valid mouse event, eat `i` bytes from the buffer buf_shift = i; if (type == TYPE_1015) num[0] -= 0x20; switch (num[0] & 3) { case 0: event->key = ((num[0] & 64) != 0) ? TB_KEY_MOUSE_WHEEL_UP : TB_KEY_MOUSE_LEFT; break; case 1: event->key = ((num[0] & 64) != 0) ? TB_KEY_MOUSE_WHEEL_DOWN : TB_KEY_MOUSE_MIDDLE; break; case 2: event->key = TB_KEY_MOUSE_RIGHT; break; case 3: event->key = TB_KEY_MOUSE_RELEASE; break; default: return TB_ERR; } // on xterm mouse release is signaled by lowercase m if (trail == 'm') event->key = TB_KEY_MOUSE_RELEASE; if ((num[0] & 32) != 0) event->mod |= TB_MOD_MOTION; event->x = (num[1] - 1 < 0) ? 0 : num[1] - 1; event->y = (num[2] - 1 < 0) ? 0 : num[2] - 1; break; } } if (buf_shift > 0) bytebuf_shift(in, buf_shift); event->type = TB_EVENT_MOUSE; return TB_OK; } static int resize_cellbufs(void) { int rv; if_err_return(rv, cellbuf_resize(&global.back, global.width, global.height)); if_err_return(rv, cellbuf_resize(&global.front, global.width, global.height)); if_err_return(rv, cellbuf_clear(&global.front)); if_err_return(rv, send_clear()); return TB_OK; } static void handle_resize(int sig) { int errno_copy = errno; if (write(global.resize_pipefd[1], &sig, sizeof(sig)) < 0) { tb_log("Couldn't write to resize pipe to notify about resize signal!"); } errno = errno_copy; } static int send_attr(uintattr_t fg, uintattr_t bg) { int rv; if (fg == global.last_fg && bg == global.last_bg) { return TB_OK; } if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_SGR0])); uint32_t cfg, cbg; switch (global.output_mode) { default: case TB_OUTPUT_NORMAL: // The minus 1 below is because our colors are 1-indexed starting // from black. Black is represented by a 30, 40, 90, or 100 for fg, // bg, bright fg, or bright bg respectively. Red is 31, 41, 91, // 101, etc. cfg = (fg & TB_BRIGHT ? 90 : 30) + (fg & 0x0f) - 1; cbg = (bg & TB_BRIGHT ? 100 : 40) + (bg & 0x0f) - 1; break; case TB_OUTPUT_256: cfg = fg & 0xff; cbg = bg & 0xff; if (fg & TB_HI_BLACK) cfg = 0; if (bg & TB_HI_BLACK) cbg = 0; break; case TB_OUTPUT_216: cfg = fg & 0xff; cbg = bg & 0xff; if (cfg > 216) cfg = 216; if (cbg > 216) cbg = 216; cfg += 0x0f; cbg += 0x0f; break; case TB_OUTPUT_GRAYSCALE: cfg = fg & 0xff; cbg = bg & 0xff; if (cfg > 24) cfg = 24; if (cbg > 24) cbg = 24; cfg += 0xe7; cbg += 0xe7; break; #if TB_OPT_ATTR_W >= 32 case TB_OUTPUT_TRUECOLOR: cfg = fg & 0xffffff; cbg = bg & 0xffffff; if (fg & TB_HI_BLACK) cfg = 0; if (bg & TB_HI_BLACK) cbg = 0; break; #endif } if (fg & TB_BOLD) if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_BOLD])); if (fg & TB_BLINK) if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_BLINK])); if (fg & TB_UNDERLINE) if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_UNDERLINE])); if (fg & TB_ITALIC) if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_ITALIC])); if (fg & TB_DIM) if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_DIM])); #if TB_OPT_ATTR_W == 64 if (fg & TB_STRIKEOUT) if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_STRIKEOUT)); if (fg & TB_UNDERLINE_2) if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_UNDERLINE_2)); if (fg & TB_OVERLINE) if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_OVERLINE)); if (fg & TB_INVISIBLE) if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_INVISIBLE])); #endif if ((fg & TB_REVERSE) || (bg & TB_REVERSE)) if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_REVERSE])); int fg_is_default = (fg & 0xff) == 0; int bg_is_default = (bg & 0xff) == 0; if (global.output_mode == TB_OUTPUT_256) { if (fg & TB_HI_BLACK) fg_is_default = 0; if (bg & TB_HI_BLACK) bg_is_default = 0; } #if TB_OPT_ATTR_W >= 32 if (global.output_mode == TB_OUTPUT_TRUECOLOR) { fg_is_default = ((fg & 0xffffff) == 0) && ((fg & TB_HI_BLACK) == 0); bg_is_default = ((bg & 0xffffff) == 0) && ((bg & TB_HI_BLACK) == 0); } #endif if_err_return(rv, send_sgr(cfg, cbg, fg_is_default, bg_is_default)); global.last_fg = fg; global.last_bg = bg; return TB_OK; } static int send_sgr(uint32_t cfg, uint32_t cbg, int fg_is_default, int bg_is_default) { int rv; char nbuf[32]; if (fg_is_default && bg_is_default) { return TB_OK; } switch (global.output_mode) { default: case TB_OUTPUT_NORMAL: send_literal(rv, "\x1b["); if (!fg_is_default) { send_num(rv, nbuf, cfg); if (!bg_is_default) { send_literal(rv, ";"); } } if (!bg_is_default) { send_num(rv, nbuf, cbg); } send_literal(rv, "m"); break; case TB_OUTPUT_256: case TB_OUTPUT_216: case TB_OUTPUT_GRAYSCALE: send_literal(rv, "\x1b["); if (!fg_is_default) { send_literal(rv, "38;5;"); send_num(rv, nbuf, cfg); if (!bg_is_default) { send_literal(rv, ";"); } } if (!bg_is_default) { send_literal(rv, "48;5;"); send_num(rv, nbuf, cbg); } send_literal(rv, "m"); break; #if TB_OPT_ATTR_W >= 32 case TB_OUTPUT_TRUECOLOR: send_literal(rv, "\x1b["); if (!fg_is_default) { send_literal(rv, "38;2;"); send_num(rv, nbuf, (cfg >> 16) & 0xff); send_literal(rv, ";"); send_num(rv, nbuf, (cfg >> 8) & 0xff); send_literal(rv, ";"); send_num(rv, nbuf, cfg & 0xff); if (!bg_is_default) { send_literal(rv, ";"); } } if (!bg_is_default) { send_literal(rv, "48;2;"); send_num(rv, nbuf, (cbg >> 16) & 0xff); send_literal(rv, ";"); send_num(rv, nbuf, (cbg >> 8) & 0xff); send_literal(rv, ";"); send_num(rv, nbuf, cbg & 0xff); } send_literal(rv, "m"); break; #endif } return TB_OK; } static int send_cursor_if(int x, int y) { int rv; char nbuf[32]; if (x < 0 || y < 0) { return TB_OK; } send_literal(rv, "\x1b["); send_num(rv, nbuf, y + 1); send_literal(rv, ";"); send_num(rv, nbuf, x + 1); send_literal(rv, "H"); return TB_OK; } static int send_char(int x, int y, uint32_t ch) { return send_cluster(x, y, &ch, 1); } static int send_cluster(int x, int y, uint32_t *ch, size_t nch) { int rv; char chu8[8]; if (global.last_x != x - 1 || global.last_y != y) { if_err_return(rv, send_cursor_if(x, y)); } global.last_x = x; global.last_y = y; int i; for (i = 0; i < (int)nch; i++) { uint32_t ch32 = *(ch + i); if (!tb_iswprint(ch32)) { ch32 = 0xfffd; // replace non-printable codepoints with U+FFFD } int chu8_len = tb_utf8_unicode_to_char(chu8, ch32); if_err_return(rv, bytebuf_nputs(&global.out, chu8, (size_t)chu8_len)); } return TB_OK; } static int convert_num(uint32_t num, char *buf) { int i, l = 0; char ch; do { buf[l++] = (char)('0' + (num % 10)); num /= 10; } while (num); for (i = 0; i < l / 2; i++) { ch = buf[i]; buf[i] = buf[l - 1 - i]; buf[l - 1 - i] = ch; } return l; } static int cell_cmp(struct tb_cell *a, struct tb_cell *b) { if (a->ch != b->ch || a->fg != b->fg || a->bg != b->bg) { return 1; } #ifdef TB_OPT_EGC if (a->nech != b->nech) { return 1; } else if (a->nech > 0) { // a->nech == b->nech return memcmp(a->ech, b->ech, a->nech); } #endif return 0; } static int cell_copy(struct tb_cell *dst, struct tb_cell *src) { #ifdef TB_OPT_EGC if (src->nech > 0) { return cell_set(dst, src->ech, src->nech, src->fg, src->bg); } #endif return cell_set(dst, &src->ch, 1, src->fg, src->bg); } static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg) { // TODO: iswprint ch? cell->ch = ch ? *ch : 0; cell->fg = fg; cell->bg = bg; #ifdef TB_OPT_EGC if (nch <= 1) { cell->nech = 0; } else { int rv; if_err_return(rv, cell_reserve_ech(cell, nch + 1)); memcpy(cell->ech, ch, sizeof(*ch) * nch); cell->ech[nch] = '\0'; cell->nech = nch; } #else (void)nch; (void)cell_reserve_ech; #endif return TB_OK; } static int cell_reserve_ech(struct tb_cell *cell, size_t n) { #ifdef TB_OPT_EGC if (cell->cech >= n) return TB_OK; cell->ech = (uint32_t *)tb_realloc(cell->ech, n * sizeof(cell->ch)); if (!cell->ech) return TB_ERR_MEM; cell->cech = n; return TB_OK; #else (void)cell; (void)n; return TB_ERR; #endif } static int cell_free(struct tb_cell *cell) { #ifdef TB_OPT_EGC if (cell->ech) tb_free(cell->ech); #endif memset(cell, 0, sizeof(*cell)); return TB_OK; } static int cellbuf_init(struct cellbuf *c, int w, int h) { c->cells = (struct tb_cell *)tb_malloc(sizeof(struct tb_cell) * w * h); if (!c->cells) return TB_ERR_MEM; memset(c->cells, 0, sizeof(struct tb_cell) * w * h); c->width = w; c->height = h; return TB_OK; } static int cellbuf_free(struct cellbuf *c) { if (c->cells) { int i; for (i = 0; i < c->width * c->height; i++) { cell_free(&c->cells[i]); } tb_free(c->cells); } memset(c, 0, sizeof(*c)); return TB_OK; } static int cellbuf_clear(struct cellbuf *c) { int rv, i; uint32_t space = (uint32_t)' '; for (i = 0; i < c->width * c->height; i++) { if_err_return(rv, cell_set(&c->cells[i], &space, 1, global.fg, global.bg)); } return TB_OK; } static int cellbuf_get(struct cellbuf *c, int x, int y, struct tb_cell **out) { if (!cellbuf_in_bounds(c, x, y)) { *out = NULL; return TB_ERR_OUT_OF_BOUNDS; } *out = &c->cells[(y * c->width) + x]; return TB_OK; } static int cellbuf_in_bounds(struct cellbuf *c, int x, int y) { if (x < 0 || x >= c->width || y < 0 || y >= c->height) { return 0; } return 1; } static int cellbuf_resize(struct cellbuf *c, int w, int h) { int rv; int ow = c->width; int oh = c->height; if (ow == w && oh == h) { return TB_OK; } w = w < 1 ? 1 : w; h = h < 1 ? 1 : h; int minw = (w < ow) ? w : ow; int minh = (h < oh) ? h : oh; struct tb_cell *prev = c->cells; if_err_return(rv, cellbuf_init(c, w, h)); if_err_return(rv, cellbuf_clear(c)); int x, y; for (x = 0; x < minw; x++) { for (y = 0; y < minh; y++) { struct tb_cell *src, *dst; src = &prev[(y * ow) + x]; if_err_return(rv, cellbuf_get(c, x, y, &dst)); if_err_return(rv, cell_copy(dst, src)); } } tb_free(prev); return TB_OK; } static int bytebuf_puts(struct bytebuf *b, const char *str) { if (!str || strlen(str) <= 0) return TB_OK; // Nothing to do for empty caps return bytebuf_nputs(b, str, (size_t)strlen(str)); } static int bytebuf_nputs(struct bytebuf *b, const char *str, size_t nstr) { int rv; if_err_return(rv, bytebuf_reserve(b, b->len + nstr + 1)); memcpy(b->buf + b->len, str, nstr); b->len += nstr; b->buf[b->len] = '\0'; return TB_OK; } static int bytebuf_shift(struct bytebuf *b, size_t n) { if (n > b->len) n = b->len; size_t nmove = b->len - n; memmove(b->buf, b->buf + n, nmove); b->len -= n; return TB_OK; } static int bytebuf_flush(struct bytebuf *b, int fd) { if (b->len <= 0) return TB_OK; ssize_t write_rv = write(fd, b->buf, b->len); if (write_rv < 0 || (size_t)write_rv != b->len) { // Note, errno will be 0 on partial write global.last_errno = errno; return TB_ERR; } b->len = 0; return TB_OK; } static int bytebuf_reserve(struct bytebuf *b, size_t sz) { if (b->cap >= sz) return TB_OK; size_t newcap = b->cap > 0 ? b->cap : 1; while (newcap < sz) { newcap *= 2; } char *newbuf; if (b->buf) { newbuf = (char *)tb_realloc(b->buf, newcap); } else { newbuf = (char *)tb_malloc(newcap); } if (!newbuf) return TB_ERR_MEM; b->buf = newbuf; b->cap = newcap; return TB_OK; } static int bytebuf_free(struct bytebuf *b) { if (b->buf) tb_free(b->buf); memset(b, 0, sizeof(*b)); return TB_OK; } int tb_iswprint(uint32_t ch) { return iswprint((wint_t)ch); } int tb_wcwidth(uint32_t ch) { return wcwidth((wchar_t)ch); } static int tb_iswprint_ex(uint32_t ch, int *w) { if (w) *w = wcwidth((wint_t)ch); return iswprint(ch); } void tb_set_log_function(int (*fn)(const char *fmt, ...)) { global.fn_print_log_msg = fn; } #endif // TB_IMPL newsraft/src/threads.c000066400000000000000000000030371516312403600153000ustar00rootroot00000000000000#include "newsraft.h" struct newsraft_thread { pthread_t thread; pthread_cond_t cond; pthread_mutex_t mutex; void *(*worker)(void *); }; static struct newsraft_thread newsraft_threads[] = { [NEWSRAFT_THREAD_DOWNLOAD] = {0, PTHREAD_COND_INITIALIZER, PTHREAD_MUTEX_INITIALIZER, &downloader_worker}, [NEWSRAFT_THREAD_SHRUNNER] = {0, PTHREAD_COND_INITIALIZER, PTHREAD_MUTEX_INITIALIZER, &executor_worker}, [NEWSRAFT_THREAD_DBWRITER] = {0, PTHREAD_COND_INITIALIZER, PTHREAD_MUTEX_INITIALIZER, &inserter_worker}, }; bool threads_start(void) { for (size_t i = 0; i < LENGTH(newsraft_threads); ++i) { if (pthread_create(&newsraft_threads[i].thread, NULL, newsraft_threads[i].worker, NULL) != 0) { write_error("Failed to start a thread!\n"); return false; } } return true; } void threads_wake_up(int thread_id) { pthread_cond_signal(&newsraft_threads[thread_id].cond); if (thread_id == NEWSRAFT_THREAD_DOWNLOAD) { downloader_curl_wakeup(); } } void threads_take_a_nap(int thread_id) { pthread_mutex_lock(&newsraft_threads[thread_id].mutex); struct timespec wake_up_time = {0}; clock_gettime(CLOCK_REALTIME, &wake_up_time); wake_up_time.tv_sec += 1; pthread_cond_timedwait( &newsraft_threads[thread_id].cond, &newsraft_threads[thread_id].mutex, &wake_up_time ); pthread_mutex_unlock(&newsraft_threads[thread_id].mutex); } void threads_stop(void) { for (size_t i = 0; i < LENGTH(newsraft_threads); ++i) { threads_wake_up(i); pthread_join(newsraft_threads[i].thread, NULL); pthread_cond_destroy(&newsraft_threads[i].cond); } } newsraft/src/wstring-format.c000066400000000000000000000050741516312403600166340ustar00rootroot00000000000000#include #include #include "newsraft.h" #define FORMAT_TMP_BUF_SIZE 200 void do_format(struct wstring *dest, const wchar_t *fmt, const struct format_arg *args) { wchar_t tmp_buf[FORMAT_TMP_BUF_SIZE + 2]; wchar_t number[300]; empty_wstring(dest); for (const wchar_t *iter = fmt; *iter != '\0';) { if (iter[0] != L'%') { wcatcs(dest, *iter); iter += 1; continue; } else if (iter[1] == L'%') { // iter[0] and iter[1] are percent signs. wcatcs(dest, L'%'); iter += 2; continue; } const wchar_t *percent = iter; bool inserted = false; for (iter = iter + 1; inserted != true && *iter != L'%' && *iter != L'\0'; ++iter) { for (size_t j = 0; args[j].specifier != L'\0'; ++j) { if (*iter != args[j].specifier) { continue; } if (iter - percent > FORMAT_TMP_BUF_SIZE) { return; } memcpy(tmp_buf, percent, sizeof(wchar_t) * (iter - percent)); tmp_buf[iter - percent] = args[j].type_specifier; tmp_buf[iter - percent + 1] = L'\0'; if (args[j].type_specifier == L's') { // Can't rely on swprintf here because it doesn't take into account // double-width characters (for example Chinese characters or emojis). // When displaying these characters, they take two widths of regular // character and printf family of functions process strings based on // their character length rather than character width. struct wstring *ws = convert_array_to_wstring(args[j].value.s, strlen(args[j].value.s)); if (ws == NULL) { return; } long need_len = wcstol(tmp_buf + 1, NULL, 10); if (need_len != 0) { bool left_adjusted = need_len < 0 ? true : false; need_len = labs(need_len); while (wcswidth(ws->ptr, ws->len) > need_len) { ws->len -= 1; ws->ptr[ws->len] = L'\0'; } long whitespace_len = need_len - wcswidth(ws->ptr, ws->len); if (whitespace_len > 0 && whitespace_len <= need_len) { if (left_adjusted == true) { for (long i = 0; i < whitespace_len; ++i) { wcatcs(ws, L' '); } } else { struct wstring *new_ws = wcrtes(whitespace_len + ws->lim); for (long i = 0; i < whitespace_len; ++i) { wcatcs(new_ws, L' '); } wcatss(new_ws, ws); free_wstring(ws); ws = new_ws; } } } wcatss(dest, ws); free_wstring(ws); } else { // Format integer if (swprintf(number, LENGTH(number), tmp_buf, args[j].value.i) > 0) { wcatas(dest, number, wcslen(number)); } } inserted = true; break; } } } } newsraft/src/wstring.c000066400000000000000000000056521516312403600153500ustar00rootroot00000000000000#include #include #include "newsraft.h" // Note to the future. // When allocating memory, we request more resources than necessary to reduce // the number of further realloc calls to expand wstring buffer. void wstr_set(struct wstring **dest, const wchar_t *src_ptr, size_t src_len, size_t src_lim) { if (*dest == NULL) { struct wstring *wstr = newsraft_malloc(sizeof(struct wstring)); wstr->ptr = newsraft_malloc(sizeof(wchar_t) * (src_lim + 1)); if (src_ptr != NULL && src_len > 0) { memcpy(wstr->ptr, src_ptr, sizeof(wchar_t) * src_len); } *(wstr->ptr + src_len) = '\0'; wstr->len = src_len; wstr->lim = src_lim; *dest = wstr; } else { if (src_lim > (*dest)->lim) { (*dest)->ptr = newsraft_realloc((*dest)->ptr, sizeof(wchar_t) * (src_lim + 1)); (*dest)->lim = src_lim; } if (src_ptr != NULL && src_len > 0) { memcpy((*dest)->ptr, src_ptr, sizeof(wchar_t) * src_len); } *((*dest)->ptr + src_len) = '\0'; (*dest)->len = src_len; } } struct wstring * wcrtes(size_t desired_capacity) { struct wstring *wstr = NULL; wstr_set(&wstr, NULL, 0, desired_capacity); return wstr; } struct wstring * wcrtas(const wchar_t *src_ptr, size_t src_len) { struct wstring *wstr = NULL; wstr_set(&wstr, src_ptr, src_len, src_len); return wstr; } void wcatas(struct wstring *dest, const wchar_t *src_ptr, size_t src_len) { size_t new_len = dest->len + src_len; if (new_len > dest->lim) { size_t new_lim = new_len * 2 + 67; dest->ptr = newsraft_realloc(dest->ptr, sizeof(wchar_t) * (new_lim + 1)); dest->lim = new_lim; } if (src_ptr != NULL && src_len > 0) { memcpy(dest->ptr + dest->len, src_ptr, sizeof(wchar_t) * src_len); } *(dest->ptr + new_len) = L'\0'; dest->len = new_len; } void wcatss(struct wstring *dest, const struct wstring *src) { wcatas(dest, src->ptr, src->len); } void wcatcs(struct wstring *dest, wchar_t c) { wcatas(dest, &c, 1); } void make_sure_there_is_enough_space_in_wstring(struct wstring *dest, size_t need_space) { if (need_space > dest->lim - dest->len) { const size_t new_lim = dest->len + need_space; dest->ptr = newsraft_realloc(dest->ptr, sizeof(wchar_t) * (new_lim + 1)); dest->lim = new_lim; } } void empty_wstring(struct wstring *dest) { dest->len = 0; dest->ptr[0] = '\0'; } void free_wstring(struct wstring *wstr) { if (wstr != NULL) { free(wstr->ptr); free(wstr); } } struct string * convert_wstring_to_string(const struct wstring *src) { struct string *str = crtes(src->len * 5); if (str == NULL) { return NULL; } str->len = wcstombs(str->ptr, src->ptr, str->lim + 1); if (str->len == (size_t)-1) { free_string(str); return NULL; } str->ptr[str->len] = '\0'; return str; } struct string * convert_warray_to_string(const wchar_t *src_ptr, size_t src_len) { struct wstring *wstr = wcrtas(src_ptr, src_len); if (wstr == NULL) { return NULL; } struct string *str = convert_wstring_to_string(wstr); free_wstring(wstr); return str; } newsraft/tests/000077500000000000000000000000001516312403600140525ustar00rootroot00000000000000newsraft/tests/assets/000077500000000000000000000000001516312403600153545ustar00rootroot00000000000000newsraft/tests/assets/convert_feeds_to_opml.xml000066400000000000000000000035671516312403600224700ustar00rootroot00000000000000 Newsraft - Exported Feeds newsraft/tests/assets/convert_feeds_with_categories_to_opml.txt000066400000000000000000000011741516312403600257370ustar00rootroot00000000000000http://www.csmonitor.com/rss/top.rss "Christian Science Monitor | Top Stories" http://www.dictionary.com/wordoftheday/wotd.rss "Dictionary.com Word of the Day" @ Tech https://archlinux.org/feeds/news/ "archlinux" https://www.linuxjournal.com/node/feed "Linux Journal" http://lwn.net/headlines/newrss "lwn" https://openwrt.org/feed.php?type=rss&mode=list&sort=date&ns=advisory&linkto=current&content=html "OpenWrt Wiki - advisory" http://rss.news.yahoo.com/rss/tech "Yahoo! News: Technology" @ NYTimes http://www.nytimes.com/services/xml/rss/nyt/Business.xml "NYT > Business" http://www.nytimes.com/services/xml/rss/nyt/Technology.xml newsraft/tests/assets/convert_feeds_with_categories_to_opml.xml000066400000000000000000000026511516312403600257210ustar00rootroot00000000000000 Newsraft - Exported Feeds newsraft/tests/assets/convert_opml_to_feeds.txt000066400000000000000000000015761516312403600225050ustar00rootroot00000000000000http://news.com.com/2547-1_3-0-5.xml "CNET News.com" http://www.washingtonpost.com/wp-srv/politics/rssheadlines.xml "washingtonpost.com - Politics" http://radio.weblogs.com/0001011/rss.xml "Scobleizer: Microsoft Geek Blogger" http://rss.news.yahoo.com/rss/tech "Yahoo! News: Technology" http://www.cadenhead.org/workbench/rss.xml "Workbench" http://www.csmonitor.com/rss/top.rss "Christian Science Monitor | Top Stories" http://www.dictionary.com/wordoftheday/wotd.rss "Dictionary.com Word of the Day" http://www.fool.com/xml/foolnews_rss091.xml "The Motley Fool" http://www.infoworld.com/rss/news.xml "InfoWorld: Top News" http://www.nytimes.com/services/xml/rss/nyt/Business.xml "NYT > Business" http://www.nytimes.com/services/xml/rss/nyt/Technology.xml "NYT > Technology" http://www.scripting.com/rss.xml "Scripting News" http://www.wired.com/news_drop/netcenter/netcenter.rdf "Wired News" newsraft/tests/assets/convert_opml_to_feeds.xml000066400000000000000000000111171516312403600224560ustar00rootroot00000000000000 mySubscriptions.opml Sat, 18 Jun 2005 12:11:52 GMT Tue, 02 Aug 2005 21:42:48 GMT Dave Winer dave@scripting.com 1 61 304 562 842 newsraft/tests/assets/convert_opml_with_categories_to_feeds.txt000066400000000000000000000011741516312403600257370ustar00rootroot00000000000000http://www.csmonitor.com/rss/top.rss "Christian Science Monitor | Top Stories" http://www.dictionary.com/wordoftheday/wotd.rss "Dictionary.com Word of the Day" @ Tech https://archlinux.org/feeds/news/ "archlinux" https://www.linuxjournal.com/node/feed "Linux Journal" http://lwn.net/headlines/newrss "lwn" https://openwrt.org/feed.php?type=rss&mode=list&sort=date&ns=advisory&linkto=current&content=html "OpenWrt Wiki - advisory" http://rss.news.yahoo.com/rss/tech "Yahoo! News: Technology" @ NYTimes http://www.nytimes.com/services/xml/rss/nyt/Business.xml "NYT > Business" http://www.nytimes.com/services/xml/rss/nyt/Technology.xml newsraft/tests/assets/convert_opml_with_categories_to_feeds.xml000066400000000000000000000041371516312403600257220ustar00rootroot00000000000000 Categories newsraft/tests/assets/newsraft_json_parse_1.json000066400000000000000000000006261516312403600225470ustar00rootroot00000000000000{ "version": "https://jsonfeed.org/version/1.1", "title": "My Example Feed", "home_page_url": "https://example.org/", "feed_url": "https://example.org/feed.json", "items": [ { "id": "2", "content_text": "This is a second item.", "url": "https://example.org/second-item" }, { "id": "1", "content_html": "

Hello, world!

", "url": "https://example.org/initial-post" } ] } newsraft/tests/assets/newsraft_json_parse_2.json000066400000000000000000000032171516312403600225470ustar00rootroot00000000000000{ "version": "https://jsonfeed.org/version/1.1", "user_comment": "This is a podcast feed. You can add this feed to your podcast client using the following URL: http://therecord.co/feed.json", "title": "The Record", "home_page_url": "http://therecord.co/", "feed_url": "http://therecord.co/feed.json", "items": [ { "id": "http://therecord.co/chris-parrish", "title": "Special #1 - Chris Parrish", "url": "http://therecord.co/chris-parrish", "content_text": "Chris has worked at Adobe and as a founder of Rogue Sheep, which won an Apple Design Award for Postage. Chris’s new company is Aged & Distilled with Guy English — which shipped Napkin, a Mac app for visual collaboration. Chris is also the co-host of The Record. He lives on Bainbridge Island, a quick ferry ride from Seattle.", "content_html": "Chris has worked at Adobe and as a founder of Rogue Sheep, which won an Apple Design Award for Postage. Chris’s new company is Aged & Distilled with Guy English — which shipped Napkin, a Mac app for visual collaboration. Chris is also the co-host of The Record. He lives on Bainbridge Island, a quick ferry ride from Seattle.", "summary": "Brent interviews Chris Parrish, co-host of The Record and one-half of Aged & Distilled.", "date_published": "2014-05-09T14:04:00-07:00", "attachments": [ { "url": "http://therecord.co/downloads/The-Record-sp1e1-ChrisParrish.m4a", "mime_type": "audio/x-m4a", "size_in_bytes": 89970236, "duration_in_seconds": 6629 } ] } ] } newsraft/tests/assets/newsraft_json_parse_3.json000066400000000000000000000012131516312403600225420ustar00rootroot00000000000000{ "version": "https://jsonfeed.org/version/1.1", "user_comment": "This is a microblog feed. You can add this to your feed reader using the following URL: https://example.org/feed.json", "title": "Brent Simmons’s Microblog", "home_page_url": "https://example.org/", "feed_url": "https://example.org/feed.json", "authors": [ { "name": "Brent Simmons", "url": "http://example.org/", "avatar": "https://example.org/avatar.png" } ], "items": [ { "id": "2347259", "url": "https://example.org/2347259", "content_text": "Cats are neat. \n\nhttps://example.org/cats", "date_published": "2016-02-09T14:22:00-07:00" } ] } newsraft/tests/assets/parse_feeds_file.txt000066400000000000000000000045751516312403600214070ustar00rootroot00000000000000http://example.org/feed1.xml http://example.org/feed2.xml < reload-period 60 http://example.org/feed3.xml < set reload-period 60 http://example.org/feed4.xml "Feed 4 title" http://example.org/feed5.xml "Feed 5 title" < reload-period 120 http://example.org/feed6.xml < reload-period 180; item-limit 100 http://example.org/feed7.xml "Feed 7 title" < reload-period 111 ; item-limit 222 http://example.org/feed8.xml < reload-period 333 ;;; ; ; ;;;; item-limit 444 http://example.org/feed9.xml < proxy 127.0.0.1 $(curl foo://bar.baz/feed10) < proxy "localhost" $(curl foo://bar.baz/feed11) < proxy 'localhost.localdomain' $(curl foo://bar.baz/feed12) < proxy 127.0.0.1; $(curl foo://bar.baz/feed13) < proxy 127.0.0.1;; $(curl foo://bar.baz/feed14) < proxy "127.0.0.1"; $(curl foo://bar.baz/feed15) < proxy '127.0.0.1'; $(curl foo://bar.baz/feed16) < proxy "127.0.0.1";; $(curl foo://bar.baz/feed17) < proxy '127.0.0.1';; @ Section 1 $(curl foo://bar.baz/feed18) "" $(curl foo://bar.baz/feed19) "Feed 19 title" @ Section 2 < reload-period 360 $(curl foo://bar.baz/feed20) < reload-period 720 $(curl foo://bar.baz/feed21) < reload-period 0 $(curl foo://bar.baz/feed22) $(curl foo://bar.baz/feed23) < reload-period 180 $(curl foo://bar.baz/feed24) @ Section 3 < reload-period 180; item-limit 10000 $(curl foo://bar.baz/feed25) $(curl foo://bar.baz/feed26) "Feed 26 title" $(curl foo://bar.baz/feed27) "Feed 27 title" < item-limit 0 $(curl foo://bar.baz/feed28) < item-limit 5000; proxy '127.0.0.1'; reload-period 0 @ Section 4 $(feed.py foobar | cut -d ')' -f 1) $(feed.py foobar | cut -d '(' -f 2) $(feed.py foobar | cut -d ')' -f 3) "parentheses 1" $(feed.py foobar | cut -d '(' -f 4) "parentheses 2" $(feed.py foobar | cut -d ')' -f 5) "parentheses 3" < reload-period 60 $(feed.py foobar | cut -d '(' -f 6) "parentheses 4" < reload-period 60 $(feed.py foobar | cut -d ")" -f 7) $(feed.py foobar | cut -d "(" -f 8) $(feed.py foobar | cut -d ")" -f 9) "parentheses 5" $(feed.py foobar | cut -d "(" -f 10) "parentheses 6" $(feed.py foobar | cut -d ")" -f 11) "parentheses 7" < reload-period 60 $(feed.py foobar | cut -d "(" -f 12) "parentheses 8" < reload-period 60 $(feed.py foobar | cut -d \) -f 13) $(feed.py foobar | cut -d \( -f 14) $(echo "test" | grep "test") $(curl -s "http://example.com/feed.xml" | jq '.items[] | select(.type == "rss")') $(bash -c "echo $(date +%s)") newsraft/tests/complete_url.c000066400000000000000000000156161516312403600167210ustar00rootroot00000000000000#include #include "newsraft.h" static const char *test_cases[][3] = { // Base Relative Result {"http://example.org", "/", "http://example.org/"}, {"http://example.org/", "/", "http://example.org/"}, {"http://example.org/test", "/", "http://example.org/"}, {"http://example.org", "..", "http://example.org/"}, {"http://example.org/", "..", "http://example.org/"}, {"http://example.org/test", "..", "http://example.org/"}, {"http://example.org", "../..", "http://example.org/"}, {"http://example.org/", "../..", "http://example.org/"}, {"http://example.org/test", "../..", "http://example.org/"}, {"http://example.org", "../../..", "http://example.org/"}, {"http://example.org/", "../../..", "http://example.org/"}, {"http://example.org/test", "../../..", "http://example.org/"}, {"http://example.org/root", "/", "http://example.org/"}, {"http://example.org/root/", "/", "http://example.org/"}, {"http://example.org/root/test", "/", "http://example.org/"}, {"http://example.org/root", "..", "http://example.org/"}, {"http://example.org/root/", "..", "http://example.org/"}, {"http://example.org/root/test", "..", "http://example.org/"}, {"http://example.org/root", "../..", "http://example.org/"}, {"http://example.org/root/", "../..", "http://example.org/"}, {"http://example.org/root/test", "../..", "http://example.org/"}, {"http://example.org/root", "../../..", "http://example.org/"}, {"http://example.org/root/", "../../..", "http://example.org/"}, {"http://example.org/root/test", "../../..", "http://example.org/"}, {"http://example.org/root/q", "/", "http://example.org/"}, {"http://example.org/root/q/", "/", "http://example.org/"}, {"http://example.org/root/q/test", "/", "http://example.org/"}, {"http://example.org/root/q", "..", "http://example.org/"}, {"http://example.org/root/q/", "..", "http://example.org/root/"}, {"http://example.org/root/q/test", "..", "http://example.org/root/"}, {"http://example.org/root/q", "../..", "http://example.org/"}, {"http://example.org/root/q/", "../..", "http://example.org/"}, {"http://example.org/root/q/test", "../..", "http://example.org/"}, {"http://example.org/root/q", "../../..", "http://example.org/"}, {"http://example.org/root/q/", "../../..", "http://example.org/"}, {"http://example.org/root/q/test", "../../..", "http://example.org/"}, {"http://example.org", "index.html", "http://example.org/index.html"}, {"http://example.org/" , "index.html", "http://example.org/index.html"}, {"http://example.org/test", "index.html", "http://example.org/index.html"}, {"http://example.org", "/index.html", "http://example.org/index.html"}, {"http://example.org/" , "/index.html", "http://example.org/index.html"}, {"http://example.org/test", "/index.html", "http://example.org/index.html"}, {"http://example.org", "../index.html", "http://example.org/index.html"}, {"http://example.org/" , "../index.html", "http://example.org/index.html"}, {"http://example.org/test", "../index.html", "http://example.org/index.html"}, {"http://example.org", "../../index.html", "http://example.org/index.html"}, {"http://example.org/" , "../../index.html", "http://example.org/index.html"}, {"http://example.org/test", "../../index.html", "http://example.org/index.html"}, {"http://example.org/root", "index.html", "http://example.org/index.html"}, {"http://example.org/root/" , "index.html", "http://example.org/root/index.html"}, {"http://example.org/root/test", "index.html", "http://example.org/root/index.html"}, {"http://example.org/root", "/index.html", "http://example.org/index.html"}, {"http://example.org/root/" , "/index.html", "http://example.org/index.html"}, {"http://example.org/root/test", "/index.html", "http://example.org/index.html"}, {"http://example.org/root", "../index.html", "http://example.org/index.html"}, {"http://example.org/root/" , "../index.html", "http://example.org/index.html"}, {"http://example.org/root/test", "../index.html", "http://example.org/index.html"}, {"http://example.org/root", "../../index.html", "http://example.org/index.html"}, {"http://example.org/root/" , "../../index.html", "http://example.org/index.html"}, {"http://example.org/root/test", "../../index.html", "http://example.org/index.html"}, {"http://example.org/root/q", "index.html", "http://example.org/root/index.html"}, {"http://example.org/root/q/" , "index.html", "http://example.org/root/q/index.html"}, {"http://example.org/root/q/test", "index.html", "http://example.org/root/q/index.html"}, {"http://example.org/root/q", "/index.html", "http://example.org/index.html"}, {"http://example.org/root/q/" , "/index.html", "http://example.org/index.html"}, {"http://example.org/root/q/test", "/index.html", "http://example.org/index.html"}, {"http://example.org/root/q", "../index.html", "http://example.org/index.html"}, {"http://example.org/root/q/" , "../index.html", "http://example.org/root/index.html"}, {"http://example.org/root/q/test", "../index.html", "http://example.org/root/index.html"}, {"http://example.org/root/q", "../../index.html", "http://example.org/index.html"}, {"http://example.org/root/q/" , "../../index.html", "http://example.org/index.html"}, {"http://example.org/root/q/test", "../../index.html", "http://example.org/index.html"}, {NULL, NULL, NULL}, }; int main(void) { int status = 0; for (size_t i = 0; test_cases[i][0] != NULL; ++i) { char *full_url = complete_url(test_cases[i][0], test_cases[i][1]); if (strcmp(full_url, test_cases[i][2]) != 0) { fprintf(stderr, "Relative %zu: \"%s\" != \"%s\"\n", i, full_url, test_cases[i][2]); status = 1; } free(full_url); } const char *abs = "https://codeberg.org/newsraft/newsraft"; for (size_t i = 0; test_cases[i][0] != NULL; ++i) { char *full_url = complete_url(test_cases[i][0], abs); if (strcmp(full_url, abs) != 0) { fprintf(stderr, "Absolute %zu: \"%s\" != \"%s\"\n", i, full_url, abs); status = 1; } free(full_url); } return status; } newsraft/tests/convert_feeds_to_opml.c000066400000000000000000000017711516312403600206030ustar00rootroot00000000000000#include #include #include "newsraft.h" int main(void) { setlocale(LC_ALL, ""); set_db_path("./newsraft-test-database"); set_feeds_path("./tests/assets/convert_opml_to_feeds.txt"); db_init(); parse_feeds_file(); db_stop(); if (freopen("newsraft-test-feeds", "w", stdout) == NULL) { return 1; } convert_feeds_to_opml(); FILE *example_stream = fopen("tests/assets/convert_feeds_to_opml.xml", "r"); FILE *generated_stream = fopen("newsraft-test-feeds", "r"); if (example_stream == NULL || generated_stream == NULL) { return 1; } char example[10000]; char generated[10000]; size_t example_len = fread(example, 1, sizeof(example), example_stream); size_t generated_len = fread(generated, 1, sizeof(generated), generated_stream); example[example_len] = '\0'; generated[generated_len] = '\0'; fclose(example_stream); fclose(generated_stream); if (strcmp(example, generated) != 0) { fprintf(stderr, "Generated OPML file:\n%s\n", generated); return 1; } return 0; } newsraft/tests/convert_feeds_with_categories_to_opml.c000066400000000000000000000020311516312403600240310ustar00rootroot00000000000000#include #include #include "newsraft.h" int main(void) { setlocale(LC_ALL, ""); set_db_path("./newsraft-test-database"); set_feeds_path("./tests/assets/convert_feeds_with_categories_to_opml.txt"); db_init(); parse_feeds_file(); db_stop(); if (freopen("newsraft-test-feeds", "w", stdout) == NULL) { return 1; } convert_feeds_to_opml(); FILE *example_stream = fopen("tests/assets/convert_feeds_with_categories_to_opml.xml", "r"); FILE *generated_stream = fopen("newsraft-test-feeds", "r"); if (example_stream == NULL || generated_stream == NULL) { return 1; } char example[10000]; char generated[10000]; size_t example_len = fread(example, 1, sizeof(example), example_stream); size_t generated_len = fread(generated, 1, sizeof(generated), generated_stream); example[example_len] = '\0'; generated[generated_len] = '\0'; fclose(example_stream); fclose(generated_stream); if (strcmp(example, generated) != 0) { fprintf(stderr, "Generated OPML file:\n%s\n", generated); return 1; } return 0; } newsraft/tests/convert_opml_to_feeds.c000066400000000000000000000016261516312403600206020ustar00rootroot00000000000000#include #include "newsraft.h" int main(void) { if (freopen("tests/assets/convert_opml_to_feeds.xml", "r", stdin) == NULL) { return 1; } if (freopen("newsraft-test-feeds", "w", stdout) == NULL) { return 1; } convert_opml_to_feeds(); FILE *example_stream = fopen("tests/assets/convert_opml_to_feeds.txt", "r"); FILE *generated_stream = fopen("newsraft-test-feeds", "r"); if (example_stream == NULL || generated_stream == NULL) { return 1; } char example[10000]; char generated[10000]; size_t example_len = fread(example, 1, sizeof(example), example_stream); size_t generated_len = fread(generated, 1, sizeof(generated), generated_stream); example[example_len] = '\0'; generated[generated_len] = '\0'; fclose(example_stream); fclose(generated_stream); if (strcmp(example, generated) != 0) { fprintf(stderr, "Generated feeds file:\n%s\n", generated); return 1; } return 0; } newsraft/tests/convert_opml_with_categories_to_feeds.c000066400000000000000000000016661516312403600240460ustar00rootroot00000000000000#include #include "newsraft.h" int main(void) { if (freopen("tests/assets/convert_opml_with_categories_to_feeds.xml", "r", stdin) == NULL) { return 1; } if (freopen("newsraft-test-feeds", "w", stdout) == NULL) { return 1; } convert_opml_to_feeds(); FILE *example_stream = fopen("tests/assets/convert_opml_with_categories_to_feeds.txt", "r"); FILE *generated_stream = fopen("newsraft-test-feeds", "r"); if (example_stream == NULL || generated_stream == NULL) { return 1; } char example[10000]; char generated[10000]; size_t example_len = fread(example, 1, sizeof(example), example_stream); size_t generated_len = fread(generated, 1, sizeof(generated), generated_stream); example[example_len] = '\0'; generated[generated_len] = '\0'; fclose(example_stream); fclose(generated_stream); if (strcmp(example, generated) != 0) { fprintf(stderr, "Generated feeds file:\n%s\n", generated); return 1; } return 0; } newsraft/tests/do_format.c000066400000000000000000000102301516312403600161640ustar00rootroot00000000000000#include #include "newsraft.h" static bool fmt_fails(struct wstring *out, const wchar_t *fmt, struct format_arg *args, const wchar_t *expect) { do_format(out, fmt, args); if (wcscmp(out->ptr, expect) != 0) { fprintf(stderr, "\"%ls\" != \"%ls\"\n", out->ptr, expect); return true; } return false; } int main(void) { setlocale(LC_ALL, ""); struct wstring *w = wcrtes(200); struct format_arg args[] = { {L'n', L'd', {.i = 567 }}, {L'i', L's', {.s = "The Box" }}, {L'l', L's', {.s = "Se7en" }}, {L't', L's', {.s = "Cars" }}, {L'o', L's', {.s = NULL }}, {L'O', L's', {.s = "The Truman Show" }}, {L'b', L's', {.s = "Shrek" }}, {L'f', L'd', {.i = 112263 }}, {L'e', L's', {.s = "Алёша Попович и Тугарин Змей"}}, {L'd', L's', {.s = "The Usual Suspects" }}, {L'h', L's', {.s = "英雄" }}, {L'u', L's', {.s = NULL }}, {L'z', L's', {.s = "Dead Poets Society" }}, {L'a', L's', {.s = "Scarface" }}, {L'm', L's', {.s = "Остров сокровищ" }}, {L'q', L's', {.s = "Big" }}, {L'c', L's', {.s = "The Ballad of Buster Scruggs"}}, {L'p', L's', {.s = "The Green Mile" }}, {L's', L's', {.s = "伍六七" }}, {L'g', L's', {.s = "Ворошиловский стрелок" }}, {L'j', L's', {.s = "The Shawshank Redemption" }}, {L'y', L's', {.s = NULL }}, {L'k', L's', {.s = "Knockin' on Heaven's Door" }}, {L'w', L's', {.s = "Брат" }}, {L'v', L's', {.s = "Wag the Dog" }}, {L'x', L's', {.s = "The Man from Earth" }}, {L'\0', L'\0', {.i = 0 }}, }; if (fmt_fails(w, L"%n%l%f", args, L"567Se7en112263")) goto error; if (fmt_fails(w, L"%v%%%p", args, L"Wag the Dog%The Green Mile")) goto error; if (fmt_fails(w, L"%%%d%%", args, L"%The Usual Suspects%")) goto error; if (fmt_fails(w, L"%m%", args, L"Остров сокровищ")) goto error; if (fmt_fails(w, L"%x%%", args, L"The Man from Earth%")) goto error; if (fmt_fails(w, L"%%%%%%%%%", args, L"%%%%")) goto error; if (fmt_fails(w, L" %q %w ", args, L" Big Брат ")) goto error; if (fmt_fails(w, L" %q%%w ", args, L" Big%w ")) goto error; if (fmt_fails(w, L"%s %h %n", args, L"伍六七 英雄 567")) goto error; if (fmt_fails(w, L"%bгыгы%f", args, L"Shrekгыгы112263")) goto error; if (fmt_fails(w, L"гугу%t%w", args, L"гугуCarsБрат")) goto error; if (fmt_fails(w, L"%a%qгого", args, L"ScarfaceBigгого")) goto error; if (fmt_fails(w, L"%13c %10z", args, L"The Ballad of Dead Poets")) goto error; if (fmt_fails(w, L"%13e", args, L"Алёша Попович")) goto error; if (fmt_fails(w, L"%-13e", args, L"Алёша Попович")) goto error; if (fmt_fails(w, L"%10s", args, L" 伍六七")) goto error; if (fmt_fails(w, L"%-10s", args, L"伍六七 ")) goto error; if (fmt_fails(w, L"%6s", args, L"伍六七")) goto error; if (fmt_fails(w, L"%-6s", args, L"伍六七")) goto error; if (fmt_fails(w, L"%-5s", args, L"伍六 ")) goto error; if (fmt_fails(w, L"%5s", args, L" 伍六")) goto error; if (fmt_fails(w, L"%3h", args, L" 英")) goto error; if (fmt_fails(w, L"%-3h", args, L"英 ")) goto error; if (fmt_fails(w, L"%2h", args, L"英")) goto error; if (fmt_fails(w, L"%-2h", args, L"英")) goto error; free_wstring(w); return 0; error: free_wstring(w); return 1; } newsraft/tests/inlinefy_string.c000066400000000000000000000012621516312403600174220ustar00rootroot00000000000000#include #include "newsraft.h" static const char *test_cases[][2] = { // Input Output {"King Stephen", "King Stephen"}, {"\t \n Hensonn", " Hensonn"}, {"OFFL1NX \r \n", "OFFL1NX "}, {" \n Vakhtang \n", " Vakhtang "}, {"Hollywood\n \n\t\tBurns", "Hollywood Burns"}, {NULL, NULL}, }; int main(void) { struct string *str = crtes(100); for (size_t i = 0; test_cases[i][0] != NULL; ++i) { cpyas(&str, test_cases[i][0], strlen(test_cases[i][0])); inlinefy_string(str); if (strcmp(str->ptr, test_cases[i][1]) != 0) { free_string(str); return 1; } } free_string(str); return 0; } newsraft/tests/newsraft_all_bits_set.c000066400000000000000000000012531516312403600205740ustar00rootroot00000000000000#include "newsraft.h" int main(void) { uint8_t a1 = 0xFF; uint16_t a2 = 0xFFFF; uint32_t a4 = 0xFFFFFFFF; uint64_t a8 = 0xFFFFFFFFFFFFFFFF; if (!NEWSRAFT_ALL_BITS_SET(a1, uint8_t)) return 1; if (!NEWSRAFT_ALL_BITS_SET(a2, uint16_t)) return 1; if (!NEWSRAFT_ALL_BITS_SET(a4, uint32_t)) return 1; if (!NEWSRAFT_ALL_BITS_SET(a8, uint64_t)) return 1; uint8_t b1 = 0xFE; uint16_t b2 = 0xFFFE; uint32_t b4 = 0xFFFFFFFE; uint64_t b8 = 0xFFFFFFFFFFFFFFFE; if (NEWSRAFT_ALL_BITS_SET(b1, uint8_t)) return 1; if (NEWSRAFT_ALL_BITS_SET(b2, uint16_t)) return 1; if (NEWSRAFT_ALL_BITS_SET(b4, uint32_t)) return 1; if (NEWSRAFT_ALL_BITS_SET(b8, uint64_t)) return 1; return 0; } newsraft/tests/newsraft_base64_encode.c000066400000000000000000000026761516312403600205430ustar00rootroot00000000000000#include #include "newsraft.h" static const char *test_cases[][2] = { // Input Output {"a", "YQ=="}, {"z", "eg=="}, {"0", "MA=="}, {"9", "OQ=="}, {"Saltburn", "U2FsdGJ1cm4="}, {"I Care a Lot", "SSBDYXJlIGEgTG90"}, {"Suspiria", "U3VzcGlyaWE="}, {"Chinese Coffee", "Q2hpbmVzZSBDb2ZmZWU="}, {"The Killing of a Sacred Deer", "VGhlIEtpbGxpbmcgb2YgYSBTYWNyZWQgRGVlcg=="}, {"Whiplash", "V2hpcGxhc2g="}, {"Scent of a Woman", "U2NlbnQgb2YgYSBXb21hbg=="}, {"I'm Thinking of Ending Things", "SSdtIFRoaW5raW5nIG9mIEVuZGluZyBUaGluZ3M="}, {"Gone Girl", "R29uZSBHaXJs"}, {"Requiem for a Dream", "UmVxdWllbSBmb3IgYSBEcmVhbQ=="}, {"Midsommar", "TWlkc29tbWFy"}, {"The Banshees of Inisherin", "VGhlIEJhbnNoZWVzIG9mIEluaXNoZXJpbg=="}, {NULL, NULL}, }; int main(void) { size_t errors = 0; for (size_t i = 0; test_cases[i][0] != NULL; ++i) { struct string *out = newsraft_base64_encode((uint8_t *)test_cases[i][0], strlen(test_cases[i][0])); if (strcmp(out->ptr, test_cases[i][1]) != 0) { fprintf(stderr, "\"%s\" != \"%s\"\n", out->ptr, test_cases[i][1]); errors += 1; } free_string(out); } return errors ? 1 : 0; } newsraft/tests/newsraft_json_parse.c000066400000000000000000000076211516312403600203000ustar00rootroot00000000000000#include #include "newsraft.h" int test1(const char *content, size_t size) { struct feed_update_state data = {}; if (!newsraft_json_parse(&data, content, size)) { return 1; } if (strcmp(data.feed.title->ptr, "My Example Feed") != 0) return 1; if (strcmp(data.feed.link->ptr, "https://example.org/") != 0) return 1; size_t items_count = 0; for (struct getfeed_item *i = data.feed.item; i != NULL; i = i->next) { items_count += 1; } if (items_count != 2) return 1; struct getfeed_item *item1 = data.feed.item; struct getfeed_item *item2 = data.feed.item->next; if (strcmp(item1->guid->ptr, "1") != 0) return 1; if (strcmp(item2->guid->ptr, "2") != 0) return 1; if (strcmp(item1->link->ptr, "https://example.org/initial-post") != 0) return 1; if (strcmp(item2->link->ptr, "https://example.org/second-item") != 0) return 1; if (strcmp(item1->content->ptr, "\x1F^\x1Ftype=text/html\x1Ftext=

Hello, world!

") != 0) return 1; if (strcmp(item2->content->ptr, "\x1F^\x1Ftext=This is a second item.") != 0) return 1; return 0; } int test2(const char *content, size_t size) { struct feed_update_state data = {}; if (!newsraft_json_parse(&data, content, size)) { return 1; } if (strcmp(data.feed.title->ptr, "The Record") != 0) return 1; if (strcmp(data.feed.link->ptr, "http://therecord.co/") != 0) return 1; size_t items_count = 0; for (struct getfeed_item *i = data.feed.item; i != NULL; i = i->next) { items_count += 1; } if (items_count != 1) return 1; struct getfeed_item *item = data.feed.item; if (strcmp(item->guid->ptr, "http://therecord.co/chris-parrish") != 0) return 1; if (strcmp(item->title->ptr, "Special #1 - Chris Parrish") != 0) return 1; if (strcmp(item->link->ptr, "http://therecord.co/chris-parrish") != 0) return 1; if (item->publication_date != 1399669440LL) return 1; size_t texts_count = 0; for (const char *i = item->content->ptr; true; ++texts_count) { i = strstr(i + 1, "\x1Ftext="); if (i == NULL) { break; } } if (texts_count != 3) return 1; if (strstr(item->content->ptr, "type=text/html") == NULL) return 1; if (strstr(item->attachments->ptr, "url=http://therecord.co/downloads/The-Record-sp1e1-ChrisParrish.m4a") == NULL) return 1; if (strstr(item->attachments->ptr, "type=audio/x-m4a") == NULL) return 1; if (strstr(item->attachments->ptr, "size=89970236") == NULL) return 1; if (strstr(item->attachments->ptr, "duration=6629") == NULL) return 1; return 0; } int test3(const char *content, size_t size) { struct feed_update_state data = {}; if (!newsraft_json_parse(&data, content, size)) { return 1; } if (strcmp(data.feed.title->ptr, "Brent Simmons’s Microblog") != 0) return 1; if (strcmp(data.feed.link->ptr, "https://example.org/") != 0) return 1; if (strcmp(data.feed.persons->ptr, "\x1F^\x1Ftype=author\x1Fname=Brent Simmons\x1Furl=http://example.org/") != 0) return 1; size_t items_count = 0; for (struct getfeed_item *i = data.feed.item; i != NULL; i = i->next) { items_count += 1; } if (items_count != 1) return 1; struct getfeed_item *item = data.feed.item; if (strcmp(item->guid->ptr, "2347259") != 0) return 1; if (strcmp(item->link->ptr, "https://example.org/2347259") != 0) return 1; if (strcmp(item->content->ptr, "\x1F^\x1Ftext=Cats are neat. \n\nhttps://example.org/cats") != 0) return 1; if (item->publication_date != 1455052920LL) return 1; return 0; } int main(void) { const char *files[] = { "tests/assets/newsraft_json_parse_1.json", "tests/assets/newsraft_json_parse_2.json", "tests/assets/newsraft_json_parse_3.json", }; char jsons[3][2000] = {}; size_t lens[3] = {}; for (size_t i = 0; i < 3; ++i) { FILE *f = fopen(files[i], "r"); if (f == NULL) { return 1; } lens[i] = fread(jsons[i], 1, sizeof(jsons[i]), f); jsons[i][lens[i]] = '\0'; fclose(f); } if (test1(jsons[0], lens[0])) return 1; if (test2(jsons[1], lens[1])) return 1; if (test3(jsons[2], lens[2])) return 1; return 0; } newsraft/tests/newsraft_simple_hash.c000066400000000000000000000013761516312403600204320ustar00rootroot00000000000000#include #include "newsraft.h" static const char *test_cases[][2] = { {"Charlie and the Chocolate Factory", ")c+Dbu!i%KCp<+C83v{/3V6p5c${H0M`6=t.UbNq 0b(%xSX<~63v/;DkkHI%-t*~9%cMmWmC~-tW6s"}, {"And Justice for All", "6,)T(sc}_B{T=}D=l|whQrJ6g;i59HN:QT&&f}.?mO7;/ptr, test_cases[i][1]) != 0) { printf("%s != %s\n", str->ptr, test_cases[i][1]); return 1; } } free_string(str); return 0; } newsraft/tests/parse_date.c000066400000000000000000000014541516312403600163310ustar00rootroot00000000000000#include "newsraft.h" static bool test_date(const char *date, int64_t true_time) { int64_t test_time = parse_date(date, false); if (test_time == true_time) { return true; } else { fprintf(stderr, "Mismatch for %s: %lld != %lld\n", date, test_time, true_time); return false; } } int main(void) { if (test_date("1996-12-19T16:39:57-08:00", 851042397) == false) return 1; if (test_date("1990-12-31T23:59:60Z", 662688000) == false) return 1; if (test_date("1990-12-31T15:59:60-08:00", 662688000) == false) return 1; if (test_date("Sun, 05 May 2024 17:53:44 +0200", 1714924424) == false) return 1; if (test_date("1978-07-03", /* YYYY-MM-DD */ 268272000) == false) return 1; if (test_date("1978/07/03", /* YYYY/MM/DD */ 268272000) == false) return 1; return 0; } newsraft/tests/parse_feeds_file.c000066400000000000000000000130461516312403600175010ustar00rootroot00000000000000#include #include #include "newsraft.h" struct feed_test { const char *url; const char *name; size_t reload_period; size_t item_limit; const char *proxy; }; struct feed_test feed_tests[] = { {"http://example.org/feed1.xml", NULL, 0, 0, ""}, {"http://example.org/feed2.xml", NULL, 60, 0, ""}, {"http://example.org/feed3.xml", NULL, 60, 0, ""}, {"http://example.org/feed4.xml", "Feed 4 title", 0, 0, ""}, {"http://example.org/feed5.xml", "Feed 5 title", 120, 0, ""}, {"http://example.org/feed6.xml", NULL, 180, 100, ""}, {"http://example.org/feed7.xml", "Feed 7 title", 111, 222, ""}, {"http://example.org/feed8.xml", NULL, 333, 444, ""}, {"http://example.org/feed9.xml", NULL, 0, 0, "127.0.0.1"}, {"$(curl foo://bar.baz/feed10)", NULL, 0, 0, "localhost"}, {"$(curl foo://bar.baz/feed11)", NULL, 0, 0, "localhost.localdomain"}, {"$(curl foo://bar.baz/feed12)", NULL, 0, 0, "127.0.0.1"}, {"$(curl foo://bar.baz/feed13)", NULL, 0, 0, "127.0.0.1"}, {"$(curl foo://bar.baz/feed14)", NULL, 0, 0, "127.0.0.1"}, {"$(curl foo://bar.baz/feed15)", NULL, 0, 0, "127.0.0.1"}, {"$(curl foo://bar.baz/feed16)", NULL, 0, 0, "127.0.0.1"}, {"$(curl foo://bar.baz/feed17)", NULL, 0, 0, "127.0.0.1"}, // Section 1 {"$(curl foo://bar.baz/feed18)", NULL, 0, 0, ""}, {"$(curl foo://bar.baz/feed19)", "Feed 19 title", 0, 0, ""}, // Section 2 {"$(curl foo://bar.baz/feed20)", NULL, 720, 0, ""}, {"$(curl foo://bar.baz/feed21)", NULL, 0, 0, ""}, {"$(curl foo://bar.baz/feed22)", NULL, 360, 0, ""}, {"$(curl foo://bar.baz/feed23)", NULL, 180, 0, ""}, {"$(curl foo://bar.baz/feed24)", NULL, 360, 0, ""}, // Section 3 {"$(curl foo://bar.baz/feed25)", NULL, 180, 10000, ""}, {"$(curl foo://bar.baz/feed26)", "Feed 26 title", 180, 10000, ""}, {"$(curl foo://bar.baz/feed27)", "Feed 27 title", 180, 0, ""}, {"$(curl foo://bar.baz/feed28)", NULL, 0, 5000, "127.0.0.1"}, // Section 4 {"$(feed.py foobar | cut -d ')' -f 1)", NULL, 0, 0, ""}, {"$(feed.py foobar | cut -d '(' -f 2)", NULL, 0, 0, ""}, {"$(feed.py foobar | cut -d ')' -f 3)", "parentheses 1", 0, 0, ""}, {"$(feed.py foobar | cut -d '(' -f 4)", "parentheses 2", 0, 0, ""}, {"$(feed.py foobar | cut -d ')' -f 5)", "parentheses 3", 60, 0, ""}, {"$(feed.py foobar | cut -d '(' -f 6)", "parentheses 4", 60, 0, ""}, {"$(feed.py foobar | cut -d \")\" -f 7)", NULL, 0, 0, ""}, {"$(feed.py foobar | cut -d \"(\" -f 8)", NULL, 0, 0, ""}, {"$(feed.py foobar | cut -d \")\" -f 9)", "parentheses 5", 0, 0, ""}, {"$(feed.py foobar | cut -d \"(\" -f 10)", "parentheses 6", 0, 0, ""}, {"$(feed.py foobar | cut -d \")\" -f 11)", "parentheses 7", 60, 0, ""}, {"$(feed.py foobar | cut -d \"(\" -f 12)", "parentheses 8", 60, 0, ""}, {"$(feed.py foobar | cut -d \\) -f 13)", NULL, 0, 0, ""}, {"$(feed.py foobar | cut -d \\( -f 14)", NULL, 0, 0, ""}, {"$(echo \"test\" | grep \"test\")", NULL, 0, 0, ""}, { "$(curl -s \"http://example.com/feed.xml\" | jq '.items[] | select(.type == \"rss\")')", NULL, 0, 0, "" }, { "$(bash -c \"echo $(date +%s)\")", NULL, 0, 0, "" }, }; int main(void) { int status = 0; setlocale(LC_ALL, ""); set_db_path("./newsraft-test-database"); set_feeds_path("./tests/assets/parse_feeds_file.txt"); db_init(); parse_feeds_file(); db_stop(); size_t feeds_count = 0; struct feed_entry **feeds = get_all_feeds(&feeds_count); if (feeds_count != LENGTH(feed_tests)) { fprintf(stderr, "Feeds count %zu != %zu\n", feeds_count, LENGTH(feed_tests)); return 1; } size_t i; for (i = 0; status == 0 && i < feeds_count; ++i) { size_t reload_period = get_cfg_uint(&feeds[i]->cfg, CFG_RELOAD_PERIOD); size_t item_limit = get_cfg_uint(&feeds[i]->cfg, CFG_ITEM_LIMIT); const char *proxy = get_cfg_string(&feeds[i]->cfg, CFG_PROXY)->ptr; if (strcmp(feeds[i]->url->ptr, feed_tests[i].url) != 0) { fprintf(stderr, "URL: %s != %s\n", feeds[i]->url->ptr, feed_tests[i].url); status = 1; } if (feeds[i]->name != NULL && feed_tests[i].name != NULL && strcmp(feeds[i]->name->ptr, feed_tests[i].name) != 0) { fprintf(stderr, "Name: %s != %s\n", feeds[i]->name->ptr, feed_tests[i].name); status = 1; } if (reload_period != feed_tests[i].reload_period) { fprintf(stderr, "Reload period: %zu != %zu\n", reload_period, feed_tests[i].reload_period); status = 1; } if (item_limit != feed_tests[i].item_limit) { fprintf(stderr, "Item limit: %zu != %zu\n", item_limit, feed_tests[i].item_limit); status = 1; } if (strcmp(proxy, feed_tests[i].proxy) != 0) { fprintf(stderr, "Proxy: %s != %s\n", proxy, feed_tests[i].proxy); status = 1; } } if (status != 0) { fprintf(stderr, "Feed %zu failed!\n", i); } return status; } newsraft/tests/render_data.c000066400000000000000000000065401516312403600164730ustar00rootroot00000000000000#include #include "newsraft.h" struct block_sample { const char *data; render_block_format type; bool needs_trimming; }; struct render_data_test { struct block_sample blocks[5]; size_t blocks_count; wchar_t *lines[5]; size_t lines_count; }; const struct render_data_test render_tests[] = { /* 0 */ {{ {"Hello, world!", TEXT_PLAIN, 0} }, 1, {L"Hello, world!", NULL, NULL, NULL }, 1}, /* 1 */ {{ {"Hello, world!", TEXT_PLAIN, 1} }, 1, {L"Hello, world!", NULL, NULL, NULL }, 1}, /* 2 */ {{ {"Hello, world!\n", TEXT_PLAIN, 0} }, 1, {L"Hello, world!", NULL, NULL, NULL }, 1}, /* 3 */ {{ {"Hello, world!\n", TEXT_PLAIN, 1} }, 1, {L"Hello, world!", NULL, NULL, NULL }, 1}, /* 4 */ {{ {"Hello, world!", TEXT_PLAIN, 0}, {"Test!", TEXT_PLAIN, 0} }, 2, {L"Hello, world!Test!", NULL, NULL, NULL }, 1}, /* 5 */ {{ {"Hello, world!", TEXT_PLAIN, 0}, {"Test!", TEXT_PLAIN, 1} }, 2, {L"Hello, world!Test!", NULL, NULL, NULL }, 1}, /* 6 */ {{ {"Hello, world!", TEXT_PLAIN, 1}, {"Test!", TEXT_PLAIN, 0} }, 2, {L"Hello, world!Test!", NULL, NULL, NULL }, 1}, /* 7 */ {{ {"Hello, world!", TEXT_PLAIN, 1}, {"Test!", TEXT_PLAIN, 1} }, 2, {L"Hello, world!Test!", NULL, NULL, NULL }, 1}, /* 8 */ {{ {"Hello, world!\n", TEXT_PLAIN, 0}, {"Test!\n", TEXT_PLAIN, 0} }, 2, {L"Hello, world!", L"Test!", NULL, NULL }, 2}, /* 9 */ {{ {"Hello, world!\n", TEXT_PLAIN, 0}, {"Test!\n", TEXT_PLAIN, 1} }, 2, {L"Hello, world!", L"Test!", NULL, NULL }, 2}, /* 10 */ {{ {"Hello, world!\n", TEXT_PLAIN, 1}, {"Test!\n", TEXT_PLAIN, 0} }, 2, {L"Hello, world!Test!", NULL, NULL, NULL }, 1}, /* 11 */ {{ {"A\nB\nC\nD", TEXT_PLAIN, 0} }, 1, {L"A", L"B", L"C", L"D" }, 4}, /* 12 */ {{ {"A\nB\nC\nD\n", TEXT_PLAIN, 0} }, 1, {L"A", L"B", L"C", L"D" }, 4}, /* 13 */ {{ {"A\n\nC\nD\n", TEXT_PLAIN, 0} }, 1, {L"A", L"", L"C", L"D" }, 4}, /* 14 */ {{ {"A\nB\n\nD\n", TEXT_PLAIN, 0} }, 1, {L"A", L"B", L"", L"D" }, 4}, }; int main(void) { struct render_blocks_list blocks = {0}; struct render_result result = {0}; for (size_t i = 0; i < sizeof(render_tests) / sizeof(*render_tests); ++i) { memset(&blocks, 0, sizeof(struct render_blocks_list)); memset(&result, 0, sizeof(struct render_result)); for (size_t j = 0; j < render_tests[i].blocks_count; ++j) { add_render_block( &blocks, render_tests[i].blocks[j].data, strlen(render_tests[i].blocks[j].data), render_tests[i].blocks[j].type, render_tests[i].blocks[j].needs_trimming ); } render_data(NULL, &result, &blocks, 80); if (result.lines_len != render_tests[i].lines_count) { fprintf(stderr, "%s: test %zu fails (invalid number of lines)!\n", __FILE__, i); return 1; } for (size_t k = 0; k < result.lines_len; ++k) { if (wcscmp(result.lines[k].ws->ptr, render_tests[i].lines[k]) != 0) { fprintf(stderr, "%s: test %zu fails (invalid lines content)!\n", __FILE__, i); return 1; } } } return 0; } newsraft/tests/run-check.sh000077500000000000000000000016121516312403600162700ustar00rootroot00000000000000#!/bin/sh set -e log_file='newsraft-test-log' green="$(printf '\x1b[32m')" red="$(printf '\x1b[31m')" reset="$(printf '\x1b[0m')" cd "$(dirname "$0")/.." make CFLAGS='-O3 -fPIC' libnewsraft.so test -e libnewsraft.so rm -f "$log_file" echo for test_file in tests/*.c; do status=0 rm -rf newsraft-test-database* make TEST_FILE="$test_file" test-program 1>>"$log_file" 2>&1 || status="$?" [ "$status" = 0 ] && env LD_LIBRARY_PATH=. ./newsraft-test 2>&1 || status="$?" echo "TEST_STATUS:$status" >> "$log_file" echo "[$([ "$status" = 0 ] && echo "${green}OKAY" || echo "${red}FAIL")${reset}] $test_file" done | tee -a "$log_file" okays_count="$(grep -c TEST_STATUS:0 "$log_file")" tests_count="$(grep -c TEST_STATUS: "$log_file")" echo echo "$okays_count/$tests_count TESTS PASSED" echo if [ "$okays_count" != "$tests_count" ]; then echo "Look for details in the ./$log_file" echo exit 1 fi newsraft/tests/serialize_array.c000066400000000000000000000034141516312403600174050ustar00rootroot00000000000000#include #include "newsraft.h" struct serialize_test { const char *key; size_t key_len; const char *value; size_t value_len; const char *result; }; static const struct serialize_test tests[] = { {"a=", 2, "test0", 5, "\x1F" "a=test0"}, {"b=", 2, "\x1F" "test1", 6, "\x1F" "b=test1"}, {"c=", 2, "te" "\x1F" "st2", 6, "\x1F" "c=test2"}, {"d=", 2, "test3" "\x1F", 6, "\x1F" "d=test3"}, {"e=", 2, "\x1F\x1F" "test4", 7, "\x1F" "e=test4"}, {"f=", 2, "te" "\x1F\x1F" "st5", 7, "\x1F" "f=test5"}, {"g=", 2, "test6" "\x1F\x1F", 7, "\x1F" "g=test6"}, {"h=", 2, "", 0, ""}, {"i=", 2, "\x1F\x1F\x1F\x1F\x1F", 5, ""}, {"j=", 2, "\x1F" "test7" "\x1F\x1F", 8, "\x1F" "j=test7"}, {"k=", 2, "\x1F" "te" "\x1F" "st8" "\x1F", 8, "\x1F" "k=test8"}, {"l=", 2, "t" "\x1F" "e" "\x1F" "s" "\x1F" "t" "\x1F" "9", 9, "\x1F" "l=test9"}, {"m=", 2, "\x1F" "t" "\x1F" "e" "\x1F" "s" "\x1F" "t" "\x1F" "A" "\x1F", 11, "\x1F" "m=testA"}, }; int main(void) { struct string *out = crtes(100); for (size_t i = 0; i < sizeof(tests) / sizeof(*tests); ++i) { serialize_array(&out, tests[i].key, tests[i].key_len, tests[i].value, tests[i].value_len); if (strcmp(out->ptr, tests[i].result) != 0) { fprintf(stderr, "Test %zu failed!\n", i); return 1; } empty_string(out); } return 0; } newsraft/tests/trim_whitespace_from_string.c000066400000000000000000000015041516312403600220160ustar00rootroot00000000000000#include #include "newsraft.h" static const char *test_cases[][2] = { {"The Devil's Advocate", "The Devil's Advocate"}, {"Monsters, Inc.", "Monsters, Inc."}, {"The Terminal", "The Terminal"}, {"消失的她", " 消失的她"}, {"The Departed", "The Departed "}, {"影", "\t \n \t 影 \t \r \t"}, {"Black Swan", " Black Swan "}, {"El hoyo", " El hoyo "}, {NULL, NULL}, }; int main(void) { struct string *str = crtes(100); for (size_t i = 0; test_cases[i][0] != NULL; ++i) { cpyas(&str, test_cases[i][1], strlen(test_cases[i][1])); trim_whitespace_from_string(str); if (strcmp(str->ptr, test_cases[i][0]) != 0) { free_string(str); return 1; } } free_string(str); return 0; }