~reesmichael1/roman

ref: 7a95a17d006e08e02f638b15caba2a56ce590741 roman/src/romanpkg/posts.nim -rw-r--r-- 5.6 KiB
7a95a17dMichael Rees Allow toggling read status from post selection 5 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import browsers
import htmlparser
import options
import strtabs
import strutils
import tables
import terminal
import unicode
import xmltree

import FeedNim / [atom, rss]
import pager

import errors
import htmlextractor
import paths
import termask
import types

from config import conf


proc formatTitle*(p: Post): string {.raises: [RomanError].} =
  var width: int
  try:
    width = terminalWidth()
  except ValueError:
    raise newException(RomanError, "could not get terminal width")
  if p.read:
    result = p.title
  else:
    result = "[*] " & p.title

  if result.len > width:
    # 3 for the ellipsis, 4 for the '> ' before and after the printing,
    # 2 for padding
    result = result[0..<width-9] & "..."


proc collectReadPosts(): seq[string] {.raises: [RomanError].} =
  try:
    for line in lines(getPostReadFile()):
      result.add(line)
  except IOError:
    let msg = getCurrentExceptionMsg()
    raise newException(RomanError,
      "could not read from the read-posts file: " & msg)


proc isPostRead(itemGUID: string): bool {.raises: [RomanError].} =
  return itemGUID in collectReadPosts()


proc extractLink(tag: XmlNode): PostLink {.raises: [].} =
  let text = tag.innerText.splitLines().join(" ")
  return PostLink(text: text, url: tag.attrs.getOrDefault("href"))


proc displayLinks(p: Post) {.raises: [RomanError].} =
  var html: XmlNode
  try:
    html = parseHTML(p.raw)
  except IOError, ValueError, Exception:
    let msg = getCurrentExceptionMsg()
    raise newException(RomanError, "could not parse post HTML: " & msg)
  var links: seq[PostLink] = @[PostLink(text: "Original Post", url: p.link)]

  # Some sources use a single link as the post content
  if html.tag == "a":
    links.add(extractLink(html))

  for a in html.findAll("a"):
    links.add(extractLink(a))

  proc shortenURL(url: string, text: string): string =
    # 3 for ' ()', 4 for '>   <'

    let availableWidth = terminalWidth() - text.runeLen() - 7
    if url.runeLen() > availableWidth:
      # Remove three more characters for the ellipsis
      return url[0..<(availableWidth - 3)] & "..."
    return url

  try:
    var displayNames = initTable[PostLink, string]()
    for link in links:
      if link.text.len > 0:
        let text = if link.text.len > (terminalWidth() div 2):
          link.text[0..<(terminalWidth() div 2)]
        else:
          link.text
        displayNames[link] = text & " (" & shortenURL(link.url, text) & ")"
      else:
        displayNames[link] = shortenURL(link.url, "")
    # Move down one line in case we're at the END line already
    echo ""
    let link = promptList("Select link to open in system browser",
        links, displayNames = displayNames).get
    openDefaultBrowser(link.url)
  except ValueError:
    discard
  except UnpackError:
    discard
  except IOError as e:
    raise newException(RomanError, "could not display links: " & e.msg)
  except Exception as e:
    raise newException(RomanError, "could not open link: " & e.msg)


proc displayPost*(p: Post) {.raises: [RomanError].} =
  try:
    var content: string
    if p.author.isSome:
      content = p.title & "\n" & p.author.unsafeGet & "\n\n" & p.rendered
    else:
      content = p.title & "\n\n" & p.rendered
    page(content, goToBottom = conf.goToBottom, goToTop = conf.goToTop,
      upOne = conf.up, downOne = conf.down, quitChar = conf.quit,
      extractLinks = conf.extractLinks, extractLinksProc = proc() {.closure,
          noSideEffect, gcsafe.} = {.noSideEffect.}: displayLinks(p))
  except IOError, ValueError:
    let msg = getCurrentExceptionMsg()
    raise newException(RomanError, "could not write to the terminal: " & msg)


proc postFromAtomEntry*(entry: AtomEntry): Post {.raises: [RomanError].} =
  result = new(Post)
  result.title = entry.title
  result.rendered = extractBody(entry.content)
  result.raw = entry.content
  # Use the post link as the UID if none is provided
  if entry.id.isEmptyOrWhitespace():
    result.guid = entry.link.href
  else:
    result.guid = entry.id
  result.read = isPostRead(result.guid)
  result.link = entry.link.href
  if entry.author.name.len > 0:
    result.author = some(entry.author.name)


proc postFromRSSItem*(item: RSSItem): Post {.raises: [RomanError].} =
  result = new(Post)
  result.title = item.title
  result.rendered = extractBody(item.description)
  result.raw = item.description
  # Use the post link as the UID if none is provided
  if item.guid.isEmptyOrWhitespace():
    result.guid = item.link
  else:
    result.guid = item.guid
  result.read = isPostRead(result.guid)
  result.link = item.link
  if item.author.len > 0:
    result.author = some(item.author)


proc markAsRead*(p: var Post) {.raises: [RomanError].} =
  var f: File
  if not f.open(getPostReadFile(), mode = fmAppend):
    raise newException(RomanError, "could not open " & getPostReadFile())
  defer: f.close()

  try:
    f.writeLine(p.guid)
    p.read = true
  except IOError:
    raise newException(RomanError,
      "could not save " & p.guid & " in the read-posts file")


proc markAsUnread(p: var Post) {.raises: [RomanError].} =
  try:
    let filename = getPostReadFile()
    let content = filename.readFile()
    let postLines = content.splitLines()

    # TODO: make a backup of the contents to write in the event of an exception
    var f: File
    if f.open(filename, fmWrite):
      defer: f.close()
      for line in postLines:
        if p.guid != line:
          f.writeLine(line)
      p.read = false
  except IOError as e:
    raise newException(RomanError,
      "could not open posts read file: " & e.msg)



proc toggleRead*(p: var Post) {.raises: [RomanError].} =
  if p.read:
    p.markAsUnread()

  else:
    p.markAsRead()