#!/usr/bin/env ruby require 'base64' require 'digest' require 'etc' require 'json' require 'readline' require 'set' require 'tempfile' require 'time' # require 'pry' # Only needed for debugging class NilClass def presence self end end class String def presence return nil if self.length == 0 self end end class Config VERSION = '1.1.3' MESSAGE_FILE = "#{ENV['HOME']}/.iris.messages" HISTORY_FILE = "#{ENV['HOME']}/.iris.history" IRIS_SCRIPT = __FILE__ ENV_EDITOR = ENV['EDITOR'].presence || `which nano`.chomp.presence @@debug_mode = false def self.hostname return @hostname if @hostname hostname = `hostname`.chomp hostname = 'localhost' if hostname.empty? components = hostname.split('.') return @hostname = hostname if components.length == 1 @hostname = components[-2..-1].compact.join('.') end def self.author user = ENV['USER'] || ENV['LOGNAME'] || ENV['USERNAME'] @author ||= "#{user}@#{self.hostname}" end def self.find_files (`ls /home/**/.iris.messages`).split("\n") end def self.messagefile_filename $test_corpus_file || Config::MESSAGE_FILE end def self.readfile_filename "#{messagefile_filename}.read" end def self.enable_debug_mode @@debug_mode = true end def self.debug? @@debug_mode end end class String COLOR_MAP = { 'n' => '0', 'i' => '1', 'u' => '4', 'v' => '7', 'r' => '31', 'g' => '32', 'y' => '33', 'b' => '34', 'm' => '35', 'c' => '36', 'w' => '37', } COLOR_RESET = "\033[0m" def color_token if self !~ /\w/ return { '\{' => '|KOPEN|', '\}' => '|KCLOSE|', '}' => COLOR_RESET}[self] end tag = self.scan(/\w/).map{ |t| COLOR_MAP[t] }.sort.join(';') "\033[#{tag}m" end def colorize r = /\\{|{[rgybmcwniuv]+\s|\\}|}/ split = self.split(r, 2) return self.color_bounded if r.match(self).nil? newstr = split.first + $&.color_token + split.last if r.match(newstr).nil? return (newstr + COLOR_RESET).gsub(/\|KOPEN\|/, '{').gsub(/\|KCLOSE\|/, '}').color_bounded end newstr.colorize.color_bounded end def decolorize self. gsub(/\\{/, '|KOPEN|'). gsub(/\\}/, '|KCLOSE|'). gsub(/{[rgybmcwniuv]+\s|}/, ''). gsub(/\|KOPEN\|/, '{'). gsub(/\|KCLOSE\|/, '}') end def wrapped(width = Display::WIDTH) self.gsub(/.{1,#{width}}(?:\s|\Z|\-)/) { ($& + 5.chr).gsub(/\n\005/,"\n").gsub(/\005/,"\n") } end def pluralize(count) count == 1 ? self : self + 's' end def color_bounded COLOR_RESET + self.gsub(/\n/, "\n#{COLOR_RESET}") + COLOR_RESET end end class Corpus def self.load if $test_corpus_file @@corpus = IrisFile.load_messages else @@corpus = Config.find_files.map { |filepath| IrisFile.load_messages(filepath) }.flatten.sort_by(&:timestamp) end @@my_corpus = IrisFile.load_messages.sort_by(&:timestamp) @@my_read_hashes = 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) agg end end def self.to_json @@corpus.to_json end def self.edited_hashes @@edited_hashes end def self.topics @@topics end def self.authors @@corpus.map(&:author).uniq.sort end def self.mine @@my_corpus end def self.is_mine?(message) @@my_corpus.map(&:hash).include? message.hash end def self.index_of(message) @@corpus.map(&:hash).index message.hash end def self.topic_index_of(message) @@topics.map(&:hash).index message.hash end def self.find_message_by_hash(hash) return nil unless hash index = @@all_hash_to_index[hash] return nil unless index @@corpus[index] end def self.find_all_by_parent_hash(hash) return [] unless hash indexes = @@all_parent_hash_to_index[hash] return [] unless indexes indexes.map{ |idx| @@corpus[idx] }.compact.select(&:show_me?) end def self.find_topic_by_id(topic_lookup) return nil unless topic_lookup index = topic_lookup.to_i - 1 @@topics[index] if index >= 0 && index < @@topics.length end def self.find_message_by_id(message_lookup) return nil unless message_lookup && message_lookup =~ /\AM\d+\Z/ index = message_lookup.gsub(/M/, '').to_i - 1 @@corpus[index] if index >= 0 && index < @@corpus.length end def self.find_topic_by_hash(topic_lookup) return nil unless topic_lookup find_message_by_hash(topic_lookup) end def self.unread_messages @@unread_messages ||= @@corpus .select { |message| message.show_me? } .reject{ |m| @@my_read_hashes.include? m.hash } .reject{ |m| @@my_corpus.map(&:hash).include? m.hash } end def self.unread_topics @@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.unread_topics_set unread_topics.map(&:hash).to_set end def self.size @@corpus.size end def self.mark_as_read(hashes) new_reads = (@@my_read_hashes + hashes).uniq.sort IrisFile.write_read_file(new_reads.to_json) Corpus.load end end class IrisFile def self.load_messages(filepath = nil) if filepath.nil? filepath = Config.messagefile_filename end return [] unless File.exists?(filepath) begin payload = JSON.parse(File.read(filepath)) rescue JSON::ParserError => e if filepath == Config.messagefile_filename Display.flowerbox( 'Your message file appears to be corrupt.', "Could not parse valid JSON from #{filepath}", 'Please fix or delete this message file to use Iris.') exit(1) else Display.say " * Unable to parse #{filepath}, skipping..." return [] end rescue Errno::EACCES => e Display.warn " * Unable to read data from #{filepath}, permission denied. Skipping..." return [] end unless payload.is_a?(Array) if filepath == Config.messagefile_filename Display.flowerbox( 'Your message file appears to be corrupt.', "Could not interpret data from #{filepath}", '(It\'s not a JSON array of messages, as far as I can tell)', 'Please fix or delete this message file to use Iris.') exit(1) else Display.say " * Unable to interpret data from #{filepath}, skipping..." return [] end end uid = File.stat(filepath).uid begin username = Etc.getpwuid(uid).name rescue ArgumentError Display.warn("'#{filepath}' does not appear to have a valid UID in /etc/passwd, skipping...") return [] end payload.map do |message_json| new_message = Message.load(message_json) new_message.validate_user(username) new_message end end def self.load_reads return [] unless File.exists? Config.readfile_filename begin read_array = JSON.parse(File.read(Config.readfile_filename)) rescue JSON::ParserError => e Display.flowerbox( 'Your read file appears to be corrupt.', "Could not parse valid JSON from #{Config.readfile_filename}", 'Please fix or delete this read file to use Iris.') exit(1) end unless read_array.is_a?(Array) Display.flowerbox( 'Your read file appears to be corrupt.', "Could not interpret data from #{Config.readfile_filename}", '(It\'s not a JSON array of message hashes, as far as I can tell)', 'Please fix or delete this read file to use Iris.') exit(1) end read_array end def self.create_message_file raise 'Should not try to create message file in test mode!' if $test_corpus_file raise 'Message file exists; refusing to overwrite!' if File.exists?(Config::MESSAGE_FILE) File.umask(0122) File.open(Config::MESSAGE_FILE, 'w') { |f| f.write('[]') } end def self.create_read_file return if File.exists?(Config.readfile_filename) File.umask(0122) File.open(Config.readfile_filename, 'w') { |f| f.write('[]') } end def self.write_corpus(corpus) File.write(Config.messagefile_filename, corpus) end def self.write_read_file(new_read_hashes) if $test_corpus_file File.write("#{$test_corpus_file}.read", new_read_hashes) else File.write(Config.readfile_filename, new_read_hashes) end end end class Message attr_reader :timestamp, :edit_hash, :author, :parent, :message, :errors, :is_deleted def initialize(message, parent = nil, author = Config.author, edit_hash = nil, timestamp = Time.now.utc.iso8601, is_deleted = nil) @message = message @parent = parent @author = author @edit_hash = edit_hash @timestamp = timestamp @hash = hash @is_deleted = is_deleted @errors = [] end def self.load(payload) data = payload if payload.is_a?(Hash) data = JSON.parse(payload) if payload.is_a?(String) loaded_message = self.new(data['data']['message'], data['data']['parent'], data['data']['author'], data['edit_hash'], data['data']['timestamp'], data['is_deleted']) loaded_message.validate_hash(data['hash']) loaded_message end def self.edit(new_text, old_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! end def edited? !(edit_hash.nil? || edit_hash.empty?) end # Only show messages that don't have a following, edited message def show_me? !Corpus.edited_hashes.include?(hash) end def validate_user(username) @errors << 'Unvalidatable; could not parse username' if username.nil? @errors << 'Unvalidatable; username is empty' if username.empty? user_regex = Regexp.new("(.*)@.*$") author_match = user_regex.match(author) unless author_match && author_match[1] == username @errors << "Bad username: got #{author}'s message from #{username}'s message file." end end def validate_hash(test_hash) if self.hash != test_hash @errors << "Broken hash: expected '#{hash.chomp}', got '#{test_hash.chomp}'" end end def valid? @errors.empty? end def replace! new_corpus = Corpus.mine.reject { |message| message.hash == self.hash } << self IrisFile.write_corpus(JSON.pretty_generate(new_corpus)) Corpus.load end def save! new_corpus = Corpus.mine << self IrisFile.write_corpus(JSON.pretty_generate(new_corpus)) Corpus.load end def hash(payload = nil) if payload.nil? return @hash if @hash payload = unconfirmed_payload.to_json end Base64.encode64(Digest::SHA1.digest(payload)) end def truncated_display_message(length) if is_deleted stub = '{r TOPIC DELETED BY AUTHOR}' else stub = message.split("\n").first end return stub.colorize if stub.decolorize.length <= length # Colorize the stub, then decolorize to strip out any partial tags stub.colorize.slice(0, length - 5 - Display.topic_index_width).decolorize + '...' end 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 stub.colorize.slice(0, length - 5 - Display.topic_index_width).decolorize + '...' end def latest_topic_timestamp (replies.map(&:timestamp).max || timestamp || 'UNKNOWN').gsub(/T/, ' ').gsub(/Z/, '') end def unread? Corpus.unread_messages.include? self end def topic_status return '{r X}' unless valid? unread_count = replies.count(&:unread?) unread_count += 1 if self.unread? return ' ' if unread_count == 0 return '*' if unread_count > 9 unread_count.to_s end def to_topic_line(index) head = [Display.print_index(index), topic_status, latest_topic_timestamp, Display.print_author(author)].join(' | ') message_stub = truncated_display_message(Display::WIDTH - head.decolorize.length - 1) '| ' + [head, message_stub].join(' | ') end def to_display error_marker = valid? ? nil : '{r ### THIS MESSAGE HAS THE FOLLOWING ERRORS ###}' error_follower = valid? ? nil : '{r ### THIS MESSAGE MAY BE CORRUPT OR MAY HAVE BEEN TAMPERED WITH ###}' message_header = "#{leader_text} On #{timestamp}, #{author} #{verb_text}..." 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] bar = indent_text + ('-' * (Display::WIDTH - indent_text.decolorize.length)) if @is_deleted message_text = nil else message_text = message.wrapped(Display::WIDTH - (indent_text.decolorize.length + 1)).split("\n").map{|m| indent_text + m }.join("\n") end [ '', error_marker, errors, error_follower, header_bar, message_text, bar ].flatten.compact.join("\n") end def to_topic_display [to_display] + replies.map(&:to_display) end # TODO: Is this only used for hashing? Maybe rename. def to_json(*args) { hash: hash, edit_hash: edit_hash, is_deleted: is_deleted, data: unconfirmed_payload }.to_json end def edit_predecessor return nil unless edit_hash 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 all_replies = Corpus.find_all_by_parent_hash(hash) all_replies += ((edit_predecessor && edit_predecessor.replies) || []) all_replies.compact.sort_by{ |reply| Corpus.index_of(reply) } end def id 'M' + (Corpus.index_of(self) + 1).to_s end def topic_id return nil unless self.is_topic? Corpus.topic_index_of(self) + 1 end private def status_flag return '{r (deleted)}' if @is_deleted '{y (edited)}' if edited? end def leader_text is_topic? ? "{g ***} [#{topic_id}] #{status_flag}" : ["{g ===}", "[#{id}]", status_flag].compact.join(' ') end def verb_text is_topic? ? 'posted' : 'replied' end def indent_text is_topic? ? '' : ' | ' end def unconfirmed_payload { author: author, parent: parent, timestamp: timestamp, message: message, } end 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 TITLE_WIDTH = WIDTH - 26 def self.permissions_error(filename, file_description, permission_string, mode_string, consequence = nil) message = [ "Your #{file_description} file has incorrect permissions! Should be \"#{permission_string}\".", "You can change this from the command line with:", " chmod #{mode_string} #{filename}", consequence ].compact self.flowerbox(message) end def self.flowerbox(*lines, box_character: '*', box_thickness: 1) box_thickness.times do say box_character * WIDTH end lines.each { |line| say line } box_thickness.times do say box_character * WIDTH end end def self.say(stuff = '') stuff = stuff.join("\n") if stuff.is_a? Array puts stuff.colorize end def self.warn(stuff = '') say("{y WARNING: }#{stuff}") if Config.debug? end def self.topic_index_width [Corpus.topics.size.to_s.length, 2].max end def self.topic_author_width Corpus.authors.map(&:length).max || 1 end def self.print_index(index) # Left-align '{w ' + ((' ' * topic_index_width) + index.to_s)[(-topic_index_width)..-1] + '}' end def self.print_author(author) # Right-align (author.to_s + (' ' * topic_author_width))[0..(topic_author_width - 1)] end def self.topic_header author_head = ('AUTHOR' + (' ' * WIDTH))[0..topic_author_width-1] '| ' + ['ID', 'U', 'TIMESTAMP ', author_head, 'TITLE'].join(' | ') end end class Interface ONE_SHOTS = %w{ compose delete edit freshen help info mark_all_read mark_read next quit reset_display topics unread } CMD_MAP = { '?' => 'help', 'c' => 'compose', 'clear' => 'reset_display', 'compose' => 'compose', 'd' => 'delete', 'delete' => 'delete', 'e' => 'edit', 'edit' => 'edit', 'f' => 'freshen', 'freshen' => 'freshen', 'h' => 'help', 'help' => 'help', 'i' => 'info', 'info ' => 'info', 'm' => 'mark_read', 'mark' => 'mark_read', 'mark_all_read' => 'mark_all_read', 'n' => 'next', 'next' => 'next', 'q' => 'quit', 'quit' => 'quit', 'r' => 'reply', 'reply' => 'reply', 'reset' => 'reset_display', 't' => 'topics', 'topics' => 'topics', 'u' => 'unread', 'undelete' => 'delete', 'unread' => 'unread', } def reset_display Display.say `tput reset`.chomp end def self.info topic_count = Corpus.topics.size unread_topic_count = Corpus.unread_topics.size message_count = Corpus.size unread_message_count = Corpus.unread_messages.size author_count = Corpus.authors.size Display.flowerbox( "Iris #{Config::VERSION}", "#{topic_count} #{'topic'.pluralize(topic_count)}, #{unread_topic_count} unread.", "#{message_count} #{'message'.pluralize(message_count)}, #{unread_message_count} unread.", "#{author_count} #{'author'.pluralize(author_count)}.", box_thickness: 0) end def info Display.say Interface.info Display.say end def self.mark_all_read Corpus.mark_as_read(Corpus.unread_messages.map(&:hash)) end def mark_all_read Display.say "Marking all messages as read..." Interface.mark_all_read Display.say "Done!" end def compose Display.say 'Writing a new topic.' message_text = external_editor() if message_text.length <= 1 Display.say '{riv Empty message, discarding...}' else Message.new(message_text).save! Display.say 'Topic saved!' end end def next Display.say if Corpus.unread_topics.size == 0 Display.say "{gvi You're all caught up! No new topics to read.}" return end message = Corpus.unread_topics.first Display.say message.to_topic_display Display.say Corpus.mark_as_read([message.hash] + message.replies.map(&:hash)) end def reply(topic_id) unless topic_id Display.say "I can't reply to nothing! Include a topic ID to reply to." return end if parent = (Corpus.find_topic_by_id(topic_id) || Corpus.find_topic_by_hash(topic_id)) reply_topic = parent.hash else Display.say "Could not reply; unable to find a topic with ID '#{topic_id}'" return end title = Corpus.find_topic_by_hash(parent.hash).truncated_message(Display::TITLE_WIDTH) Display.say Display.say "Writing a reply to topic '#{title}'" message_text = external_editor() if message_text.length <= 1 Display.say '{riv Empty message, discarding...}' else Message.new(message_text, reply_topic).save! Display.say 'Reply saved!' end end def edit(message_id = nil) unless message_id Display.say "I can't edit nothing! Include a message ID to edit." 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 edit; unable to find a message with ID '#{message_id}'" return end unless Corpus.is_mine?(message) Display.say "Message with ID '#{message_id}' belongs to someone else." Display.say "You can only edit your own messages!" return end title = message.truncated_message(Display::TITLE_WIDTH) Display.say Display.say "Editing message '#{title}'" message_text = external_editor(message.message) if message_text.length <= 1 Display.say 'Empty message, not updating...' elsif message_text == message.message Display.say 'No change made, not updating...' else Message.edit(message_text, message) Display.say 'Message edited!' end 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 Corpus.mark_as_read([message.hash] + message.replies.map(&:hash)) 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." 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 delete or undelete; unable to find a message with ID '#{message_id}'" return end unless Corpus.is_mine?(message) Display.say "Message with ID '#{message_id}' belongs to someone else." Display.say "You can only delete or undelete your own messages!" return end message.delete title = message.truncated_message(Display::TITLE_WIDTH) Display.say if message.is_deleted Display.say "{r Deleted message '#{title}' }" else Display.say "{y Undeleted message '#{title}' }" end end def external_editor(preload_text = nil) tf = Tempfile.new('iris') if preload_text tf.write(preload_text) tf.flush end raise "No `$EDITOR` environment variable set!" unless Config::ENV_EDITOR system("#{Config::ENV_EDITOR} #{tf.path}") tf.rewind message_text = tf.read tf.unlink message_text end def handle(line) tokens = line.split(/\s/) cmd = tokens.first cmd = CMD_MAP[cmd] || cmd return self.send(cmd.to_sym) if ONE_SHOTS.include?(cmd) && tokens.length == 1 return show_topic(cmd) if cmd =~ /^\d+$/ # If we've gotten this far, we must have args. Let's handle 'em. arg = tokens.last 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 def show_topic(num) index = num.to_i - 1 # TODO: Paginate here if index >= 0 && index < Corpus.topics.length msg = Corpus.topics[index] Display.say msg.to_topic_display Display.say Corpus.mark_as_read([msg.hash] + msg.replies.map(&:hash)) else Display.say 'Could not find a topic with that ID' end end def quit exit(0) end def self.start(args) self.new(args) end def prompt "#{Config.author}~> " end def initialize(args) @history_loaded = false Display.say "Welcome to Iris v#{Config::VERSION}. Type 'help' for a list of commands; Ctrl-D or 'quit' to leave." 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 unread_hashes = Corpus.unread_topics_set Corpus.topics.each_with_index do |topic, index| if unread_hashes.include?(topic.hash) 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 Display.say end def help Display.flowerbox( "Iris v#{Config::VERSION} readline interface", '', 'Commands', '========', 'READING', 'topics, t - List all topics', 'unread, u - List all topics with unread messages', '# (topic id) - Read specified topic', 'next, n - Read the next unread topic', 'mark_read #, m # - Mark the associated topic as read', 'mark_all_read - Mark all messages 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', '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', 'quit, q - Quit Iris', '', 'Full documentation available here:', 'https://github.com/Calamitous/iris/blob/master/README.md', box_character: '') end def freshen Corpus.load Display.say 'Reloaded!' unread end def readline(prompt) if !@history_loaded && File.exist?(Config::HISTORY_FILE) @history_loaded = true if File.readable?(Config::HISTORY_FILE) File.readlines(Config::HISTORY_FILE).each { |l| Readline::HISTORY.push(l.chomp) } end end if line = Readline.readline(prompt, true) if File.writable?(Config::HISTORY_FILE) File.open(Config::HISTORY_FILE) { |f| f.write(line+"\n") } end return line else return nil end end end class CLI def self.print_help Display.flowerbox( "Iris v#{Config::VERSION} command-line", '', 'Usage', '========', "#{Config::IRIS_SCRIPT} [options]", '', 'Options', '========', '--help, -h - Display this text.', '--version, -v - Display the current version of Iris.', '--stats, -s - Display Iris version and message stats.', '--interactive, -i - Enter interactive mode (default)', '--mark-all-read - Mark all messages as read.', '--dump, -d - Dump entire message corpus out.', '--test-file , -f - Use the specified test file for messages.', '--debug - Print warnings and debug informtation during use.', '', 'If no options are provided, Iris will enter interactive mode.', box_character: '') end def self.start(args) if (args & %w{-v --version}).any? Display.say "Iris #{Config::VERSION}" exit(0) end if (args & %w{-h --help}).any? print_help exit(0) end if (args & %w{-s --stats}).any? Interface.info exit(0) end if (args & %w{-d --dump}).any? puts Corpus.to_json exit(0) end if (args & %w{--mark-all-read}).any? Interface.mark_all_read exit(0) end Display.say "Unrecognized option(s) #{args.join(', ')}" Display.say "Try -h for help" exit(1) end end class Startupper INTERACTIVE_OPTIONS = %w[-i --interactive] NONINTERACTIVE_OPTIONS = %w[-d --dump -h --help -v --version -s --stats --mark-all-read] NONFILE_OPTIONS = %w[-h --help -v --version] def initialize(args) Startupper.perform_file_checks unless NONFILE_OPTIONS.include?(args) load_corpus(args) is_interactive = (args & NONINTERACTIVE_OPTIONS).none? || (args & INTERACTIVE_OPTIONS).any? Config.enable_debug_mode if (args & %w{--debug}).any? if is_interactive Interface.start(args) else CLI.start(args) end end def self.perform_file_checks unless File.exists?(Config::MESSAGE_FILE) Display.say "You don't have a message file at #{Config::MESSAGE_FILE}." response = Readline.readline 'Would you like me to create it for you? (y/n) ', true if /[Yy]/ =~ response IrisFile.create_message_file else Display.say 'Cannot run Iris without a message file!' exit(1) end end IrisFile.create_read_file if File.stat(Config::MESSAGE_FILE).mode != 33188 Display.permissions_error(Config::MESSAGE_FILE, 'message', '-rw-r--r--', '644', "Leaving your file with incorrect permissions could allow unauthorized edits!") end if File.stat(Config.readfile_filename).mode != 33188 Display.permissions_error(Config.readfile_filename, 'read', '-rw-r--r--', '644') end if File.stat(Config::IRIS_SCRIPT).mode != 33261 Display.permissions_error(Config::IRIS_SCRIPT, 'Iris', '-rwxr-xr-x', '755', 'If this file has the wrong permissions the program may be tampered with!') end end def load_corpus(args) if (args & %w{-f --test-file}).any? filename_idx = (args.index('-f') || args.index('--test-file')) + 1 filename = args[filename_idx] unless filename Display.say "Option `--test-file` (`-f`) expects a filename" exit(1) end unless File.exist?(filename) Display.say "Could not load test file: #{filename}" exit(1) end Display.say "Using Iris with test file: #{filename}" $test_corpus_file = filename end Corpus.load end end Startupper.new(ARGV) if __FILE__==$0