Features and fixes for 1.0.11

* Speed up the topic listing significantly
* Add 'unread' (short form 'u') to only list topics with unread messages
* Add 'mark_unread' (short form 'm') to mark topics as read without displaying them
* Tweaks to help text
* Default main listing to unread topics instead of listing all topics
* Updates to the way screen dimensions are calculated
* Preliminaary work to support pagination
* Change permissions message from error to warning so it only shows in debug mode
This commit is contained in:
Eric B. Budd 2020-11-26 19:57:50 -05:00
parent ff6aedd008
commit bcf74fb8b4
4 changed files with 135 additions and 34 deletions

View File

@ -35,7 +35,7 @@ Iris has a readline interface that can be used to navigate the message corpus.
### Readline Interface Example
```
%> iris
Welcome to Iris v. 1.0.10. Type "help" for a list of commands.; Ctrl-D or 'quit' to leave.
Welcome to Iris v. 1.0.11. Type "help" for a list of commands.; Ctrl-D or 'quit' to leave.
| ID | U | TIMESTAMP | AUTHOR | TITLE
| 1 | | 2018-01-24T05:49:53Z | jimmy_foo@ctrl-c.club | Welcome!
@ -308,7 +308,7 @@ This outputs the current version of Iris, along with messsage, topic, and author
```
jennie_minnie@ctrl-c.club~> info
Iris 1.0.10
Iris 1.0.11
13 topics, 0 unread.
50 messages, 0 unread.
10 authors.
@ -341,7 +341,7 @@ iris --version
```
```
Iris 1.0.10
Iris 1.0.11
```
---
@ -357,7 +357,7 @@ iris --stats
```
```
Iris 1.0.10
Iris 1.0.11
13 topics, 0 unread.
50 messages, 0 unread.
10 authors.
@ -546,6 +546,7 @@ The one place we're breaking the rules on requiring gems is in the tests. Mocha
## Technical Bits
* [Dependencies](#dependencies)
* [Conventions](#conventions)
* [Message Files](#message-files)
* [Messages](#messages)
@ -556,6 +557,14 @@ The one place we're breaking the rules on requiring gems is in the tests. Mocha
* [Topic List](#topic-list)
* [Replies](#replies)
### Dependencies
While trying to stay reasonably lightweight, Iris does dependend on a few tools being installed:
* `ls` is used to get a list of all the Iris message files on the system.
* `hostname` is used to find the name of the server Iris is running on.
* `tput` is used to get the terminal reset command.
### Conventions
Iris leans heavily on convention. Iris' security and message authentication is provided by filesystem permissions and message hashing.

13
TODO.md
View File

@ -18,13 +18,14 @@
* Gracefully handle attempt to "r 1 message"
### Features
* Add pagiation/less for long message lists
* Add pagination/less for long message lists
* https://github.com/Calamitous/iris/issues/1
* Allow shelling out to editor for message editing
* https://github.com/Calamitous/iris/issues/2
* Add local timezone rendering
* Add "Mark all read" option
* Add option to mark all messages in a thread as read
* CLI option to show response count to threads the user authored
### Tech debt
* Flesh out tests
@ -68,6 +69,16 @@
# Changelog
## 1.0.11
* Speed up the topic listing significantly
* Add 'unread' (short form 'u') to only list topics with unread messages
* Add 'mark_unread' (short form 'm') to mark topics as read without displaying them
* Tweaks to help text
* Default main listing to unread topics instead of listing all topics
* Updates to the way screen dimensions are calculated
* Preliminaary work to support pagination
* Change permissions message from error to warning so it only shows in debug mode
## 1.0.10
* ~Fix bug causing system to crash when a user removes read permissions from their directory/iris.messages file~

129
iris.rb
View File

@ -8,7 +8,7 @@ require 'time'
# require 'pry' # Only needed for debugging
class Config
VERSION = '1.0.10'
VERSION = '1.0.11'
MESSAGE_FILE = "#{ENV['HOME']}/.iris.messages"
HISTORY_FILE = "#{ENV['HOME']}/.iris.history"
IRIS_SCRIPT = __FILE__
@ -135,9 +135,14 @@ class Corpus
end
@@my_corpus = IrisFile.load_messages.sort_by(&:timestamp)
@@topics = @@corpus.select{ |m| m.parent == nil && m.show_me? }
@@my_reads = IrisFile.load_reads
@@unread_messages = nil
@@all_hash_to_index = @@corpus.reduce({}) { |agg, msg| agg[msg.hash] = @@corpus.index(msg); agg }
@@edited_hashes = @@corpus.map(&:edit_hash).compact
@@topics = @@corpus.select(&:is_topic?)
@@all_parent_hash_to_index = @@corpus.reduce({}) do |agg, msg|
agg[msg.parent] ||= []
agg[msg.parent] << @@corpus.index(msg)
@ -153,8 +158,8 @@ class Corpus
@@corpus
end
def self.displayable
@@corpus.select { |message| message.show_me? }
def self.edited_hashes
@@edited_hashes
end
def self.topics
@ -188,9 +193,9 @@ class Corpus
all[index]
end
def self.find_message_by_edit_hash(hash)
def self.has_edit_hash(hash)
return nil unless hash
Corpus.all.detect { |message| message.edit_hash == hash }
Corpus.all.map(&:edit_hash).include?(hash)
end
def self.find_all_by_parent_hash(hash)
@ -222,11 +227,21 @@ class Corpus
end
def self.unread_messages
displayable.reject{ |m| @@my_reads.include? m.hash }
@@unread_messages ||= @@corpus
.select { |message| message.show_me? }
.reject{ |m| @@my_reads.include? m.hash }
end
def self.unread_message_hashes
self.unread_messages.map(&:hash)
end
def self.unread_topics
@@topics.reject{ |m| @@my_reads.include? m.hash }
@@topics.select do |m|
# Is the topic unread, or are any of its displayable replies unread?
m.unread? ||
find_all_by_parent_hash(m.hash).reduce(false) { |agg, r| agg || r.unread? }
end
end
def self.size
@ -256,7 +271,7 @@ class IrisFile
return []
end
rescue Errno::EACCES => e
Display.say " * Unable to read data from #{filepath}, permission denied. Skipping..."
Display.warn " * Unable to read data from #{filepath}, permission denied. Skipping..."
return []
end
@ -371,6 +386,10 @@ class Message
Message.new(new_text, old_message.parent, old_message.author, old_message.hash, old_message.timestamp).save!
end
def is_topic?
parent.nil? && show_me?
end
def delete
@is_deleted = !@is_deleted
replace!
@ -380,8 +399,9 @@ class Message
!(edit_hash.nil? || edit_hash.empty?)
end
# Only show messages that don't have a following, edited message
def show_me?
!Corpus.find_message_by_edit_hash(hash)
!Corpus.edited_hashes.include?(hash)
end
def validate_user(username)
@ -444,7 +464,7 @@ class Message
def truncated_message(length)
stub = message.split("\n").first
return stub.colorize if stub.decolorize.length <= length
# colorize the stub, then decolorize to strip out any partial tags
# Colorize the stub, then decolorize to strip out any partial tags
stub.colorize.slice(0, length - 5 - Display.topic_index_width).decolorize + '...'
end
@ -479,7 +499,7 @@ class Message
header_bar = (indent_text + message_header + ('-' * (Display::WIDTH)))
header_offset = header_bar.length - header_bar.decolorize.length
header_bar = header_bar[0..Display::WIDTH+header_offset-1]
header_bar = header_bar[0..Display::WIDTH + header_offset - 1]
bar = indent_text + ('-' * (Display::WIDTH - indent_text.decolorize.length))
@ -518,6 +538,8 @@ class Message
Corpus.find_message_by_hash(edit_hash)
end
# Find all messages replying to the current topic, including replies to topics
# which have been edited.
def replies
(Corpus.find_all_by_parent_hash(hash) + ((edit_predecessor && edit_predecessor.replies) || [])).compact
end
@ -562,7 +584,14 @@ end
class Display
MIN_WIDTH = 80
MIN_HEIGHT = 8
WIDTH = [ENV['COLUMNS'].to_i, `tput cols`.chomp.to_i, MIN_WIDTH].compact.max
HEIGHT = [ENV['ROWS'].to_i, `tput lines`.chomp.to_i, MIN_HEIGHT].compact.max
# p Readline.get_screen_size
# WIDTH = Readline.get_screen_size[1]
TITLE_WIDTH = WIDTH - 26
def self.permissions_error(filename, file_description, permission_string, mode_string, consequence = nil)
message = [
@ -614,10 +643,14 @@ class Display
end
class Interface
ONE_SHOTS = %w{help topics compose quit freshen reset_display reply edit delete info}
ONE_SHOTS = %w{help topics unread compose quit freshen reset_display reply edit delete mark_read info}
CMD_MAP = {
't' => 'topics',
'topics' => 'topics',
'u' => 'unread',
'unread' => 'unread',
'm' => 'mark_read',
'mark' => 'mark_read',
'c' => 'compose',
'compose' => 'compose',
'h' => 'help',
@ -651,6 +684,7 @@ class Interface
return reply(arg) if cmd == 'reply'
return edit(arg) if cmd == 'edit'
return delete(arg) if cmd == 'delete'
return mark_read(arg) if cmd == 'mark_read'
Display.say 'Unrecognized command. Type "help" for a list of available commands.'
end
@ -700,7 +734,7 @@ class Interface
@mode = :replying
@text_buffer = ''
title = Corpus.find_topic_by_hash(parent.hash).truncated_message(Display::WIDTH - 26)
title = Corpus.find_topic_by_hash(parent.hash).truncated_message(Display::TITLE_WIDTH)
Display.say
Display.say "Writing a reply to topic '#{title}'"
Display.say 'Type a period on a line by itself to end message.'
@ -731,12 +765,33 @@ class Interface
@mode = :editing
@old_message = message
@text_buffer = ''
title = message.truncated_message(Display::WIDTH - 26)
title = message.truncated_message(Display::TITLE_WIDTH)
Display.say
Display.say "Editing message '#{title}'"
Display.say 'Type a period on a line by itself to end message.'
end
def mark_read(message_id = nil)
unless message_id
Display.say "I'm not a nihilist; I can't do something with nothing! Include a message ID to mark as read."
return
end
message =
Corpus.find_message_by_hash(message_id) ||
Corpus.find_message_by_id(message_id) ||
Corpus.find_topic_by_id(message_id)
unless message
Display.say "Could not mark as read; unable to find a message with ID '#{message_id}'"
return
end
new_reads = (Corpus.read_hashes + [message.hash] + message.replies.map(&:hash)).uniq.sort
IrisFile.write_read_file(new_reads.to_json)
Corpus.load
end
def delete(message_id = nil)
unless message_id
Display.say "I'm not a nihilist; I can't do something with nothing! Include a message ID to delete or undelete."
@ -761,7 +816,7 @@ class Interface
message.delete
title = message.truncated_message(Display::WIDTH - 26)
title = message.truncated_message(Display::TITLE_WIDTH)
Display.say
if message.is_deleted
Display.say "{r Deleted message '#{title}' }"
@ -838,6 +893,7 @@ class Interface
def show_topic(num)
index = num.to_i - 1
# TODO: Paginate here
if index >= 0 && index < Corpus.topics.length
msg = Corpus.topics[index]
@reply_topic = msg.hash
@ -873,16 +929,35 @@ class Interface
@mode = :browsing
Display.say "Welcome to Iris v#{Config::VERSION}. Type 'help' for a list of commands; Ctrl-D or 'quit' to leave."
topics
unread
while line = readline(prompt) do
handle(line)
end
end
def unread
Display.say
if Corpus.unread_topics.size == 0
Display.say "{gvi You're all caught up! No new topics to read.}"
return
end
Display.say Display.topic_header
# TODO: Paginate here
Corpus.topics.each_with_index do |topic, index|
if Corpus.unread_topics.include?(topic)
Display.say topic.to_topic_line(index + 1)
end
end
Display.say
end
def topics
Display.say
Display.say Display.topic_header
# TODO: Paginate here
Corpus.topics.each_with_index do |topic, index|
Display.say topic.to_topic_line(index + 1)
end
@ -896,20 +971,22 @@ class Interface
'Commands',
'========',
'READING',
'topics, t - List all topics',
'# (topic id) - Read specified topic',
'help, h, ? - Display this text',
'topics, t - List all topics',
'unread, u - List all topics with unread messages',
'# (topic id) - Read specified topic',
'mark_read #, m # - Mark the associated topic as read',
'help, h, ? - Display this text',
'',
'WRITING',
'compose, c - Add a new topic',
'reply #, r # - Reply to a specific topic',
'edit #, e # - Edit a topic or message',
'compose, c - Add a new topic',
'reply #, r # - Reply to a specific topic',
'edit #, e # - Edit a topic or message',
'delete #, d #, undelete # - Delete {u or undelete} a topic or message',
'',
'SCREEN AND FILE UTILITIES',
'freshen, f - Reload to get any new messages',
'reset, clear - Fix screen in case of text corruption',
'info, i - Display Iris version and message stats',
'freshen, f - Reload to get any new messages',
'reset, clear - Fix screen in case of text corruption',
'info, i - Display Iris version and message stats',
'',
'Full documentation available here:',
'https://github.com/Calamitous/iris/blob/master/README.md',

View File

@ -1,6 +1,7 @@
require 'minitest/autorun'
require 'mocha/mini_test'
$test_corpus_file = "./tests/iris.messages.json"
# Setting this before loading the main code file so that the Config contants
# load correctly. This will allows the test to pretend that user "jerryberry"
# is logged in.
@ -10,7 +11,7 @@ require './iris.rb'
describe Config do
it 'has the Iris semantic version number' do
Config::VERSION.must_match /^\d\.\d\.\d$/
Config::VERSION.must_match /^\d\.\d\.\d+$/
end
it 'has the message file location' do
@ -81,11 +82,14 @@ describe Corpus do
end
it 'returns nil if the hash is not found in the corpus' do
skip
# Corpus.load
Corpus.find_message_by_hash('NoofMcGoof').must_equal nil
end
it 'returns the message associated with the hash if it is found'
it 'returns the message associated with the hash if it is found' do
Corpus.load
Corpus.find_message_by_hash("gpY2WW/jGcH+BODgySCwDANJlIM=").must_equal "Test"
end
end
describe '.find_all_by_parent_hash' do