M README.md => README.md +4 -0
@@ 22,6 22,10 @@ Then, when you run `roman`, you will be able to select a feed and post to view.
`roman` is still in very early development. Several improvements are planned!
+## Configuration
+
+You will need to copy the `config/config` file in this repository to `roman/config` within your platform's standard config directory. You can change the values in the file to your taste.
+
## Platforms
Cross-platform support is fully intended, but for now, `roman` is only tested on Linux. (Most of the code should work just fine, but some filepaths that are generated are currently Linux-only. A patch would be happily accepted!)
A config/config => config/config +8 -0
@@ 0,0 1,8 @@
+[Keyboard]
+# The config values for keyboard navigation.
+# By default, we use vi keybindings.
+down=j
+up=k
+next=n
+previous=p
+quit=q
A src/romanpkg/config.nim => src/romanpkg/config.nim +37 -0
@@ 0,0 1,37 @@
+import parsecfg
+import sequtils
+
+import errors
+import paths
+
+from types import RomanConfig
+
+
+proc strToChar(config: Config, section: string, key: string): char {.
+ raises: [RomanError].} =
+ try:
+ let s = config.getSectionValue(section, key)
+ if s.len != 1:
+ raise newException(RomanError, "expected single char for " & section &
+ "." & key & ", got '" & s & "'")
+ return toSeq(s.items)[0]
+ except KeyError:
+ raise newException(RomanError,
+ "missing config value for " & section & "." & key)
+
+
+proc mustLoadConfig*(): RomanConfig {.raises: [].} =
+ try:
+ let path = getConfigFilePath()
+ let dict = loadConfig(path)
+ result.down = strToChar(dict, "Keyboard", "down")
+ result.up = strToChar(dict, "Keyboard", "up")
+ result.next = strToChar(dict, "Keyboard", "next")
+ result.previous = strToChar(dict, "Keyboard", "previous")
+ result.quit = strToChar(dict, "Keyboard", "quit")
+ except:
+ echo "error loading config file: " & getCurrentExceptionMsg()
+ quit(1)
+
+
+var conf* = mustLoadConfig()
M src/romanpkg/feeds.nim => src/romanpkg/feeds.nim +9 -11
@@ 11,12 11,7 @@ import errors
import posts
import termask
-
-type
- Feed* = object
- posts*: seq[Post]
- title*: string
- unreadPosts*: int
+from types import Feed, Post, Subscription
proc updateUnread*(feed: var Feed) {.raises: [].} =
@@ 53,16 48,19 @@ proc displayFeed*(feed: var Feed) {.raises: [RomanError, InterruptError].} =
raise newException(RomanError, "could not set terminal style: " & e.msg)
-proc getFeed*(url: string): Feed {.raises: [RomanError].} =
+proc getFeed*(sub: Subscription): Feed {.raises: [RomanError].} =
try:
- let rssFeed = FeedNim.getRSS(url)
- result.title = rssFeed.title
+ let rssFeed = FeedNim.getRSS(sub.url)
+ if sub.name.len > 0:
+ result.title = sub.name
+ else:
+ result.title = rssFeed.title
result.posts = map(rssFeed.items,
proc (i: RSSItem): Post = postFromRSSItem(i))
result.updateUnread()
except ValueError:
- raise newException(RomanError, url & " is not a valid URL")
+ raise newException(RomanError, sub.url & " is not a valid URL")
except:
let msg = getCurrentExceptionMsg()
raise newException(RomanError,
- "error while accessing " & url & ": " & msg)
+ "error while accessing " & sub.url & ": " & msg)
M src/romanpkg/main.nim => src/romanpkg/main.nim +4 -2
@@ 8,6 8,8 @@ import feeds
import subscriptions
import termask
+from types import Feed, Subscription
+
proc chooseFeed(feeds: seq[Feed]): Feed {.raises: [RomanError,
InterruptError].} =
@@ 36,10 38,10 @@ proc runMainPath() {.raises: [RomanError, InterruptError].} =
"Use --subscribe [url] to add some."
return
elif subs.len == 1:
- feed = getFeed(subs[0].url)
+ feed = getFeed(subs[0])
feeds = @[feed]
else:
- feeds = map(subs, proc(s: Subscription): Feed = getFeed(s.url))
+ feeds = map(subs, getFeed)
while true:
if feeds.len == 1:
M src/romanpkg/paths.nim => src/romanpkg/paths.nim +8 -2
@@ 3,6 3,10 @@ import os
import errors
+proc getConfigFilePath*(): string {.raises: [].} =
+ joinPath(getConfigDir(), "roman", "config")
+
+
proc getSubsFilePath*(): string {.raises: [].} =
joinPath(getConfigDir(), "roman", "subscriptions")
@@ 11,8 15,10 @@ proc initConfigDir*() {.raises: [RomanError].} =
let configDir = joinPath(getConfigDir(), "roman")
try:
if not existsOrCreateDir(configDir):
- let subsFile = joinPath(configDir, "subscriptions")
- writeFile(subsFile, "")
+ let config = getConfigFilePath()
+ let subs = getSubsFilePath()
+ writeFile(config, "")
+ writeFile(subs, "")
except OSError as e:
raise newException(RomanError, e.msg)
except IOError as e:
M src/romanpkg/posts.nim => src/romanpkg/posts.nim +2 -11
@@ 10,15 10,7 @@ import errors
import htmlextractor
import paths
-
-type
- # Use our own Post type instead of RSSItem
- # to show metadata we collect (e.g., read/unread)
- Post* = object
- title*: string
- content*: string
- guid*: string
- read*: bool
+from types import Post
proc formatTitle*(p: Post): string {.raises: [].} =
@@ 55,8 47,7 @@ proc displayPost*(p: Post) {.raises: [RomanError].} =
raise newException(RomanError, "could not write to the terminal: " & msg)
-proc postFromRSSItem*(item: RSSItem): Post {.raises: [
- RomanError].} =
+proc postFromRSSItem*(item: RSSItem): Post {.raises: [RomanError].} =
result.title = item.title
result.content = extractBody(item.description)
result.guid = item.guid
M src/romanpkg/subscriptions.nim => src/romanpkg/subscriptions.nim +3 -6
@@ 2,15 2,12 @@ import algorithm
import os
import parsecsv
+import config
import errors
import feeds
import paths
-
-type
- Subscription* = object
- url*: string
- name*: string
+from types import Subscription
proc getSubscriptions*(): seq[Subscription] {.raises: [RomanError].} =
@@ 34,7 31,7 @@ proc getSubscriptions*(): seq[Subscription] {.raises: [RomanError].} =
proc addSubscriptionToSubsFile*(url: string) {.raises: [RomanError].} =
try:
- let feed = getFeed(url)
+ let feed = getFeed(Subscription(url: url))
let subscription = Subscription(name: feed.title, url: url)
let subs = getSubscriptions()
if subscription in subs:
M src/romanpkg/termask.nim => src/romanpkg/termask.nim +13 -12
@@ 8,6 8,9 @@ import fab
import errors
+from config import conf
+from types import RomanConfig
+
# This function was originally based on the promptListInteractive function
# in Nimble, and is therefore under the same license.
@@ 114,9 117,7 @@ proc promptList*(question: string, args: openarray[string],
while not selectionMade:
setForegroundColor(fgDefault)
if argSlices.len > 1:
- cursorUp(stdout)
- echo "[", sliceIx + 1, "/", argSlices.len,
- "] N/Right to advance, P/Left to go back"
+ echo "[", sliceIx + 1, "/", argSlices.len, "]"
let width = terminalWidth()
for ix, arg in currentArgs:
@@ 143,15 144,15 @@ proc promptList*(question: string, args: openarray[string],
while true:
let c = getch()
- case c:
- of 'j': # go down
+ # Use ifs instead of case because case requires known values at comptime
+ if c == conf.down: # go down
goDown(selectedIx, currentArgs)
break
- of 'k': # go up
+ elif c == conf.up: # go up
goUp(selectedIx, currentArgs)
break
# Handle arrow keys
- of chr(27):
+ elif c == chr(27):
# Skip the useless [
discard getch()
case getch():
@@ 168,18 169,18 @@ proc promptList*(question: string, args: openarray[string],
goBackPage(currentArgs, selectedIx, sliceIx, argSlices)
break
else: break
- of '\r':
+ elif c == '\r':
selectionMade = true
break
- of 'N':
+ elif c == conf.next:
advancePage(currentArgs, selectedIx, sliceIx, argSlices)
break
- of 'P':
+ elif c == conf.previous:
goBackPage(currentArgs, selectedIx, sliceIx, argSlices)
break
- of 'q':
+ elif c == conf.quit:
return none(string)
- of '\3':
+ elif c == '\3':
showCursor(stdout)
# Move the cursor down to the end of the arguments list
# so that after the interrupt, the error message is displayed
A src/romanpkg/types.nim => src/romanpkg/types.nim +24 -0
@@ 0,0 1,24 @@
+type
+ RomanConfig* = object
+ up*: char
+ down*: char
+ next*: char
+ previous*: char
+ quit*: char
+
+ Subscription* = object
+ url*: string
+ name*: string
+
+ # Use our own Post type instead of RSSItem
+ # to show metadata we collect (e.g., read/unread)
+ Post* = object
+ title*: string
+ content*: string
+ guid*: string
+ read*: bool
+
+ Feed* = object
+ posts*: seq[Post]
+ title*: string
+ unreadPosts*: int