Skip to content

Commit

Permalink
Merge branch 'ignore_invalid_utf8'
Browse files Browse the repository at this point in the history
  • Loading branch information
garybernhardt committed Oct 14, 2013
2 parents dbf8177 + ff344e2 commit b10e3d8
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 27 deletions.
57 changes: 41 additions & 16 deletions selecta
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,19 @@ KEY_DELETE = 127.chr

class Selecta
def main
options = Options.new(20)
choices = $stdin.readlines.map(&:chomp).sort_by(&:length)
search = Search.blank(options, choices)
config = Configuration.from_inputs($stdin.readlines)
search = Search.blank(config)

search = run_in_screen(options, search)
search = run_in_screen(config, search)
puts search.selected_choice
end

# Initialize the screen (and tear it down properly when done).
def run_in_screen(options, search)
def run_in_screen(config, search)
Screen.with_screen do |screen, tty|
# We emit the number of lines we'll use later so we don't clobber whatever
# was already on the screen.
(options.visible_choices).times { tty.puts }
(config.visible_choices).times { tty.puts }
begin
ui_event_loop(search, screen, tty)
ensure
Expand Down Expand Up @@ -82,24 +81,50 @@ class Selecta
end
end

class Options < Struct.new(:visible_choices)
class Configuration < Struct.new(:visible_choices, :choices)
def initialize(visible_choices, choices)
# Constructor is defined to force argument presence; otherwise Struct
# defaults missing arguments to nil
super
end

def self.from_inputs(choices)
choices = massage_choices(choices)
Configuration.new(20, choices)
end

def self.massage_choices(choices)
choices.sort_by(&:length).map do |choice|
# Encoding to UTF-8 with `:invalid => :replace` isn't good enough; it
# still leaves some invalid characters. For example, this string will fail:
#
# echo "девуш\xD0:" | selecta
#
# Round-tripping through UTF-16, with `:invalid => :replace` as well,
# fixes this. I don't understand why. I found it via:
#
# http://stackoverflow.com/questions/2982677/ruby-1-9-invalid-byte-sequence-in-utf-8
utf16 = choice.encode('UTF-16', 'UTF-8', :invalid => :replace, :replace => '')
utf16.encode('UTF-8', 'UTF-16')
end.map(&:strip)
end
end

class Search
attr_reader :choices, :index, :query, :options
attr_reader :choices, :index, :query, :config

def initialize(vars)
@vars = vars
@options = vars.fetch(:options)
@config = vars.fetch(:config)
@choices = vars.fetch(:choices)
@index = vars.fetch(:index)
@query = vars.fetch(:query)
@done = vars.fetch(:done)
end

def self.blank(options, choices)
new(:options => options,
:choices => choices,
def self.blank(config)
new(:config => config,
:choices => config.choices,
:index => 0,
:query => "",
:done => false)
Expand All @@ -118,7 +143,7 @@ class Search
end

def down
index = [@index + 1, matches.count - 1, options.visible_choices - 1].min
index = [@index + 1, matches.count - 1, config.visible_choices - 1].min
merge(:index => index)
end

Expand Down Expand Up @@ -154,7 +179,7 @@ end
class Renderer < Struct.new(:search, :screen)
def self.render!(search, screen)
rendered = Renderer.new(search, screen).render
start_line = screen.height - search.options.visible_choices - 1
start_line = screen.height - search.config.visible_choices - 1
screen.with_cursor_hidden do
screen.write_lines(start_line, rendered.choices)
screen.move_cursor(start_line, rendered.search_line.length)
Expand All @@ -173,8 +198,8 @@ class Renderer < Struct.new(:search, :screen)
end

def correct_match_count(matches)
limited = matches[0, search.options.visible_choices]
padded = limited + [""] * (search.options.visible_choices - limited.length)
limited = matches[0, search.config.visible_choices]
padded = limited + [""] * (search.config.visible_choices - limited.length)
padded
end

Expand Down
27 changes: 27 additions & 0 deletions spec/configuration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require "base64"
require_relative "spec_helper"

describe Configuration do
describe "choices" do
it "removes leading and trailing whitespace" do
config = Configuration.from_inputs([" a choice "])
config.choices.should == ["a choice"]
end

it "silences invalid UTF characters" do
path = File.expand_path(File.join(File.dirname(__FILE__),
"invalid_utf8.txt"))
invalid_string = File.read(path)

# Make sure that the string is actually invalid.
expect do
invalid_string.strip
end.to raise_error(ArgumentError, /invalid byte sequence in UTF-8/)

# We should silently fix the error
expect do
Configuration.from_inputs([invalid_string])
end.not_to raise_error
end
end
end
1 change: 1 addition & 0 deletions spec/invalid_utf8.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# We can't use `require` or `load` because of the Bash preamble on the script.
source = File.read(File.expand_path("../../selecta", __FILE__))
preamble, source = source.split("#!ruby", 2)
eval(source)
18 changes: 7 additions & 11 deletions spec/world_spec.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
# We can't use `require` or `load` because of the Bash preamble on the script.
source = File.read(File.expand_path("../../selecta", __FILE__))
preamble, source = source.split("#!ruby", 2)
eval(source)

#load File.expand_path("../../selecta", __FILE__)
require_relative "spec_helper"

describe Search do
let(:options) { Options.new(20) }
let(:world) { Search.blank(options, ["one", "two", "three"]) }
let(:config) { Configuration.from_inputs(["one", "two", "three"]) }
let(:world) { Search.blank(config) }

it "selects the first choice by default" do
world.selected_choice.should == "one"
Expand All @@ -23,8 +18,8 @@
end

it "won't move past the visible choice limit" do
options = Options.new(2)
world = Search.blank(options, ["one", "two", "three"])
config = Configuration.new(2, ["one", "two", "three"])
world = Search.blank(config)
world.down.down.down.selected_choice.should == "two"
end

Expand Down Expand Up @@ -62,7 +57,8 @@
end

it "matches punctuation" do
world = Search.blank(options, ["/! symbols $^"])
config = Configuration.from_inputs(["/! symbols $^"])
world = Search.blank(config)
world.append_search_string("/!$^").matches.should == ["/! symbols $^"]
end
end
Expand Down

0 comments on commit b10e3d8

Please sign in to comment.