~reesmichael1/roman

d221da741039b0f749eea2a854310369d92c8337 — Michael Rees 8 months ago 5982bd3
Load configuration values from config file

This is a large commit! The types were all moved out of their own
modules into a common types module to avoid circular dependencies with
the config object. It also adds a default config file and starts the
process of moving some of the hardcoded defaults into the file.
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