An RSS/Atom newsreader that sucks less

Canto | Configuration

Updated for 0.7.x. Older versions here
HOMENEWSFAQSCREENSHOTSDOWNLOADGETTING STARTEDCONFIGGITWEBBUG TRACKERCONTACT

This is where you learn the details of ~/.canto/conf.py. For the impatient, you can skip to the example config.

If you're interested in more programmer-centric customization and aren't afraid of getting your hands dirty with some Python, then you may also be interested in advanced configuration.

Configuration

This section covers Canto's basics features and how to use filters, sorts, tags, and the other pre-written goodies that can be found in canto.extra. The actual writing of custom content is covered in later sections. This is all intended to be put into ~/.canto/conf.py (conf, without the extension, is acceptable as well).

Adding feeds

add()

add is the basic building block of Canto's config. As the name suggests, it adds a feed to the config. Ninety-nine percent of the time, a call like this will get the job done:

add("http://someurl")

You can also tweak some other settings having to do with fetching the feed. The rate and keep variables effect the rate at which the feed is fetched from the server and keep determines how many items should be kept. The following line will update a feed every 30 minutes and keep up to a 100 items.

add("http://someurl", rate=30, keep=100)

NOTE: "keep" will be silently ignored if it's below the number of items in the feed. In fact, by default keep = 0. This behavior differs from 0.6.x.

The default rate is 5 for fetching from the server every five minutes.

Password Protected Feeds

If the feed is behind browser authentication (i.e. when you try to reach it in a browser it brings up a username/password box), you can specify those in the feed definition too.

add("http://someprotectedurl", username="myuser", password="mypass")

NOTE: In order to protect sensitive information in your config, it's standard practice to chmod 600 ~/.canto/conf so that other users can't read your password even if they can read your home directory. However, Canto will not enforce these permissions as some other programs.

There are a few other options for add, but these are more logically covered elsewhere.

Script Extensions

Canto supports using Snownews extensions. Essentially, these are executable scripts that, when run, spit out the feed XML. These are usually used to make a feed out of a webpage that doesn't usually provide a feed (which are thankfully becoming more and more rare). By default, these are put into ~/.canto/scripts/, but this can be changed by adding the -S flag to canto-fetch.

A typical example of using a script is to get a feed for the Slashdot polls which, as of this writing, has no RSS just for it. slashdotpolls is a script that will scrape Slashdot and output a feed. To use it:

$ wget http://codezen.org/static/slashdotpolls
$ chmod +x slashdotpolls
$ mkdir ~/.canto/scripts
$ mv slashdotpolls ~/.canto/scripts/

It's very important that the script is marked as executable, or the extension will fail.

NOTE: Because these extensions require an arbitrary script to be run as your user, DO NOT EVER use a script that comes from an unknown location without first READING the script to make sure it's not MALICIOUS.

Then, to use the script from Canto, you'd add a feed starting with "script:", like this:

add("script:slashdotpolls -external")
add("script:myscript -arg1 -arg2 ...")

For slashdotpolls, -external is a flag that makes it print the RSS. You can find a lot more extensions like this in the Snownews repository.

"Sourcing" Other Files

Canto supports adding feeds from other file formats. This can be useful when trying to keep URLs synced between readers. Canto can source OPML files at runtime simply by giving a path to the OPML file.

source_opml("/home/myuser/feeds.opml")

Canto can also source plain lists of URLs, delimited by newlines.

source_urls("/home/myuser/urls")

Feeds that are sourced are added with the equivalent of a basic add call with a URL. If you want to add other attributes to feeds that have been added this way, then you can use change_feed that takes the same arguments as add does.

Tweaking Defaults

At some point you may want to change the rate and keep parameters for a large quantity of feeds and do so simultaneously. Using default_rate and default_keep you can set those parameters for every feed following the call. Because this change only affects feeds that are added after the call, it can be used to set 'keep' and 'rate' variables for batches of feeds, rather than all feeds. If you want the 'keep' and 'rate' variables to affect all feed behavior globally, set the defaults before you define your feeds.

NOTE: To reiterate from above, rate is in minutes and keep will ignore any number lower than the number of items in the feed's source.

The following is a good application of using the default calls:

default_rate(30)    # News feeds 
add("http://news1")
add("http://news2")
...

default_rate(120)   # Slow blog feeds
add("http://blog1")
add("http://blog2")
...

default_rate(1)     # Quick feed
default_keep(100)   # Lots of items could be missed
add("http://quick1")
add("http://quick2")
...

If you choose not to change settings, rate is set to five minutes (5) and keep is set to 0, which indicates that all the items in the feed source should be kept.

Discard Policy

Usually, it's preferable to discard items that are old enough that they're no longer inside the keep range for a particular feed. If you'd like to avoid ever discarding items with a particular tag or state, you can use the never_discard function. For example, to avoid ever discarding unread items:

never_discard("unread")

You can also specify a tag like "Slashdot", but I wouldn't suggest it unless you're okay with spending large amounts of disk space for the 1000s of Slashdot articles you'll accumulate.

Cursor Behavior (0.7.7+)

As of 0.7.7 Canto supports multiple types of cursor behavior.

The default behavior since the beginning of Canto has been scrolling by one when the cursor attempts to go past an edge. Edge is the simplest type of scrolling, but Canto now supports the user defining how far from the actual end of the screen that edge should be.

Also in 0.7.7 the ability to keep the cursor in one spot was added.

This cursor behavior is changed with three different variables:

Name Valid Settings Meaning
cursor_type
"edge","top","middle","bottom"
Which cursor behavior to use
cursor_scroll
"scroll","page"
How the interface should scroll (only valid with "edge" cursor_type)
cursor_edge
integer
How far the cursor is from the end of the screen before it scrolls

Before 0.7.7 the default scroll behavior was:

cursor_type = "edge"
cursor_scroll = "scroll"
cursor_edge = 0

In 0.7.7 the new default is the same, but with a wider margin set with cursor_edge.

cursor_type = "edge"
cursor_scroll = "scroll"
cursor_edge = 5

Other common scrolling effects can be achieved with these variables. For example, paging like mutt:

cursor_type = "edge"
cursor_scroll = "page"
cursor_edge = 0

With cursor_scroll = "page" the cursor_edge value is respected, but it's highly recommended to keep the number low (0,1,2) to keep just enough context around the item. Higher edges are generally very disorienting.

Or to keep the cursor in the middle of the page (at least when it doesn't screen real estate):

cursor_type = "middle"

Browsing

Canto supports using external programs to open the content found in a feed item. Typically, you just want to set a link_handler to your favorite browser.

link_handler("firefox \"%u\"")

This will use firefox as your browser. The \"%u\" will be replaced with the URL. Users that want to use a text based browser like elinks, have to tell Canto to relinquish the terminal while you use it, like so:

link_handler("elinks \"%u\"", text=True)

If you find yourself bouncing between the Linux console and an X terminal, you can use a bit of logic to automatically set the browser based on the TERM environmental variable.

import os
if os.getenv("TERM") == "linux":
    link_handler("elinks \"%u\"", text=True)    # Text-only
else:
    link_handler("firefox \"%u\"")              # X terminal

Non-HTML Content

Links to PDFs and other content you'd rather view in a program other than your browser (like enclosures) can be setup by using link_handler with an extension. For example, to open and .mp3 in a podcast:

link_handler("mplayer -someoptions \"%u\"", ext="mp3")

Fortunately, mplayer can stream from the web by default. Some applications require the content to be fetched before hand. This requirement can be handled using the fetch parameter. For example, to open a .pdf in evince that doesn't support opening from the internet directly you can write:

link_handler("evince \"%u\"", ext="pdf", fetch=True)

Canto will then fetch the content into /tmp and run the associated program.

Images

Images are handled similarly to links with the image_handler call. It takes the same arguments as link_handler. A good example:

image_handler("fbv \"%u\"", text=True, fetch=True)

This will use fbv to view an image in a text console.

NOTE: Image links are denoted by the color blue in the reader

Reader Layout

You can dedicate space for the reader, rather than having it float above the items (the default behavior). reader_orientation and reader_lines.

Reader orientation can be one of five possible settings.

reader_orientation = None       # Default floating
reader_orientation = "left"     # Dedicated left of the item list
reader_orientation = "right"    # Dedicated right of the item list
reader_orientation = "top"      # Dedicated on top of the item list
reader_orientation = "bottom"   # Dedicated under the item list

You can also specify the size for any of the dedicated layouts (i.e. not floating). For "left" and "right", reader_lines controls the width, and for "top" and "bottom" it controls the height. It's set like this:

reader_lines = 10

reader_lines has a minimum of three lines since the default theme ceases to behave well when its space is so constricted. Three lines is practically unreadable, so this is unlikely to change.

Layout Hook

Setting the orientation and size of the reader area statically can be useful, but can lead to trouble (like setting the reader area to be larger than the available space, which is not good). Hooks are covered later, but for now a resize_hook is useful to resize the reader area to be a proportion of the available space, rather than a constant.

This hook will make a reader area that takes half of the screen to the left, no matter how the window is resized and set the number of columns in the main list.

def resize_hook(cfg):
    cfg.reader_orientation = "left"
    cfg.reader_lines = cfg.width / 2
    cfg.columns = (cfg.width / 2) / 65

Copying and pasting this anywhere in your config will achieve the desired effect.

Colors

Changing the colors of the interface is simple. There are eight default ncurses colors, and one place holder for a default value.

Curses Color Number Representation
-1 "default"
0 "black"
1 "red"
2 "green"
3 "yellow"
4 "blue"
5 "pink" or "magenta"
6 "cyan"
7 "white"

NOTE: "default" is usually black on a default terminal. If your terminal supports transparency though, it will be made transparent.

ALSO NOTE: With curses colors you occasionally have to be creative about getting colors not listed here. For example, to achieve "gray", you have to use "black", but make the text bold.

You can use these colors in eight different slots in canto.

Color Pair Definition How it's used
0 (White, Black) This is default color pair
1 (Blue, Black) This is used for unread story items.
2 (Yellow, Black) This is used for read story items.
3 (Green, Black) This is used for links in the reader.
4 (Magenta, Black) This is used for quotes in the reader.
5 (Black,Black) This is used for emphasis (italic/small/em) text in the reader, used with %B to appear gray
6 (Blue,Black) This is used for image links in the reader
7 (Black,Black) This is unset/unused.

Changing these items is as simple as using the colors list.

colors[0] = "blue"
colors[0] = 4
colors[0] = (4, -1)
colors[0] = ("blue", "default")

These statements are equivalent. If you only specify one number or one color, it's used as the foreground color and inherits the background of colors[0], or "default" if you're setting colors[0]. Therefore:

colors[0] = ("blue", "white")
colors[1] = "red"

Now colors[1] inherits colors[0]'s background, which would now be set to ("red", "white").

256 Colors

On terminals that support 256 colors, you can specify colors by number. A color chart for xterm is available here

colors[0] = 120

To make sure that your terminal supports 256 colors, you can test it with this color script, which is a mirrored copy of this Vim script.

If you're having trouble with your terminal and are sure that it supports 256 colors, try setting your TERM variable before invoking canto:

$ TERM="xterm-256color" canto

Using Advanced Features

Canto is extremely powerful due to its internal use of the Python interpreter for all of its configuration requirements. The details of writing extension content is covered elsewhere, but there is a lot of good information included with the source.

Importing canto.extra

In order to use extra content it must be imported in the usual pythonic way, as in:

from canto.extra import *

A call to import canto.extra will make all of the goodies packaged with Canto available to your config.

Keybinds

Specifying Keys

The first step to define your own keybinds is to learn how to specify which key you're binding to. Typically, it's very easy to rebind keys.

keys['a'] = ...
reader_keys['a'] = ...

Any visible non-newline character can be used directly. Whitespace characters (including newline) can be embedded with their typical escape (i.e. t for tab, n for newline, etc.).

keys['\n'] = ...    # Enter
keys['\t'] = ...    # Tab
keys[' '] = ...     # Space
keys['  '] = ...    # Tab

Any invisible characters, like function keys, arrow keys, etc. can be used by their ncurses name. On the man page for getch(), a list of all possible names is available. Here's an online copy. Typically definitions using these keys look like this:

keys['KEY_F1'] = ...
keys['KEY_LEFT'] = ...

To specify Control or Alt key combinations, you can use "C-" for control and "M-" (meta) for Alt.

keys['C-a'] = ...   # Ctrl+A
keys['M-a'] = ...   # Alt+A
keys['C-M-a'] = ... # Ctrl+Alt+A

Default Binds

The following keybinds are typically available to the user. They will be used in the examples below.

Main View

Default Binding Name Function
h
help
Shows the man page (has all of these bindings listed).
KEY_DOWN / j
next_item
Move to the next item.
KEY_UP / k
prev_item
Move to the previous item.
KEY_NPAGE / l
next_tag
Move to the next feed/group of items
KEY_PPAGE / o
prev_tag
Move to the previous feed/group of items.
KEY_RIGHT
just_read
Mark current story read and nothing else.
KEY_LEFT
just_unread
Mark current story unread and nothing else.
g
goto
Open the current story in your browser.
f
inline_search
Mark all stories matching a search.
n
next_mark
Go to the next marked story.
p
prev_mark
Go to the previous marked story.
.
next_unread
Go to the next unread story.
,
prev_unread
Go to the previous unread story.
Space
"reader"
Mark the story read and open the reader.
c
toggle_collapse_tag
Collapse/Show a tag of items.
C
set_collapse_all
Collapse on all tags.
V
unset_collapse_all
Uncollapse all tags.
m
toggle_mark
Mark/unmark an item.
M
all_unmarked
Unmark all items
r
tag_read
Set all stories in a feed/group read.
R
all_read
Set all stories read.
u
tag_unread
Set all stories in a feed/group unread.
U
all_unread
Set all stories unread.
C-r
force_update
Reread stories from disk.
C-l
refresh
Redraw the screen.
q
quit
Quit Canto.
\
restart
Restart canto (0.7.6+)
]
next_filter
Apply next global filter.
[
prev_filter
Apply previous global filter
}
next_tag_filter
Apply next tag filter (from filters)
{
prev_tag_filter
Apply previous feed filter
=
next_tag_sort
Apply next tag sort
-
prev_tag_sort
Apply previous tag sort
<
prev_tagset
Show previous set of tags
>
next_tagset
Show next set of tags
;
goto_reltag
Goto the nth visible tag, relative to current index (filter aware)
:
goto_tag
Goto the nth tag (filter unaware)

Reader View

Default Binding Name Function
KEY_DOWN / j
scroll_down
Scrolls, if there's more text.
KEY_UP / k
scroll_up
Scroll up, if not at the top.
KEY_NPAGE
page_down
Page down.
KEY_PPAGE
page_up
Page Up.
n
["destroy", "just_read", "next_item", "reader"]
Goto the next story without closing the reader.
p
["destroy", "just_read", "prev_item", "reader"]
Goto the previous story without closing the reader.
g
goto
Go to a specific link listed inside the item text.
l
toggle_show_links
Show/hide the list of links at the bottom of the reader.
Space
["destroy", "just_read"]
Close the reader
q
["destroy", "just_read", "quit"]
Quit Canto.
h
["destroy", "just_read", "help"]
Show help.

Using Default Binds

Setting a new key for pre-existing functionality is easy to do using strings. As you can see in the above table, the bind "help" brings up the help page. To rebind this functionality to the F1 key (a typical DOS binding), you could simpy do

keys["KEY_F1"] = "help"

As you might expect, you can also override existing keys

keys[' '] = "next_item" # Overrides the default "reader" command

And you can unset a key all together

keys['q'] = None    # Unsets 'q'

Macros

Canto allows you to queue up more than one action with a keybind. A simple list can get the job done. For example, to create a keybind that will set an item as read and move to the next list item (rather than using the right arrow followed by the down arrow) we could set a macro like this

keys['j'] = ["just_read", "next_item"]

"just_read" sets the item as read and "next_item" moves to the next item.

More complicated macros can be created that can cover both main view and reader view keybinds. Take for example the default binding of "n" in reader view.

reader_keys['n'] = [ "destroy", "next_item", "reader" ]

This macro allows you to go to the next item without leaving the reader. When this macro executes three events happen: "destroy" kills the reader, "next_item" makes the main interface go to the next item, and "reader" makes the main interface re-open the reader. All this work is done with one keystroke.

Another common macro task is to open the reader and automatically open the link list. This also can be achieved with this simple code

keys[' '] = ["just_read", "reader", "toggle_show_links" ]

Using macros and keybinds, it's possible to get a maximum amount of work from a minimum number of keystrokes.

Keybind Goodies.

Rebinding some existing functionality to a different key or creating a simple macro will certainly make most users work faster and easier.

Up until now, we've only used strings in the keybinds and macros. These strings are shorthand for built-in functionality. However, in place of these strings, you can bind functions to keystrokes. Doing so, adds a very powerful feature to Canto's interface.

Later in the document we'll cover set_filter, set_tag_filter, and set_tag_sort which are all defined in canto.extra. For now, we'll cover some more interesting and useful additions.

Searching

You can setup a keybind to search for your favorite terms using the search keybind, which takes a keyword argument or a regex. This uses the internal inline_search behavior and marks all items matching the search.

keys['1'] = search("Linux")
keys['2'] = search(".*[Uu]buntu.*", regex=True)

You can also use search_filter which will prompt you interactively for a keyword (or a regex if you prefix the string with "rgx:") and filter out all unmatching items.

keys['/'] = search_filter

Once again, note that search_filter is not in quotes, it is not a string because it's not a builtin keybind. search_filter is defined in canto.extra and therefore is used as a function.

Copying (Yanking)

A neat function for putting a link on the X clipboard (for use in pasting into a chat, a browser, etc.) can be used

keys['y'] = yank

NOTE: Yank requires xclip to be installed and visible in your PATH. On Debian based distros it's the xclip package, but on some it might be included with a generic X11 application meta-package. If in doubt, do which xclip from your shell.

Downloading Content

New in version 0.7.6 is the capability to wget content out of links. This essentially amounts to a custom link_handler/image_handler.

reader_keys['w'] = wget_link("/path/to/downloads")

The above will make 'w' in the reader prompt you for a link number and will download that link into the path specified.

NOTE: wget_link requires wget to be installed and visible in your PATH. On most distros this is already installed or is available in a wget package.

Saving

The last neat little utility is save which writes a file (~/canto_out) with a title and a link when called. This is designed as a template example for writing a keybind, rather than a fully functional bind but it can be useful.

keys['s'] = save

Filters

Perhaps the most useful extra feature Canto provides is its powerful filter system. canto.extra provides a number of useful filters

None
Filter no items.
show_unread
Ignore all items that have been marked read
show_marked
Ignore all items that are unmarked
only_with("string")
Show only items that have "string" in the title
only_without("string")
Show only items that **don't** have "string" in the title
all_of(filter1, filter2, ...)
Show only items that pass all listed filters (binary and)
any_of(filter1, filter2, ...)
Show items that pass any of the listed filters (binary or).

Additionally, there is with_tag_in, which is covered in the tag section, specifically.

There are three ways to apply filters.

Using Global Filters

Of the the three filters, global filters are arguably the most useful.For example, a global filter can be used to filter out items that have already been read. Accomplishing that is simple:

filters=[show_unread]

Setting the 'show unread' filter will remove all previously read feed items by default when Canto opens. If you still want to have access to all items, you can add the None filter to the list:

filters=[show_unread, None]

With this filter in place, you can switch between show_unread and None using [ and ] to cycle through the list.

If you're more comfortable using a keybind to choose your global filters, then you can use set_filter. This allows you to set the global filter regardless of whether it's in the filters list:

keys['1'] = set_filter(show_unread)
keys['2'] = set_filter(show_marked)
keys['3'] = set_filter(None)

This lets you use the 1, 2, and 3 keys to set your filters directly, without needing to cycle through the list.

Using Feed Filters

Most of the time, feed filters are only useful if you want to completely ignore some easily filtered content in a feed. My favorite example is ignoring all non-comic items in a webcomic feed. Take penny-arcade's feed for example. Each item's title is clearly marked with "Comic:" or "News:".

If I wanted just completely ignore non-comic items, I could modify the add call for Penny Arcade to use the only_with filter:

add("http://feeds.penny-arcade.com/pa-mainsite", filter=only_with("Comic:"))

This filter will eliminate all items that don't have "Comic:" in the title. Other examples include filtering distro package feeds for only a certain type of package (i.e. Gentoo, only_with("sys-devel")), or filtering porn torrents from torrent site feeds (only_without("XXX"), provided the feed items are clearly marked).

Tags

A tag is an arbitrary set of stories. By default, Canto creates a single tag per feed and if you never use any other tags, feeds and tags are analogous.

A tag allows you to filter, sort and otherwise customize how these groups of items are displayed.

Manipulating Default Tags

As mentioned above, each feed is given a tag by default. That tag's name is the name specified in the feed's source. So for the reddit feed, the tag's title (which is displayed at the top of the box of stories in the interface) is "reddit.com: what's new online!". That title is a bit long, and we want to use something a little more concise. So, to override the default tag, we can add this to the config:

add("http://reddit.com/.rss", tags=["Reddit"])

This addition will cause the displayed name to change to "Reddit" from the longer "reddit.com: what's new online!".

Adding Tags

Adding a tag to a feed is as simple as coming up with a name and adding it to the tag list.

add("http://some-blog", tags=[None, "blogs"])
add("http://some-other-blog", tags=[None, "blogs"])

NOTE: None in the tag is shorthand for using the title included with the feed. If all tags are omitted, tags=[None] is implied.

This addition will define an implicit tag "blogs". After adding that tag, you can use < and > to switch between the default set of tags (i.e. one per feed) to the "blogs" tag.

Notice that when you switch to the "blogs" tag, the displayed content will be the stories in the first feed followed by the stories in the second feed. This display may not seem very useful if you're using implicit tags, but, when you add a sort to mix, the two feeds you can achieve some neat effects, like organizing all of your favorite blog posts from around the internet in chronological order.

Using Tags as Folders

Typically, the above behavior, appending the items together by using a common tag is not what a user expects unless they're going to use a sort. Usually, tags are used as folders names so that switching to "blogs" means showing all the feeds that have "blogs" in the tags.

This behavior is accomplished using the with_tag_in filter. Following the above example, we can emulate folders with global filters:

add("http://some-blog", tags=[None, "blogs"])
add("http://some-other-blog", tags=[None, "blogs"])
filters = [ None, with_tag_in("blogs") ]

With this snippet, using ']' to switch to the next global filter will cause Canto to display only the items in the two "blogs" feeds, but the items will still be organized by feed rather than displayed as an appended list of items.

You can also list multiple tags and use implicit default tags for use in with_tag_in

add("http://rss.slashdot.org/slashdot/Slashdot") # Creates implicit "Slashdot" tag
add("http://.some-blog", tags=[None, "blogs"])
filters = [ None, with_tag_in("Slashdot", "blogs") ]

Lastly, you can combine with_tag_in and other filters with all_of

filters = [ None, all_of(with_tag_in("blogs"), show_unread) ]

This combination will make your second global filter show you all of your blog feeds, but only their unread items.

Adding Explicit Tags

So far we've only dealt with implicit tags that are either created by default or by appending a string to the tags list. Such creations are only useful for using tags with < / > or in filters.

However, tags themselves can have attributes. You can make an explicit tag with the add_tag function.

add("http://some-blog", tags=[None, "blogs"])
add("http://some-other-blog", tags=[None, "blogs"])
add_tag("blogs", ...parameters...)

These definitions can come before or after you use them in add calls.

Tag Filters

Tag filters, as the name would suggest only apply to a specific tag. These filters are useful if a filter would only make sense for a certain set of items rather than globally.

Let's return to the webcomic example from the feed filter section. In that example, we wanted to entirely discard posts that were news and only see comics. Using a tag filter, however, it's possible to keep all items, but merely hide (rather than entirely discard) the other stories. This is useful if you want to prioritize one set of stories over another. In this case, we want to prioritize the comics, but make the news items available on request.

add("http://feeds.penny-arcade.com/pa-mainsite") # Implicitly creates "Penny Arcade" tag
add_tag("Penny Arcade", filters=[only_with("Comic:"), only_with("News:")])

This example makes the "Penny Arcade" tag explicit and sets up two tag filters. Now when you've selected the Penny Arcade feed, you can use { and } to switch between the tag filters and show comics or news. Alternatively, a similar effect could be achieved by using only_without("Comic:") as the second filter, which would allow all items not shown in the first filter, not necessarily just items with "News:" in them.

Using these tag filters, you can essentially turn one tag or feed into multiple overlapping tags and cycle through them.

NOTE: Tag filters are always overridden by global filters. If your global filter is show_unread, even if your tag filter is None, you won't see any read items.

Like global filters, tag filters can be set by default.

default_tag_filters([show_unread])

Similar to default_rate and default_keep, these defaults are applied as explicit tags are created. Any tags created with add_tag will inherit the default tag filters from the call immediately before the add_tag (or [None] if it hasn't been called at all). Implicit tags (i.e. not created with add_tag) are made explicit after the rest of the configuration is done, so they will inherit the defaults from the last call to default_tag_filters made in the config.

Just like global filters, tag filters can be set directly via keybind

keys['u'] = set_tag_filter(show_unread)

This keybind will set the current tag's filter to show_unread.

NOTE : Unlike global filters, tag filters will never make a tag fully disappear since there would be no easy way to change the tag filter back to one with items in it.

Sorts

Another benefit of making explicit tags is the ability to sort items in varied ways. canto.extra defines some default sorts to use.

None
Use the ordering specified in the feed.
by_date
Order by the time the items are parsed.
by_len
Order by length of title.
by_content
Order by length of content.
by_alpha
Sort alphabetically.
by_unread
Order by read status.
reverse_sort(sort)
Reverse the given sort.

NOTE: Sorts based on strings are done on unparsed strings. This means that the strings could still have HTML built into them and untranslated entities. This effects the sort because the length or the first character may not be what's displayed. A title "<strong>Zoo</strong>" will sort alphabetically before "Aardvark Sighting" because "<" is before "A", despite the fact that the HTML will not be displayed.

This was done to speed sorts so that interpretable HTML wouldn't have to be stripped before and replaced after the sorting it done.

ALSO NOTE: Sorts can possibly make Canto's memory footprint increase marginally if they require access to data that isn't usually kept in memory. Sorts that function on the title (by_length, by_alpha, etc.) have no effect because the title is always in memory. Sorts like by_date require a date field to be kept in memory so it adds a couple of bytes per story.

This was also a speed tweak to avoid stories hitting the disk every time it's sorted which makes the program grind to a halt.

The simplest way to use a sort is to do so when you define a tag

add_tag("Tag", sorts=[by_unread])

The above code will sort the given tag with unread stories first.

Similarly to filters and sorts you can set defaults and use keybinds to set sorts.

default_tag_sorts([show_unread])
keys['s'] = set_tag_sort(show_unread)

And, once again, like default_tag_filters, explicit tags inherit the tag filters from the previous call to default_tag_sorts, while implicit tags inherit the sorts from the final call to default_tag_sorts.

Sorts like by_date are most useful when combining two feeds into a single tag

add("http://news1", tags=["News"])
add("http://news2", tags=["News"])
add_tag("News", sorts=[by_date])

Sort Order

Anywhere that a sort can be used, you can use multiple sorts with the sort_order function from canto.extra. This takes any number of sorts in order of priority.

default_tag_sorts([sort_order(by_unread, by_alpha)])

This snippet will make tags sort items first by unread status and then sort the same items alphabetically.

Adding Content

A common task is to add relevant information to the reader.

Typical Content

A lot of feeds support typical information about each item. By default, the reader displays the title, the description, and the subsequent links. If you wanted to add other content, you can use add_info. For example, to add the author of an item to the reader:

r = get_default_renderer()
add_info(r, "Author")

This will add the following line to the reader, above the content:

Author: [author]

add_info takes other arguments to customize how the line is displayed.

add_info(r, "Author", caption="by: ")

The resulting line now looks like this:

by: [author]

If the item being displayed doesn't include any author information, the line will be entirely ommitted. Additionally, it could be that the information isn't useful and should be ignored. Lots of feed generators set author to a default like donotreply@somedomain which isn't useful info. Other feeds will put author information into the content anyway. Because of this, you can specify to only add the information to particular tags.

add_info(r, "Author", tags=["Slashdot"...])

Less Common Content

It's difficult to know whether your RSS includes any special information. As of 0.7.x, canto includes a simple wrapper script called canto-inspect. You call it like so:

canto-inspect [URL] > output

It's essentially a custom pretty printer for the XML, Although it is not extremely advanced, using canto-inspect you can detect interesting content, as in this partial output from canto-inspect http://rss.slashdot.org/slashdot/Slashdot:

[entries]
    [0]
        [summary_detail]
            [base]: http://rss.slashdot.org/slashdot/Slashdot
            [type]: text/html
            [value]: ...
            [language]
        [updated_parsed]: ...
        [links]
            [0]
                [href]: http://rss.slashdot.org/~r/Slashdot/slashdot/...
                [type]: text/html
                [rel]: alternate
        [title]: Doctors Fight Patent On Medical Knowledge
        [slash_department]: no-not-patent-medicine
        [feedburner_origlink]: http://yro.slashdot.org/story/09/07/...
        [author]: kdawson
        [updated]: 2009-07-21T18:20:00+00:00
        [summary]: ...
        [title_detail]
            [base]: http://rss.slashdot.org/slashdot/Slashdot
            [type]: text/plain
            [value]: Doctors Fight Patent On Medical Knowledge
            [language]
        [slash_section]: yro
        [link]: http://rss.slashdot.org/~r/Slashdot/slashdot/~3/...
        [slash_hit_parade]: 0,0,0,0,0,0,0
        [id]: http://yro.slashdot.org/story/09/07/21/1646216/...
        [tags]
            [0]
                [term]: patents
                [scheme]
                [label]

In the above hodge-podge of information, we can see some content which might interest some users. Take slash_department and slash_section divisions for example. Using the add_info function, we can add the content as follows:

r = get_default_renderer()
add_info(r, "slash_department", caption="Dept: ", tags=["Slashdot"])

NOTE: The first argument to add_info corresponds to the content in the [brackets], but isn't case sensitive.

Highlighting

New in 0.7.6 is the ability to statically highlight words in the reader or main views.

r = get_default_renderer()
add_hook_pre_reader(r, highlight_word("NASA"))
add_hook_pre_story(r, highlight_word("never"))

This will, for example, highlight the word "NASA" in the reader and "never" in the main view. These are not case sensitive by default. Those familiar with Python regex can specify a flags arg but if all you need is case sensitivity you can set it to None

r = get_default_renderer()
add_hook_pre_reader(r, highlight_word("NASA", None))

A case sensitive version of the reader highlight above.

Update Triggers

Canto supports a number of different update mechanisms.

NOTE: These triggers are to update the client from disk only, they have nothing to do with getting items from the server. That is only controlled by running canto-fetch and the rates you have set in the configuration.

Considerations

Multiple update triggers allow users to update Canto's feeds in different ways depending upon their reading habits. Users who don't appreciate text shifting out from under their eyes might want to avoid the every-minute interval update and use the change_tag or manual update triggers to insure more predictable refreshes. On the other hand, users that tend to jump from one tag to another and do short bursts of content reading might find the interval triggers more to their liking. It's all about what suits you.

If you feel there is an update trigger that we need to implement, file a feature request bug and we'll consider it. Update triggers are fairly easy to implement.

Using Triggers

Using triggers is a simple as using a list. By default, triggers is set like this:

triggers = ["interval"]

You can add triggers or remove triggers with typical Python list functions

triggers.append("change_tag")
triggers.append("signal")
triggers.remove("interval")

You can only set "change_tag", "signal", and "interval" in triggers. Manual isn't considered a real trigger, but is set like other keybinds to force_update.

I expect there will be refinements to the trigger system in upcoming releases. Once again, any ideas for new triggers or improvements (or even code) are welcome.

Hooks

Canto includes a number of hooks for outside extensibility. You may find hooks to be most useful when you author them yourself. Even so, canto.extra does include a few basic, but useful, hooks.

The possible hooks:

start_hook
Run once, on startup
resize_hook
Run when the window is resized (including on start)
new_hook
Run once for every new item.
select_hook
Run when a new item is selected
unselect_hook
Run when an item is unselected
state_change_hook
Run whenever an item's state (read/marked) changes
update_hook
Run when the interface updates
end_hook
Run when the interface closes

NOTE: All hooks are enforced by the interface except new_hook. new_hook is intended to be used as a notification method. All other hooks don't function unless Canto is running.

Using Hooks

There are only two hooks included in canto.extra by default. These are for manipulating the titles of an xterm (or another compatible X terminal).

select_hook = set_xterm_title
end_hook = clear_xterm_title

This will set the xterm's title to "Tag - Title" when you select an item and clear it when Canto closes.

Because this hook doesn't work everywhere and where it doesn't work it essentially clobbers ncurses by printing to the screen (you set an xterm title by writing to stdout with a special code), it's usually preferable to check the environment's TERM before employing the hooks.

import os
if os.getenv("TERM") == "xterm":  # Or other compatible term
    select_hook = set_xterm_title
    end_hook = clear_xterm_title

This code ensures that when you switch to the Linux console or another terminal, Canto won't start spewing uninterpreted content to the screen.

Example Config

Here is a modified version of my own config that should serve as a decent starting point for any new users.

from canto.extra import *
import os

# Handlers when in Linux console or xterm
if os.getenv("TERM") == "linux":
    link_handler("elinks \"%u\"", text=True)
    image_handler("fbi \"%u\"", text=True, fetch=True)
else:
    link_handler("firefox \"%u\"")
    image_handler("feh \"%u\"", fetch=True)

# Max column width of 65 characters
def resize_hook(cfg):
    cfg.columns = cfg.width / 65

# Never discard items I haven't seen
never_discard("unread")

# I prefer change_tag to interval
# Uncomment these to use it too

# triggers.remove("interval")
# triggers.append("change_tag")

keys['/'] = search_filter
keys['y'] = yank

# Use [ / ] to switch between global filters
filters=[show_unread, None]

# Make unread items float to the top, when not
# using show_unread filter
default_tag_sorts([by_unread])

# Selected Feeds
add("http://rss.slashdot.org/slashdot/Slashdot", tags=[None, "news"])
add("http://osnews.com/files/recent.xml", tags=[None, "news"])
add("http://www.damninteresting.com/?feed=rss2")
add("http://reddit.com/.rss", tags=["Reddit", "reddits", "news"])
add("http://programming.reddit.com/.rss", tags=[None, "reddits"])
add("http://netsec.reddit.com/.rss", tags=[None, "reddits"])
#...

# Some examples
# Uncomment if you've downloaded the script
# add("script:slashdotpolls -external")
#
# Simple password example
# add("http://feedparser.org/docs/examples/digest_auth.xml", username="test",
#        password="digest")

You can download this example config here

Upgrading from 0.6.x

For most users, upgrading to 0.7.x from 0.6.x should be painless. There are some quirks that may cause trouble.

Standard Procedures

First of all, if you run canto-fetch as a daemon, you want to make sure that all the old daemons aren't running. There aren't any differences in the disk format between the two versions, but it's bad practice to have multiple versions of software running on the same data. You can properly kill all running canto-fetch instances like so:

$ killall -INT canto-fetch

After that, you should have no running instances. You can check with

$ ps -u [youruser] | grep canto-fetch

If you still having running instances after a few moments, you can issue killall -9 canto-fetch to force them to exit.

Shared Memory

The (multi)processing module requires semaphores that are supported by /dev/shm with glibc. If you're getting weird errors like

OSError: [Errno 13] Permission denied

or

OSError: [Errno 38] Function not implemented

Then you need to mount /dev/shm and make sure you have read/write permissions. You can do this as root or with sudo like this:

$ sudo mount shm /dev/shm -t tmpfs

By default, tmpfs is has 777 permissions, but just in case:

$ sudo chmod 777 /dev/shm

As a side note, this is mounted by default in most common distros, and can improve the peformance of some applications using shared memory. In fact, glibc 2.2+ expects it to be mounted. To get it to be mounted on startup, add this line to your /etc/fstab

tmpfs    /dev/shm    tmpfs   defaults    0 0

Configuration

Once again, for most users, changes to your configuration shouldn't be necessary. If you loop through the color array, you may have to change your configuration. If you use sorts, or define sorts and filters, then you may need a configuration change.

Color array

This is mainly if you're doing a more advanced loop through the color list. If you're just setting colors in the typical way (colors[0] = (num/str,num/str)), then you should be okay.

If you're looping with

for i, (fg, bg) in colors:

Then you may run into trouble. The new default colors are not all set to tuples when configured so the (fg, bg) may except. However, the most common use for this loop is to set a common background for all colors. In 0.7.x, if a color is not set, the background of a color defaults to the background color of the first pair, making the loop unnecessary. In short

# 0.6.x version
for i, (fg, bg) in colors:
    colors[i] = (fg, "newbackground")

# 0.7.x version, setting the background of 0, changes them all
colors[0] = ("white", "newbackground")

Using Sorts

The primary difference with using sorts is that sort order is no longer conveyed as a simple list. This was confusing and made for a lot of double lists in weird places.

add_tag("sometag", sorts=[[by_date]])       # 0.6.x
add_tag("sometag", sorts=[by_date])         # 0.7.x

To convey the same meaning as the double lists used to (i.e. sort order), you can use the new sort_order function.

add_tag("sometag", sorts=[[by_alpha, by_len]])          # 0.6.x
add_tag("sometag", sorts=[sort_order(by_alpha, by_len)] # 0.7.x

Defining Filters and Sorts

If you created your own filters and sorts for 0.6.x, the main difference is that these now must be classes which subclass Filter and Sort respectively. So where once

# 0.6.x valid filter
def myfilt(tag, story):
    ...perform filter...

was valid. You now need

# 0.7.x valid filter
class myfilt(Filter):
    def __call__(self, tag, story):
        ...perform filter...

Also, any items used other than "title", "link", "id", and "canto_state", should be added to the precache variable of the class.

class myfilt(Filter):
    def __init__(self):
        Filter.__init__(self)
        self.precache = ["extra_item", ...]
    ...

You'll know that this needs to be done if Canto is extremely sluggish. Of course, you can see examples of the new classes in canto.extra

Validation

0.7.x is more strict than 0.6.x about validating your configuration. It's possible that accepted input that doesn't fall under the previous categories and still doesn't work with 0.7.x. Usually in this case, the error message is enough to set you straight. If you're still having trouble, contact me.

Other Changes

If All Else Fails

If you're really stuck and confused trying to upgrade: contact me.

Send all bug reports to canto-reader [at] codezen [dot] org
Or come to discuss in #canto on irc.freenode.net