fmog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 30d24be1819b9fcbf9a414639dd4abe51873ca73f1563a67f7daf9844147a3e4
4
+ data.tar.gz: 609268fc98dd0c9fc4f4857b63fc2ab35f330f85985a2d041835a5bf5148b43f
5
+ SHA512:
6
+ metadata.gz: 1c3225d42416c692e36ad9d90f75164d25d688717072fe41305a98a9567221a50a812de2ca5cce6b45b77108df19a79cfdfea414f11fe85cb58405ff13fca6f2
7
+ data.tar.gz: 4a0bf86d883ebe793dfdfde3045dbd4b5fc910f99dc50283c0b3ea5bc71d9b836667d630ed88a25e6f2a5630261e4db19b299dec9a7cc6630d7fe1cda46fbc8b
data/exe/fmog ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/fmog"
5
+
6
+ Fmog::CLI.start(ARGV)
data/lib/fmog/cli.rb ADDED
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json"
5
+ require "terminal-table"
6
+
7
+ module Fmog
8
+ class BaseCLI < Thor
9
+ private
10
+
11
+ def tty?
12
+ $stdout.tty?
13
+ end
14
+
15
+ def output_single(hash, message)
16
+ if tty?
17
+ puts message
18
+ else
19
+ puts JSON.generate(hash)
20
+ end
21
+ end
22
+ end
23
+
24
+ class FeedCLI < BaseCLI
25
+ namespace "feed"
26
+
27
+ desc "add URL", "Add a feed"
28
+ def add(url)
29
+ id = Feed.add(url)
30
+ output_single({ id: id, url: url }, "Added feed ##{id}: #{url}")
31
+ rescue SQLite3::ConstraintException
32
+ $stderr.puts "Error: Feed already exists: #{url}"
33
+ exit 1
34
+ end
35
+
36
+ desc "list", "List feeds"
37
+ def list
38
+ feeds = Feed.list
39
+ if tty?
40
+ if feeds.empty?
41
+ puts "No feeds."
42
+ return
43
+ end
44
+ table = Terminal::Table.new(
45
+ headings: ["ID", "Title", "URL", "Last Fetched"],
46
+ rows: feeds.map { |f| [f["id"], f["title"] || "-", f["url"], f["last_fetched_at"] || "-"] }
47
+ )
48
+ puts table
49
+ else
50
+ feeds.each { |f| puts JSON.generate(f) }
51
+ end
52
+ end
53
+
54
+ desc "remove ID", "Remove a feed"
55
+ def remove(id)
56
+ count = Feed.remove(id.to_i)
57
+ if count > 0
58
+ output_single({ id: id.to_i }, "Removed feed ##{id}")
59
+ else
60
+ $stderr.puts "Error: Feed not found: #{id}"
61
+ exit 1
62
+ end
63
+ end
64
+
65
+ desc "fetch [ID]", "Fetch feed(s)"
66
+ def fetch(id = nil)
67
+ if id
68
+ count = Fetcher.fetch(id.to_i)
69
+ output_single({ id: id.to_i, count: count }, "Fetched #{count} items from feed ##{id}")
70
+ else
71
+ results = Fetcher.fetch_all
72
+ if tty?
73
+ results.each { |r| puts "Feed ##{r[:id]} (#{r[:url]}): #{r[:count]} items" }
74
+ else
75
+ results.each { |r| puts JSON.generate(r) }
76
+ end
77
+ end
78
+ rescue => e
79
+ $stderr.puts "Error: #{e.message}"
80
+ exit 1
81
+ end
82
+
83
+ end
84
+
85
+ class ItemCLI < BaseCLI
86
+ namespace "item"
87
+
88
+ desc "list", "List items"
89
+ option :feed, type: :numeric, desc: "Filter by feed ID"
90
+ option :unread, type: :boolean, default: false, desc: "Show only unread items"
91
+ option :limit, type: :numeric, default: 50, desc: "Max items to show"
92
+ def list
93
+ items = Item.list(feed_id: options[:feed], unread: options[:unread], limit: options[:limit])
94
+ if tty?
95
+ if items.empty?
96
+ puts "No items."
97
+ return
98
+ end
99
+ table = Terminal::Table.new(
100
+ headings: ["ID", "Feed", "Title", "Published", "Read"],
101
+ rows: items.map { |i|
102
+ read_mark = i["read_at"] ? "Y" : ""
103
+ title = (i["title"] || "").slice(0, 60)
104
+ [i["id"], i["feed_id"], title, i["published_at"] || "-", read_mark]
105
+ }
106
+ )
107
+ puts table
108
+ else
109
+ items.each { |i| puts JSON.generate(i) }
110
+ end
111
+ end
112
+
113
+ desc "show ID", "Show item details"
114
+ def show(id)
115
+ item = Item.find(id.to_i)
116
+ unless item
117
+ $stderr.puts "Error: Item not found: #{id}"
118
+ exit 1
119
+ end
120
+
121
+ if tty?
122
+ puts "ID: #{item["id"]}"
123
+ puts "Feed: #{item["feed_id"]}"
124
+ puts "Title: #{item["title"]}"
125
+ puts "URL: #{item["url"]}"
126
+ puts "Published: #{item["published_at"]}"
127
+ puts "Read: #{item["read_at"] || "no"}"
128
+ puts "---"
129
+ puts item["body"] if item["body"]
130
+ else
131
+ puts JSON.generate(item)
132
+ end
133
+ end
134
+
135
+ desc "read ID", "Mark item as read"
136
+ def read(id)
137
+ count = Item.mark_read(id.to_i)
138
+ if count > 0
139
+ output_single({ id: id.to_i, read: true }, "Marked item ##{id} as read")
140
+ else
141
+ $stderr.puts "Error: Item not found: #{id}"
142
+ exit 1
143
+ end
144
+ end
145
+
146
+ desc "unread ID", "Mark item as unread"
147
+ def unread(id)
148
+ count = Item.mark_unread(id.to_i)
149
+ if count > 0
150
+ output_single({ id: id.to_i, read: false }, "Marked item ##{id} as unread")
151
+ else
152
+ $stderr.puts "Error: Item not found: #{id}"
153
+ exit 1
154
+ end
155
+ end
156
+
157
+ end
158
+
159
+ class CLI < Thor
160
+ desc "feed SUBCOMMAND", "Manage feeds"
161
+ subcommand "feed", FeedCLI
162
+
163
+ desc "item SUBCOMMAND", "Manage items"
164
+ subcommand "item", ItemCLI
165
+ end
166
+ end
data/lib/fmog/db.rb ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sqlite3"
4
+ require "fileutils"
5
+ require "xdg"
6
+
7
+ module Fmog
8
+ module DB
9
+ DB_DIR = XDG.new.data_home.join("fmog").to_s
10
+ DB_PATH = File.join(DB_DIR, "fmog.db")
11
+
12
+ def self.connection
13
+ @connection ||= begin
14
+ FileUtils.mkdir_p(DB_DIR)
15
+ db = SQLite3::Database.new(DB_PATH)
16
+ db.results_as_hash = true
17
+ db.execute("PRAGMA foreign_keys = ON")
18
+ migrate(db)
19
+ db
20
+ end
21
+ end
22
+
23
+ def self.migrate(db)
24
+ db.execute(<<~SQL)
25
+ CREATE TABLE IF NOT EXISTS feeds (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ url TEXT NOT NULL UNIQUE,
28
+ title TEXT,
29
+ last_fetched_at DATETIME,
30
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
31
+ )
32
+ SQL
33
+
34
+ db.execute(<<~SQL)
35
+ CREATE TABLE IF NOT EXISTS items (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ feed_id INTEGER NOT NULL REFERENCES feeds(id) ON DELETE CASCADE,
38
+ guid TEXT NOT NULL,
39
+ title TEXT,
40
+ url TEXT,
41
+ body TEXT,
42
+ published_at DATETIME,
43
+ read_at DATETIME,
44
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
45
+ UNIQUE(feed_id, guid)
46
+ )
47
+ SQL
48
+ end
49
+
50
+ def self.reset!
51
+ @connection = nil
52
+ end
53
+ end
54
+ end
data/lib/fmog/feed.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fmog
4
+ module Feed
5
+ def self.add(url)
6
+ db = DB.connection
7
+ db.execute("INSERT INTO feeds (url) VALUES (?)", [url])
8
+ db.last_insert_row_id
9
+ end
10
+
11
+ def self.list
12
+ DB.connection.execute("SELECT id, url, title, last_fetched_at, created_at FROM feeds ORDER BY id")
13
+ end
14
+
15
+ def self.find(id)
16
+ DB.connection.execute("SELECT id, url, title, last_fetched_at, created_at FROM feeds WHERE id = ?", [id]).first
17
+ end
18
+
19
+ def self.remove(id)
20
+ db = DB.connection
21
+ db.execute("DELETE FROM feeds WHERE id = ?", [id])
22
+ db.changes
23
+ end
24
+
25
+ def self.update_title(id, title)
26
+ DB.connection.execute("UPDATE feeds SET title = ? WHERE id = ?", [title, id])
27
+ end
28
+
29
+ def self.touch_fetched(id)
30
+ DB.connection.execute("UPDATE feeds SET last_fetched_at = datetime('now') WHERE id = ?", [id])
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rss"
4
+ require "open-uri"
5
+
6
+ module Fmog
7
+ class Fetcher
8
+ def self.fetch(feed_id)
9
+ new(feed_id).call
10
+ end
11
+
12
+ def self.fetch_all
13
+ Feed.list.map do |feed|
14
+ count = fetch(feed["id"])
15
+ { id: feed["id"], url: feed["url"], count: count }
16
+ end
17
+ end
18
+
19
+ def initialize(feed_id)
20
+ @feed_id = feed_id
21
+ end
22
+
23
+ def call
24
+ feed = Feed.find(@feed_id)
25
+ raise "Feed not found: #{@feed_id}" unless feed
26
+
27
+ content = URI.open(feed["url"]).read
28
+ parsed = RSS::Parser.parse(content, false)
29
+ raise "Failed to parse feed: #{feed["url"]}" unless parsed
30
+
31
+ title = extract_feed_title(parsed)
32
+ Feed.update_title(@feed_id, title) if title
33
+
34
+ count = 0
35
+ items = parsed.items || []
36
+ items.each do |entry|
37
+ guid = extract_guid(entry)
38
+ next unless guid
39
+
40
+ Item.upsert(
41
+ feed_id: @feed_id,
42
+ guid: guid,
43
+ title: extract_title(entry),
44
+ url: extract_url(entry),
45
+ body: extract_body(entry),
46
+ published_at: extract_date(entry)&.strftime("%Y-%m-%d %H:%M:%S")
47
+ )
48
+ count += 1
49
+ end
50
+
51
+ Feed.touch_fetched(@feed_id)
52
+ count
53
+ end
54
+
55
+ private
56
+
57
+ def extract_feed_title(parsed)
58
+ if parsed.respond_to?(:channel) && parsed.channel
59
+ parsed.channel.title
60
+ elsif parsed.title.respond_to?(:content)
61
+ parsed.title.content
62
+ end
63
+ end
64
+
65
+ def extract_guid(entry)
66
+ # RSS 2.0
67
+ return entry.guid&.content if entry.respond_to?(:guid) && entry.guid
68
+ # Atom
69
+ return entry.id&.content if entry.respond_to?(:id) && entry.id.respond_to?(:content)
70
+ # fallback
71
+ extract_url(entry)
72
+ end
73
+
74
+ def extract_title(entry)
75
+ return entry.title.content if entry.title.respond_to?(:content)
76
+ entry.title.to_s
77
+ end
78
+
79
+ def extract_url(entry)
80
+ # Atom
81
+ if entry.respond_to?(:link) && entry.link.respond_to?(:href)
82
+ return entry.link.href
83
+ end
84
+ # RSS 2.0
85
+ return entry.link if entry.respond_to?(:link)
86
+ nil
87
+ end
88
+
89
+ def extract_body(entry)
90
+ # Atom content
91
+ if entry.respond_to?(:content) && entry.content
92
+ return entry.content.content if entry.content.respond_to?(:content)
93
+ end
94
+ # RSS description
95
+ if entry.respond_to?(:description)
96
+ return entry.description
97
+ end
98
+ nil
99
+ end
100
+
101
+ def extract_date(entry)
102
+ # Atom
103
+ return entry.updated&.content if entry.respond_to?(:updated) && entry.updated
104
+ return entry.published&.content if entry.respond_to?(:published) && entry.published
105
+ # RSS 2.0
106
+ return entry.date if entry.respond_to?(:date)
107
+ return entry.pubDate if entry.respond_to?(:pubDate)
108
+ nil
109
+ end
110
+ end
111
+ end
data/lib/fmog/item.rb ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fmog
4
+ module Item
5
+ COLUMNS = "id, feed_id, guid, title, url, body, published_at, read_at, created_at"
6
+
7
+ def self.upsert(feed_id:, guid:, title:, url:, body:, published_at:)
8
+ DB.connection.execute(<<~SQL, [feed_id, guid, title, url, body, published_at])
9
+ INSERT INTO items (feed_id, guid, title, url, body, published_at)
10
+ VALUES (?, ?, ?, ?, ?, ?)
11
+ ON CONFLICT(feed_id, guid) DO UPDATE SET
12
+ title = excluded.title,
13
+ url = excluded.url,
14
+ body = excluded.body,
15
+ published_at = excluded.published_at
16
+ SQL
17
+ end
18
+
19
+ def self.list(feed_id: nil, unread: false, limit: 50)
20
+ conditions = []
21
+ params = []
22
+
23
+ if feed_id
24
+ conditions << "feed_id = ?"
25
+ params << feed_id
26
+ end
27
+
28
+ if unread
29
+ conditions << "read_at IS NULL"
30
+ end
31
+
32
+ where = conditions.empty? ? "" : "WHERE #{conditions.join(" AND ")}"
33
+
34
+ DB.connection.execute(
35
+ "SELECT #{COLUMNS} FROM items #{where} ORDER BY published_at DESC, id DESC LIMIT ?",
36
+ params + [limit]
37
+ )
38
+ end
39
+
40
+ def self.find(id)
41
+ DB.connection.execute("SELECT #{COLUMNS} FROM items WHERE id = ?", [id]).first
42
+ end
43
+
44
+ def self.mark_read(id)
45
+ DB.connection.execute("UPDATE items SET read_at = datetime('now') WHERE id = ?", [id])
46
+ DB.connection.changes
47
+ end
48
+
49
+ def self.mark_unread(id)
50
+ DB.connection.execute("UPDATE items SET read_at = NULL WHERE id = ?", [id])
51
+ DB.connection.changes
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fmog
4
+ VERSION = "0.1.0"
5
+ end
data/lib/fmog.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "fmog/db"
4
+ require_relative "fmog/feed"
5
+ require_relative "fmog/item"
6
+ require_relative "fmog/fetcher"
7
+ require_relative "fmog/cli"
data/sig/fmog/cli.rbs ADDED
@@ -0,0 +1,25 @@
1
+ module Fmog
2
+ class BaseCLI < Thor
3
+ private
4
+
5
+ def tty?: () -> bool
6
+ def output_single: (Hash[Symbol, untyped] hash, String message) -> void
7
+ end
8
+
9
+ class FeedCLI < BaseCLI
10
+ def add: (String url) -> void
11
+ def list: () -> void
12
+ def remove: (String id) -> void
13
+ def fetch: (?String? id) -> void
14
+ end
15
+
16
+ class ItemCLI < BaseCLI
17
+ def list: () -> void
18
+ def show: (String id) -> void
19
+ def read: (String id) -> void
20
+ def unread: (String id) -> void
21
+ end
22
+
23
+ class CLI < Thor
24
+ end
25
+ end
data/sig/fmog/db.rbs ADDED
@@ -0,0 +1,12 @@
1
+ module Fmog
2
+ module DB
3
+ DB_DIR: String
4
+ DB_PATH: String
5
+
6
+ @connection: SQLite3::Database?
7
+
8
+ def self.connection: () -> SQLite3::Database
9
+ def self.migrate: (SQLite3::Database db) -> void
10
+ def self.reset!: () -> void
11
+ end
12
+ end
data/sig/fmog/feed.rbs ADDED
@@ -0,0 +1,10 @@
1
+ module Fmog
2
+ module Feed
3
+ def self.add: (String url) -> Integer
4
+ def self.list: () -> Array[Hash[String, untyped]]
5
+ def self.find: (Integer id) -> Hash[String, untyped]?
6
+ def self.remove: (Integer id) -> Integer
7
+ def self.update_title: (Integer id, String title) -> void
8
+ def self.touch_fetched: (Integer id) -> void
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ module Fmog
2
+ class Fetcher
3
+ def self.fetch: (Integer feed_id) -> Integer
4
+ def self.fetch_all: () -> Array[{ id: Integer, url: String, count: Integer }]
5
+
6
+ def initialize: (Integer feed_id) -> void
7
+ def call: () -> Integer
8
+
9
+ private
10
+
11
+ def extract_feed_title: (untyped parsed) -> String?
12
+ def extract_guid: (untyped entry) -> String?
13
+ def extract_title: (untyped entry) -> String
14
+ def extract_url: (untyped entry) -> String?
15
+ def extract_body: (untyped entry) -> String?
16
+ def extract_date: (untyped entry) -> Time?
17
+ end
18
+ end
data/sig/fmog/item.rbs ADDED
@@ -0,0 +1,11 @@
1
+ module Fmog
2
+ module Item
3
+ COLUMNS: String
4
+
5
+ def self.upsert: (feed_id: Integer, guid: String, title: String?, url: String?, body: String?, published_at: String?) -> void
6
+ def self.list: (?feed_id: Integer?, ?unread: bool, ?limit: Integer) -> Array[Hash[String, untyped]]
7
+ def self.find: (Integer id) -> Hash[String, untyped]?
8
+ def self.mark_read: (Integer id) -> Integer
9
+ def self.mark_unread: (Integer id) -> Integer
10
+ end
11
+ end
data/sig/rss.rbs ADDED
@@ -0,0 +1,5 @@
1
+ module RSS
2
+ class Parser
3
+ def self.parse: (String content, bool do_validate) -> untyped
4
+ end
5
+ end
data/sig/sqlite3.rbs ADDED
@@ -0,0 +1,13 @@
1
+ module SQLite3
2
+ class Database
3
+ attr_accessor results_as_hash: bool
4
+
5
+ def initialize: (String filename) -> void
6
+ def execute: (String sql, ?Array[untyped] bind_vars) -> Array[Hash[String, untyped]]
7
+ def last_insert_row_id: () -> Integer
8
+ def changes: () -> Integer
9
+ end
10
+
11
+ class ConstraintException < Exception
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module Terminal
2
+ class Table
3
+ def initialize: (**untyped opts) -> void
4
+ def to_s: () -> String
5
+ end
6
+ end
data/sig/thor.rbs ADDED
@@ -0,0 +1,9 @@
1
+ class Thor
2
+ def self.desc: (String usage, String description) -> void
3
+ def self.option: (Symbol name, **untyped opts) -> void
4
+ def self.namespace: (String name) -> void
5
+ def self.subcommand: (String name, Class command_class) -> void
6
+ def self.start: (Array[String] args) -> void
7
+
8
+ def options: () -> Hash[String | Symbol, untyped]
9
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fmog
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kenichi Takahashi
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rss
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sqlite3
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: thor
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.3'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ - !ruby/object:Gem::Dependency
55
+ name: terminal-table
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: xdg
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '10.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '10.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: minitest
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '5.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '5.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rake
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '13.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '13.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rbs
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '3.0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: steep
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '1.9'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '1.9'
138
+ description: fmog (Feed MogMog) is a command-line RSS/Atom feed aggregator. Subscribe
139
+ to feeds, fetch updates, and read items — all from your terminal, with JSON output
140
+ for piping.
141
+ email:
142
+ - kenichi.taka@gmail.com
143
+ executables:
144
+ - fmog
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - exe/fmog
149
+ - lib/fmog.rb
150
+ - lib/fmog/cli.rb
151
+ - lib/fmog/db.rb
152
+ - lib/fmog/feed.rb
153
+ - lib/fmog/fetcher.rb
154
+ - lib/fmog/item.rb
155
+ - lib/fmog/version.rb
156
+ - sig/fmog/cli.rbs
157
+ - sig/fmog/db.rbs
158
+ - sig/fmog/feed.rbs
159
+ - sig/fmog/fetcher.rbs
160
+ - sig/fmog/item.rbs
161
+ - sig/rss.rbs
162
+ - sig/sqlite3.rbs
163
+ - sig/terminal_table.rbs
164
+ - sig/thor.rbs
165
+ homepage: https://github.com/kenchan/fmog
166
+ licenses:
167
+ - MIT
168
+ metadata:
169
+ source_code_uri: https://github.com/kenchan/fmog
170
+ changelog_uri: https://github.com/kenchan/fmog/blob/main/CHANGELOG.md
171
+ rubygems_mfa_required: 'true'
172
+ rdoc_options: []
173
+ require_paths:
174
+ - lib
175
+ required_ruby_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '4.0'
180
+ required_rubygems_version: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: '0'
185
+ requirements: []
186
+ rubygems_version: 4.0.3
187
+ specification_version: 4
188
+ summary: Feed MogMog - CLI feed aggregator
189
+ test_files: []