This commit is contained in:
Jorge Morante 2023-04-12 15:46:20 +02:00
parent 4d45983f71
commit 9b50a233be
50 changed files with 2771 additions and 3 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

View File

@ -25,6 +25,7 @@ GEM
rspec-support (3.9.3)
PLATFORMS
arm64-darwin-22
ruby
x86_64-linux

BIN
bin/tmux-fingers Executable file

Binary file not shown.

BIN
bin/tmux-fingers.dwarf Normal file

Binary file not shown.

3
kill-windows.sh Normal file
View File

@ -0,0 +1,3 @@
for id in $(tmux list-windows -F "#{window_id}:#{window_name}" | grep 'fingers' | cut -f1 -d:); do
tmux kill-window -t $id
done

6
lib/.shards.info Normal file
View File

@ -0,0 +1,6 @@
---
version: 1.0
shards:
priority_queue:
git: https://github.com/amadanmath/priority_queue.cr.git
version: 0.1.0+git.commit.5ce7c82b876644f28715d384f3615a2dc7b2a66b

25
lib/fingers/cli.cr Normal file
View File

@ -0,0 +1,25 @@
require "fingers/commands/start"
require "fingers/commands/load_config"
require "fingers/commands/send_input"
require "fingers/commands/version"
module Fingers
class Cli
def run
command, *args = ARGV
cmd = case command
when "start"
Fingers::Commands::Start.new(args)
when "load-config"
Fingers::Commands::LoadConfig.new(args)
when "send-input"
Fingers::Commands::SendInput.new(args)
when "version"
Fingers::Commands::Version.new(args)
end
cmd.run if cmd
end
end
end

View File

@ -0,0 +1,12 @@
module Fingers::Commands
class Base
@args : Array(String)
def initialize(args)
@args = args
end
def run
end
end
end

View File

@ -0,0 +1,212 @@
require "fingers/dirs"
require "fingers/config"
require "tmux"
require "file_utils"
class Fingers::Commands::LoadConfig < Fingers::Commands::Base
@fingers_options_names : Array(String) | Nil
DISALLOWED_CHARS = /cimqn/
FINGERS_FILE_PATH = "#{ENV["HOME"]}/.fingersrc"
DEFAULT_PATTERNS = {
"ip": "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}",
"uuid": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
"sha": "[0-9a-f]{7,128}",
"digit": "[0-9]{4,}",
#"url": "((https?://|git@|git://|ssh://|ftp://|file:///)[^ ()"\"]+)",
"path": "(([.\\w\\-~\\$@]+)?(/[.\\w\\-@]+)+/?)"
}
ALPHABET_MAP = {
"qwerty": "asdfqwerzxcvjklmiuopghtybn",
"qwerty-homerow": "asdfjklgh",
"qwerty-left-hand": "asdfqwerzcxv",
"qwerty-right-hand": "jkluiopmyhn",
"azerty": "qsdfazerwxcvjklmuiopghtybn",
"azerty-homerow": "qsdfjkmgh",
"azerty-left-hand": "qsdfazerwxcv",
"azerty-right-hand": "jklmuiophyn",
"qwertz": "asdfqweryxcvjkluiopmghtzbn",
"qwertz-homerow": "asdfghjkl",
"qwertz-left-hand": "asdfqweryxcv",
"qwertz-right-hand": "jkluiopmhzn",
"dvorak": "aoeuqjkxpyhtnsgcrlmwvzfidb",
"dvorak-homerow": "aoeuhtnsid",
"dvorak-left-hand": "aoeupqjkyix",
"dvorak-right-hand": "htnsgcrlmwvz",
"colemak": "arstqwfpzxcvneioluymdhgjbk",
"colemak-homerow": "arstneiodh",
"colemak-left-hand": "arstqwfpzxcv",
"colemak-right-hand": "neioluymjhk"
}
def run
ensure_cache_folder
validate_options!
parse_tmux_conf
setup_bindings
end
#private
def parse_tmux_conf
options = shell_safe_options
user_defined_patterns = [] of String
Fingers.reset_config
config = Fingers::Config.new
options.each do |option, value|
puts "#{option} => #{value}"
# TODO generate an enum somehow and use an exhaustive case
case option
when "key"
config.key = value
when "keyboard_layout"
config.keyboard_layout = value
when "main_action"
config.main_action = value
when "ctrl_action"
config.ctrl_action = value
when "alt_action"
config.alt_action = value
when "shift_action"
config.shift_action = value
when "hint_format"
config.hint_format = tmux.parse_format(value)
when "selected_hint_format"
config.selected_hint_format = tmux.parse_format(value)
when "highlight_format"
config.highlight_format = tmux.parse_format(value)
when "selected_highlight_format"
config.selected_highlight_format = tmux.parse_format(value)
end
if option.match(/pattern/)
user_defined_patterns.push(value)
end
end
config.patterns = clean_up_patterns([
*enabled_default_patterns,
*user_defined_patterns
])
config.alphabet = ALPHABET_MAP[Fingers.config.keyboard_layout].split("")
puts config
config.save
Fingers.reset_config
end
def clean_up_patterns(patterns)
patterns.reject { |pattern| pattern.empty? }
end
def setup_bindings
`tmux bind-key #{Fingers.config.key} run-shell -b "#{cli} start "\#{pane_id}" self >>#{Fingers::Dirs::LOG_PATH} 2>&1"`
`tmux bind-key O run-shell -b "#{cli} start "\#{pane_id}" other >>#{Fingers::Dirs::LOG_PATH} 2>&1"`
setup_fingers_mode_bindings
end
def setup_fingers_mode_bindings
("a".."z").to_a.each do |char|
next if char.match(DISALLOWED_CHARS)
fingers_mode_bind(char, "hint:#{char}:main")
fingers_mode_bind(char.upcase, "hint:#{char}:shift")
fingers_mode_bind("C-#{char}", "hint:#{char}:ctrl")
fingers_mode_bind("M-#{char}", "hint:#{char}:alt")
end
fingers_mode_bind("Space", "fzf")
fingers_mode_bind("C-c", "exit")
fingers_mode_bind("q", "exit")
fingers_mode_bind("Escape", "exit")
fingers_mode_bind("?", "toggle-help")
fingers_mode_bind("Enter", "noop")
fingers_mode_bind("Tab", "toggle-multi-mode")
fingers_mode_bind("Any", "noop")
end
def enabled_default_patterns
DEFAULT_PATTERNS.values
end
def to_bool(input)
input == "1"
end
def shell_safe_options
options = {} of String => String
fingers_options_names.each do |option|
option_method = option_to_method(option)
options[option_method] = `tmux show-option -gv #{option}`.chomp
end
options
end
def valid_option?(option)
option_method = option_to_method(option)
# TODO crystal does not support responds_to in runtime i think
true
#Fingers.config.responds_to?(option_method.to_sym) || option.match(/^@fingers-pattern-\d+$/)
end
def ensure_cache_folder
FileUtils.mkdir_p(Fingers::Dirs::CACHE) unless File.exists?(Fingers::Dirs::CACHE)
end
def fingers_options_names
@fingers_options_names ||= `tmux show-options -g | grep ^@fingers`.chomp.split("\n").map { |line| line.split(" ")[0] }
end
def unset_tmux_option!(option)
`tmux set-option -ug #{option}`
end
def validate_options!
errors = [] of String
fingers_options_names.each do |option|
unless valid_option?(option)
errors << "#{option} is not a valid option"
unset_tmux_option!(option)
end
end
return if errors.empty?
puts "[tmux-fingers] Errors found in tmux.conf:"
errors.each { |error| puts " - #{error}" }
exit(1)
end
def option_to_method(option)
option.gsub(/^@fingers-/, "").tr("-", "_")
end
def fingers_mode_bind(key, command)
`tmux bind-key -Tfingers "#{key}" run-shell -b "#{cli} send-input #{command}"`
end
def cli
Process.executable_path
end
def tmux
Tmux.new
end
end

View File

@ -0,0 +1,11 @@
require "fingers/commands/base"
module Fingers::Commands
class SendInput < Base
def run
socket = InputSocket.new
socket.send_message(@args[0])
end
end
end

View File

@ -0,0 +1,142 @@
require "fingers/commands/base"
require "fingers/hinter"
require "fingers/view"
require "fingers/state"
require "fingers/input_socket"
require "tmux"
module Fingers::Commands
class PanePrinter < Fingers::Printer
@pane_tty : String
@file : File
def initialize(pane_tty)
@pane_tty = pane_tty
@file = File.open(@pane_tty, "w")
end
def print(msg)
@file.print(msg)
end
def flush
@file.flush
#@file.print(@buf)
end
end
class Start < Base
@original_options : Hash(String, String) = {} of String => String
def run
pane_id, mode = @args
track_options_to_restore!
show_hints
handle_input
teardown
#tmux.swap_panes(target_pane.pane_id, fingers_window.pane_id)
#tmux.kill_window(fingers_window.window_id)
end
private def track_options_to_restore!
options_to_preserve.each do |option|
value = tmux.get_global_option(option)
@original_options[option] = value
end
end
private def restore_options
@original_options.each do |option, value|
tmux.set_global_option(option, value)
end
end
private def options_to_preserve
%w[prefix]
end
private def show_hints
view.render
fingers_pane_id = fingers_window.pane_id
tmux.swap_panes(fingers_pane_id, target_pane.pane_id)
tmux.zoom_pane(fingers_pane_id) if pane_was_zoomed?
end
private def handle_input
input_socket = InputSocket.new
#tmux.disable_prefix
tmux.set_key_table "fingers"
input_socket.on_input do |input|
view.process_input(input)
break if state.exiting
end
end
private def pane_was_zoomed?
target_pane.window_zoomed_flag
end
private def teardown
tmux.set_key_table "root"
tmux.swap_panes(fingers_pane_id, target_pane.pane_id)
tmux.kill_pane(fingers_pane_id)
tmux.zoom_pane(target_pane.pane_id) if pane_was_zoomed?
restore_options
#view.run_action if state.result
end
private getter target_pane : Tmux::Pane do
tmux.find_pane_by_id(@args[0]).not_nil!
end
private getter fingers_window : Tmux::Window do
tmux.create_window("[fingers]", "cat", 80, 24)
end
private getter fingers_pane_id : String do
fingers_window.pane_id
end
private getter pane_printer : PanePrinter do
PanePrinter.new(fingers_window.pane_tty)
end
private getter state : Fingers::State do
::Fingers::State.new
end
private getter hinter : Hinter do
Fingers::Hinter.new(
input: tmux.capture_pane(target_pane.pane_id),
width: target_pane.pane_width.to_i,
#state: state,
output: pane_printer
)
end
private getter view : View do
::Fingers::View.new(
hinter: hinter,
state: state,
output: pane_printer,
original_pane: target_pane
)
end
private getter tmux : Tmux do
Tmux.new
end
end
end

View File

@ -0,0 +1,9 @@
require "fingers/commands/base"
module Fingers::Commands
class Version < Base
def run
puts "version"
end
end
end

56
lib/fingers/config.cr Normal file
View File

@ -0,0 +1,56 @@
require "json"
module Fingers
struct Config
include JSON::Serializable
property key : String
property keyboard_layout : String
property patterns : Array(String)
property alphabet : Array(String)
property main_action : String
property ctrl_action : String
property alt_action : String
property shift_action : String
property hint_position : String
property hint_format : String
property selected_hint_format : String
property highlight_format : String
property selected_highlight_format : String
def initialize(
@key = "F",
@keyboard_layout = "qwerty",
@alphabet = [] of String,
@patterns = [] of String,
@main_action = ":copy:",
@ctrl_action = ":open:",
@alt_action = "",
@shift_action = ":paste:",
@hint_position = "left",
@hint_format = "fg=yellow,bold",
@selected_hint_format = "fg=green,bold",
@selected_highlight_format = "fg=green,nobold,dim",
@highlight_format = "fg=yellow,nobold,dim",
)
end
def self.load_from_cache
Config.from_json(File.open(::Fingers::Dirs::CONFIG_PATH))
end
def save
to_json(File.open(::Fingers::Dirs::CONFIG_PATH, "w"))
end
end
def self.config
@@config ||= Config.load_from_cache
rescue
@@config ||= Config.new
end
def self.reset_config
@@config = nil
end
end

13
lib/fingers/dirs.cr Normal file
View File

@ -0,0 +1,13 @@
# TODO maybe use some xgd shite here?
module Fingers::Dirs
TMUX_PID = (ENV["TMUX"] || ",0000").split(",")[1]
FINGERS_REPO_ROOT = Pathname.new(__dir__).parent.parent
ROOT = Path["~/.tmux"].expand(home: true)
LOG_PATH = ROOT / "fingers.log"
CACHE = ROOT / "cr-tmux-#{TMUX_PID}"
CONFIG_PATH = CACHE / "fingers.config"
SOCKET_PATH = CACHE / "fingers.sock"
end

150
lib/fingers/hinter.cr Normal file
View File

@ -0,0 +1,150 @@
require "huffman"
require "fingers/config"
require "fingers/match_formatter"
require "fingers/types"
module Fingers
class Hinter
@formatter : Formatter
@patterns : Array(String)
@alphabet : Array(String)
@lines : Array(String) | Nil
@pattern : Regex | Nil
@hints : Array(String) | Nil
@n_matches: Int32 | Nil
def initialize(
input : String,
width : Int32,
#state,
output : Printer,
patterns = Fingers.config.patterns,
alphabet = Fingers.config.alphabet,
huffman = Huffman.new,
formatter = ::Fingers::MatchFormatter.new
)
@input = input
@width = width
@hints_by_text = {} of String => String
@lookup_table = {} of String => String
#@state = state
@output = output
@formatter = formatter
@huffman = huffman
@patterns = patterns
@alphabet = alphabet
end
def run
lines[0..-2].each { |line| process_line(line, "\n") }
process_line(lines[-1], "")
#STDOUT.flush
output.flush
build_lookup_table!
end
def lookup(hint)
lookup_table.fetch(hint) { nil }
end
def matches
@matches ||= @hints_by_text.keys.uniq.flatten
end
#private
private getter :hints,
:hints_by_text,
:input,
:lookup_table,
:width,
#:state,
:formatter,
:huffman,
:output,
:patterns,
:alphabet
def build_lookup_table!
@lookup_table = hints_by_text.invert
end
def process_line(line, ending)
result = line.gsub(pattern) { |_m| replace($~) }
output.print(result + ending)
end
def pattern : Regex
@pattern ||= Regex.new("(#{patterns.join('|')})")
end
def hints : Array(String)
return @hints.as(Array(String)) if !@hints.nil?
@hints = huffman.generate_hints(alphabet: alphabet, n: n_matches)
end
def replace(match)
text = match[0]
#captured_text = match && match.named_captures["capture"] || text
captured_text = text
#if match.named_captures["capture"]
#match_start, match_end = match.offset(0)
#capture_start, capture_end = match.offset(:capture)
#capture_offset = [capture_start - match_start, capture_end - capture_start]
#else
#capture_offset = nil
#end
if hints_by_text.has_key?(captured_text)
hint = hints_by_text[captured_text]
else
hint = hints.pop
raise "Too many matches" if hint.nil?
hints_by_text[captured_text] = hint
end
# TODO: this should be output hint without ansi escape sequences
formatter.format(
hint: hint,
highlight: text,
#selected: state.selected_hints.include?(hint),
selected: false,
offset: nil
)
end
def lines : Array(String)
@lines ||= input.split("\n")
end
def n_matches : Int32
return @n_matches.as(Int32) if !@n_matches.nil?
match_set = Set(String).new
#Fingers.benchmark_stamp('counting-matches:start')
lines.each do |line|
line.scan(pattern) do |match|
# TODO hey cuidao
match_set.add(match.to_a.first || "")
end
end
#Fingers.benchmark_stamp('counting-matches:end')
@n_matches = match_set.size
match_set.size
end
end
end

View File

@ -0,0 +1,44 @@
require "socket"
require "fingers/dirs"
module Fingers
class InputSocket
@path : String
def initialize(path = Fingers::Dirs::SOCKET_PATH.to_s)
@path = path
end
def on_input
remove_socket_file
loop do
socket = server.accept
message = socket.gets
yield (message || "")
end
end
def send_message(cmd)
socket = UNIXSocket.new(path)
socket.puts(cmd)
socket.close
end
def close
server.close
remove_socket_file
end
private getter :path
def server
@server ||= UNIXServer.new(path)
end
def remove_socket_file
`rm -rf #{path}`
end
end
end

View File

@ -33,9 +33,7 @@ class InputSocket
remove_socket_file
end
private
attr_reader :path
private getter :path
def server
@server ||= UNIXServer.new(path)

View File

@ -0,0 +1,72 @@
require "fingers/config"
require "fingers/types"
module Fingers
class MatchFormatter < Fingers::Formatter
def initialize(
hint_format : String = Fingers.config.hint_format,
highlight_format : String = Fingers.config.highlight_format,
selected_hint_format : String = Fingers.config.selected_hint_format,
selected_highlight_format : String = Fingers.config.selected_highlight_format,
hint_position : String = Fingers.config.hint_position,
reset_sequence : String = `tput sgr0`.chomp
)
@hint_format = hint_format
@highlight_format = highlight_format
@selected_hint_format = selected_hint_format
@selected_highlight_format = selected_highlight_format
@hint_position = hint_position
@reset_sequence = reset_sequence
end
def format(hint : String, highlight : String, selected : Bool, offset : Tuple(Int32, Int32) | Nil)
before_offset(offset, highlight) +
format_offset(selected, hint, within_offset(offset, highlight)) +
after_offset(offset, highlight)
end
private getter :hint_format, :highlight_format, :selected_hint_format, :selected_highlight_format, :hint_position, :reset_sequence
private def before_offset(offset, highlight)
return "" if offset.nil?
start, _ = offset
highlight[0..(start - 1)]
end
private def within_offset(offset, highlight)
return highlight if offset.nil?
start, length = offset
highlight[start..(start + length - 1)]
end
private def after_offset(offset, highlight)
return "" if offset.nil?
start, length = offset
highlight[(start + length)..]
end
private def format_offset(selected, hint, highlight)
chopped_highlight = chop_highlight(hint, highlight)
hint_pair = (selected ? selected_hint_format : hint_format) + hint
highlight_pair = (selected ? selected_highlight_format : highlight_format) + chopped_highlight
if hint_position == "right"
highlight_pair + hint_pair + reset_sequence
else
hint_pair + highlight_pair + reset_sequence
end
end
private def chop_highlight(hint, highlight)
if hint_position == "right"
highlight[0..-(hint.size + 1)] || ""
else
highlight[hint.size..-1] || ""
end
rescue
puts "failed for hint '#{hint}' and '#{highlight}'"
""
end
end
end

25
lib/fingers/state.cr Normal file
View File

@ -0,0 +1,25 @@
module Fingers
class State
def initialize
@show_help = false
@multi_mode = false
@input = ""
@modifier = ""
@selected_hints = [] of String
@selected_matches = [] of String
@multi_matches = [] of String
@result = ""
@exiting = false
end
property :show_help,
:multi_mode,
:input,
:modifier,
:selected_hints,
:selected_matches,
:multi_matches,
:result,
:exiting
end
end

10
lib/fingers/types.cr Normal file
View File

@ -0,0 +1,10 @@
module Fingers
abstract class Printer
abstract def print(msg : String)
abstract def flush()
end
abstract class Formatter
abstract def format(hint : String, highlight : String, selected : Bool, offset : Tuple(Int32, Int32) | Nil)
end
end

74
lib/fingers/view.cr Normal file
View File

@ -0,0 +1,74 @@
require "tmux"
require "fingers/hinter"
module Fingers
class View
CLEAR_ESCAPE_SEQUENCE = "\e[H\e[J"
@hinter : Hinter
@state : State
@output : Printer
@original_pane : Tmux::Pane
def initialize(
@hinter,
@output,
@original_pane,
@state
)
end
def render
output.print CLEAR_ESCAPE_SEQUENCE
hinter.run
end
def process_input(input : String)
command, *args = input.split(":")
case command
when "hint"
char, modifier = args
hint(char, modifier)
when "exit"
request_exit!
when "toggle-help"
when "toggle-toggle-multi-mode"
when "fzf"
# soon
end
end
private def hide_cursor
output.print `tput civis`
end
private def hint(char, modifier)
state.input += char
state.modifier = modifier
match = hinter.lookup(state.input)
match = hinter.lookup(state.input)
handle_match(match) if match
end
private getter :output, :hinter, :original_pane, :state
private def handle_match(match)
if state.multi_mode
state.multi_matches << match
state.selected_hints << state.input
state.input = ""
render
else
state.result = match
request_exit!
end
end
private def request_exit!
state.exiting = true
end
end
end

135
lib/huffman.cr Normal file
View File

@ -0,0 +1,135 @@
require "priority_queue"
class HuffmanNode
def initialize(weight : Int32, children : Array(HuffmanNode))
@weight = weight
@children = children
end
getter :children, :weight
end
class Huffman
@alphabet : Array(String)
@n : Int32
getter :alphabet, :n, :queue
def initialize
@n = 0
@queue = PriorityQueue(HuffmanNode).new
@alphabet = [] of String
end
def generate_hints(alphabet : Array(String), n : Int32)
puts "generating for n: #{n} alphabet: #{alphabet}"
setup!(alphabet: alphabet, n: n)
return alphabet if n <= alphabet.size
first_node = true
while queue.size > 1
if first_node
n_branches = initial_number_of_branches
first_node = false
else
n_branches = arity
end
smallest = get_smallest(n_branches)
puts "smallest: #{smallest.map { |node| node.weight }}"
new_node = new_node_from(smallest)
queue.push(new_node.weight, new_node)
end
result = [] of String
root = queue.pop
puts root.weight
#traverse_inline(root)
traverse_tree(root) do |node, path|
#puts "node #{node.weight} path: #{path}"
result.push(translate_path(path)) if node.children.empty?
end
result.sort_by { |hint| hint.size }
end
#private
#attr_reader :alphabet, :n, :heap
def setup!(alphabet, n)
@alphabet = alphabet
@n = n
@queue = build_heap
end
def initial_number_of_branches
result = 1
(1..(n.to_i // arity.to_i + 1)).to_a.each do |t|
result = n - t * (arity - 1)
break if result >= 2 && result <= arity
result = arity
end
result
end
def arity
alphabet.size
end
def build_heap
queue = PriorityQueue(HuffmanNode).new
n.times { |i| queue.push(-i.to_i, HuffmanNode.new(weight: -i, children: [] of HuffmanNode)) }
queue
end
def get_smallest(n : Int32) : Array(HuffmanNode)
puts "n: #{n}"
puts "queue.size: #{queue.size}"
result = [] of HuffmanNode
[n, queue.size].min.times.each { result.push(queue.pop) }
result
end
def new_node_from(nodes)
weight = nodes.sum do |node|
node.weight
end
HuffmanNode.new(weight: weight, children: nodes)
end
def traverse_tree(node, path = [] of Int32, &block : (HuffmanNode, Array(Int32)) -> Nil)
yield node, path
node.children.each_with_index do |child, index|
traverse_tree(child, [*path, index], &block)
end
end
def traverse_inline(node, path = [] of Int32)
puts "[inline] node: #{node} #{node.weight}, path: #{path}"
node.children.each_with_index do |child, index|
traverse_inline(child, [*path, index])
end
end
def translate_path(path)
path.map { |i| alphabet[i] }.join("")
end
end

View File

@ -44,6 +44,7 @@ class Huffman
end
smallest = get_smallest(n_branches)
puts "smallest: #{smallest.map { |node| node.weight }}"
new_node = new_node_from(smallest)
heap << new_node
@ -52,6 +53,7 @@ class Huffman
result = []
traverse_tree(heap.elements[1]) do |node, path|
puts "node #{node.weight} path: #{path}"
result.push(translate_path(path)) if node.children.empty?
end

63
lib/priority_queue.cr Normal file
View File

@ -0,0 +1,63 @@
class PriorityQueue(T)
@q : Hash(Int32, Array(T))
def initialize(data=nil)
@q = Hash(Int32, Array(T)).new do |h, k|
h[k] = [] of T
end
data.each {|priority, item| @q[priority] << item} if data
@priorities = @q.keys.sort
end
def push(priority : Int32, item : T)
@q[priority].push(item)
@priorities = @q.keys.sort
end
def pop
p = @priorities.last
item = @q[p].shift
if @q[p].empty?
@q.delete(p)
@priorities.pop
end
item
end
def peek
unless empty?
@q[@priorities[0]][0]
end
end
def empty?
@priorities.empty?
end
def each
@q.each do |priority, items|
items.each {|item| yield priority, item}
end
end
def dup
@q.each_with_object(self.class.new) do |(priority, items), obj|
items.each {|item| obj.push(priority, item)}
end
end
def merge(other)
raise TypeError unless self.class == other.class
pq = dup
other.each {|priority, item| pq.push(priority, item)}
pq # return a new object
end
def inspect
@q.inspect
end
def size
@q.values.sum { |list| list.size }
end
end

View File

@ -0,0 +1,7 @@
[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

8
lib/priority_queue/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/doc/
/lib/
/bin/
/.shards/
# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
/shard.lock

View File

@ -0,0 +1 @@
language: crystal

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Goran Topic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,41 @@
# priority_queue
A simple priority queue implementation, inspired by the article
[Implementing a Priority Queue in Ruby](http://www.brianstorti.com/implementing-a-priority-queue-in-ruby/) by Brian Storti, written mostly
as an exercise in Crystal.
## Installation
Add this to your application's `shard.yml`:
```yaml
dependencies:
priority_queue:
github: amadanmath/priority_queue.cr
```
## Usage
```crystal
require "priority_queue"
queue = PriorityQueue(Int32, String).new
queue[3] = "Three"
queue[100] = "Hundred"
queue[0] = "Zero"
queue.pop # => "Hundred"
queue.pop # => "Three"
queue.size # => 1
```
## Contributing
1. Fork it ( https://github.com/amadanmath/priority_queue.cr/fork )
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Add some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create a new Pull Request
## Contributors
- [amadanmath](https://github.com/amadanmath) Goran Topic - creator, maintainer

1
lib/priority_queue/lib Symbolic link
View File

@ -0,0 +1 @@
..

View File

@ -0,0 +1,9 @@
name: priority_queue
version: 0.1.0
authors:
- Goran Topic <amadanmath+github@gmail.com>
crystal: 0.24.1
license: MIT

View File

@ -0,0 +1,96 @@
require "./spec_helper"
describe PriorityQueue do
it "can be constructed with hash-like type literal" do
queue = PriorityQueue{
2 => "Two",
6 => "Six",
3 => "Three"
}
queue.size.should eq 3
queue.pop.should eq "Six"
end
it "removes highest-priority element" do
queue = PriorityQueue(Int32, String).new
queue[2] = "Two"
queue[6] = "Six"
queue[3] = "Three"
queue[1] = "One"
queue[4] = "Four"
queue[5] = "Five"
queue.pop.should eq "Six"
queue.pop.should eq "Five"
queue.pop.should eq "Four"
queue.pop.should eq "Three"
queue.pop.should eq "Two"
queue.pop.should eq "One"
end
it "yields to block if popped when empty" do
queue = PriorityQueue(Int32, String).new
yielded = false
queue.pop { yielded = true }
yielded.should be_true
end
it "raises an error if popped without block when empty" do
queue = PriorityQueue(Int32, String).new
expect_raises(IndexError) { queue.pop }
end
it "can peek at the top element and its priority" do
queue = PriorityQueue{
2 => "Two",
6 => "Six",
3 => "Three"
}
queue.peek?.should eq "Six"
queue.priority?.should eq 6
queue.pop
queue.peek?.should eq "Three"
queue.priority?.should eq 3
queue.pop
queue.peek?.should eq "Two"
queue.priority?.should eq 2
queue.pop
queue.peek?.should be_nil
queue.priority?.should be_nil
end
it "stringifies correctly" do
queue = PriorityQueue{
2 => "Two",
6 => "Six",
3 => "Three"
}
queue.inspect.should eq "PriorityQueue{6 => \"Six\"... [+2]}"
queue.pop
queue.inspect.should eq "PriorityQueue{3 => \"Three\"... [+1]}"
queue.pop
queue.inspect.should eq "PriorityQueue{2 => \"Two\"}"
queue.pop
queue.inspect.should eq "PriorityQueue{}"
end
it "can duplicate" do
array = [1]
queue1 = PriorityQueue{
1 => array
}
queue2 = queue1.dup
queue1.pop.pop
queue1.size.should eq 0
queue2.pop.size.should eq 0
queue2.size.should eq 0
end
it "can return all elements in order" do
queue = PriorityQueue{
2 => "Two",
6 => "Six",
3 => "Three"
}
queue.to_a.should eq ["Six", "Three", "Two"]
end
end

View File

@ -0,0 +1,2 @@
require "spec"
require "../src/priority_queue"

View File

@ -0,0 +1,173 @@
require "./priority_queue/*"
# Implements a priority queue data structure. Each element is tagged
# with a priority, which is a Comparable; `pop` removes and returns
# the element with the highest priority.
class PriorityQueue(P, T)
# The class used in the underlying array
private class Element(P, T)
include Comparable(Element)
getter priority : P
getter item : T
def initialize(@priority : P, @item : T)
end
def <=>(other : Element)
priority <=> other.priority
end
end
# Returns the underlying array
protected property elements = [] of Element(P, T)
# Returns `true` if the queue is empty, `false` otherwise
def empty?
return @elements.empty?
end
# Removes and returns the item with the highest priority.
# If the queue is empty, it yields the block.
def pop
if empty?
yield
else
exchange(0, @elements.size - 1)
max = @elements.pop
bubble_down(0)
max.item
end
end
# Like `#pop`, but raises `IndexError` if the queue is empty.
def pop
pop { raise IndexError.new }
end
# Like `#pop`, but returns `nil` if the queue is empty.
def pop?
pop { nil }
end
# Returns the item with the highest priority (without removing it).
# If the queue is empty, it yields the block.
def peek
if empty?
yield
else
@elements[0].item
end
end
# Like `#peek`, but raises `IndexError` if the queue is empty.
def peek
peek { raise IndexError.new }
end
# Like `#peek`, but returns `nil` if the queue is empty.
def peek?
peek { nil }
end
# Returns the highest priority item's priority (without removing it).
# If the queue is empty, it yields the block.
def priority
if empty?
yield
else
@elements[0].priority
end
end
# Like `#priority`, but raises `IndexError` if the queue is empty.
def priority
priority { raise IndexError.new }
end
# Like `#priority`, but returns `nil` if the queue is empty.
def priority?
priority { nil }
end
# Returns the current number of elements in the queue
def size
@elements.size
end
# Returns a human-readable string representation
def inspect(io : IO)
io << "PriorityQueue{"
unless empty?
priority.inspect(io)
io << " => "
peek.inspect(io)
if size > 1
io << "... [+"
(size - 1).inspect(io)
io << "]"
end
end
io << "}"
end
# Inserts a new element into the queue, at given priority. `priority` needs to be a `Comparable`.
def []=(priority, object)
@elements << Element.new(priority, object)
bubble_up(@elements.size - 1)
end
# Returns a new `PriorityQueue` that has exactly `self`'s elements.
def dup
other = PriorityQueue(P, T).new
other.elements = elements.dup
other
end
# Yields each element in the queue in order of priority
def each
copy = dup
until copy.empty?
yield copy.pop
end
end
# Returns an array containing each element in the queue in order of priority
def to_a
array = [] of T
each do |element|
array << element
end
array
end
private def bubble_up(index)
return if index == 0
parent_index = (index - 1) / 2
return if @elements[parent_index] >= @elements[index]
exchange(index, parent_index)
bubble_up(parent_index)
end
private def bubble_down(index)
child_index = index * 2 + 1
return if child_index >= @elements.size
not_the_last_element = child_index < @elements.size - 1
left_element = @elements[child_index]
child_index += 1 if not_the_last_element && @elements[child_index + 1] > left_element
return if @elements[index] >= @elements[child_index]
exchange(index, child_index)
bubble_down(child_index)
end
private def exchange(source, target)
@elements[source], @elements[target] = @elements[target], @elements[source]
end
end

View File

@ -0,0 +1,3 @@
class PriorityQueue(P, T)
VERSION = "0.2.0"
end

261
lib/tmux.cr Normal file
View File

@ -0,0 +1,261 @@
require "json"
require "tmux_format_printer"
def to_tmux_string(value)
# TODO tmux syntax to escape quotes
"\"\#{#{value}}\""
end
def to_tmux_number(value)
"\#{#{value}}"
end
def to_tmux_bool(value)
"\#{?#{value},true,false}"
end
def build_tmux_format(hash)
fields = hash.map do |field, type|
if type == String
"\"#{field}\": #{to_tmux_string(field)}"
elsif type == Int32
"\"#{field}\": #{to_tmux_number(field)}"
elsif type == Bool
"\"#{field}\": #{to_tmux_bool(field)}"
end
end
"{#{fields.join(",")}}"
end
# TODO maybe use system everywhere?
# rubocop:disable Metrics/ClassLength
class Tmux
struct Pane
include JSON::Serializable
property pane_id : String
property window_id : String
property pane_width : Int32
property pane_height : Int32
property pane_current_path : String
property pane_in_mode : Bool
#property scroll_position : Int32
property window_zoomed_flag : Bool
end
struct Window
include JSON::Serializable
property window_id : String
property window_width : Int32
property window_height : Int32
property pane_id : String
property pane_tty : String
end
# TODO make a macro or something
PANE_FORMAT = build_tmux_format({
pane_id: String,
window_id: String,
pane_width: Int32,
pane_height: Int32,
pane_current_path: String,
pane_in_mode: Bool,
#scroll_position: Int32,
window_zoomed_flag: Bool,
})
WINDOW_FORMAT = build_tmux_format({
window_id: String,
window_width: Int32,
window_height: Int32,
pane_id: String,
pane_tty: String,
})
@panes : Array(Pane) | Nil
#def refresh!
#@panes = nil
#@windows = nil
#end
def panes : Array(Pane)
`#{tmux} list-panes -a -F '#{PANE_FORMAT}'`.chomp.split("\n").map do |pane|
Pane.from_json(pane)
end
end
def find_pane_by_id(id) : Pane | Nil
panes.find { |pane| pane.pane_id == id }
end
def windows
`#{tmux} list-windows -a -F '#{WINDOW_FORMAT}'`.chomp.split("\n").map do |pane|
Window.from_json(pane)
end
end
def new_session(name, cmd, width, height)
flags: Array(String) = [] of String
flags.push("-f", config_file) if config_file
`env -u TMUX #{tmux} #{flags.join(" ")} new-session -d -s #{name} -x #{width} -y #{height} "#{cmd}"`
end
def start_server
flags = [] of String
flags.push("-f", config_file) if config_file
`#{tmux} #{flags.join(" ")} start-server &`
end
def pane_by_id(id)
panes.find { |pane| pane.pane_id == id }
end
def window_by_id(id)
windows.find { |window| window.window_id == id }
end
def panes_by_window_id(window_id)
panes.select { |pane| pane.window_id == window_id }
end
def pane_exec(pane_id, cmd)
send_keys(pane_id, " #{cmd}")
send_keys(pane_id, "Enter")
end
def send_keys(pane_id, keys)
`#{tmux} send-keys -t "#{pane_id}" "#{keys}"`
end
def capture_pane(pane_id)
pane = pane_by_id(pane_id)
return "" unless pane
#if pane.pane_in_mode
#start_line = -pane.scroll_position.to_i
#end_line = pane.pane_height.to_i - pane.scroll_position.to_i - 1
#`#{tmux} capture-pane -J -p -t "#{pane_id}" -S #{start_line} -E #{end_line}`
#else
`#{tmux} capture-pane -J -p -t '#{pane_id}'`.chomp
#end
end
def create_window(name, cmd, _pane_width, _pane_height)
output = `#{tmux} new-window -P -d -n '#{name}' -F '#{WINDOW_FORMAT}' '#{cmd}'`.chomp
Window.from_json(output)
end
def swap_panes(src_id, dst_id)
# TODO: -Z not supported on all tmux versions
system(tmux, ["swap-pane", "-d", "-s", src_id, "-t", dst_id])
end
def kill_pane(id)
`#{tmux} kill-pane -t #{id}`
end
def kill_window(id)
`#{tmux} kill-window -t #{id}`
end
# TODO: this command is version dependant D:
def resize_window(window_id, width, height)
system(tmux, "resize-window", "-t", window_id, "-x", width.to_s, "-y", height.to_s)
end
# TODO: this command is version dependant D:
def resize_pane(pane_id, width, height)
system(tmux, ["resize-pane", "-t", pane_id, "-x", width.to_s, "-y", height.to_s])
end
def last_pane_id
`#{tmux} display -pt":.{last}" "#{pane_id}"`
end
def set_window_option(name, value)
system(tmux, "set-window-option", name, value)
end
def set_key_table(table)
system(tmux, ["set-window-option", "key-table", table])
system(tmux, ["switch-client", "-T", table])
end
def disable_prefix
set_global_option("prefix", "None")
set_global_option("prefix2", "None")
end
def set_global_option(name, value)
system(tmux, ["set-option", "-g", name, value])
end
def get_global_option(name)
`#{tmux} show -gqv #{name}`.chomp
end
def set_buffer(value)
return unless value
system(tmux, "set-buffer", value)
end
def select_pane(id)
system(tmux, "select-pane", "-t", id)
end
def zoom_pane(id)
system(tmux, ["resize-pane", "-Z", "-t", id])
end
# TODO
def parse_format(format)
format_printer.print(format).chomp
end
def format_printer
@format_printer ||= TmuxFormatPrinter.new
end
def tmux
flags = [] of String
#flags.push("-L", socket_flag_value) if socket_flag_value
#return "tmux #{flags.join(" ")}" unless flags.empty?
"tmux"
end
def build_tmux_output_format(fields)
fields.map { |field| format("\#{%<field>s}", field: field) }.join(";")
end
def parse_tmux_formatted_output(output)
output.split("\n").map do |line|
fields = line.split(";")
yield fields
end
end
def socket_flag_value
return ENV["FINGERS_TMUX_SOCKET"] if ENV["FINGERS_TMUX_SOCKET"]
socket
end
def display_message(msg)
`#{tmux} display-message "#{msg}"`
end
end

129
lib/tmux_format_printer.cr Normal file
View File

@ -0,0 +1,129 @@
class TmuxFormatPrinter
abstract class Shell
abstract def exec(cmd)
end
FORMAT_SEPARATOR = /[ ,]+/
COLOR_MAP = {
black: 0,
red: 1,
green: 2,
yellow: 3,
blue: 4,
magenta: 5,
cyan: 6,
white: 7
}
LAYER_MAP = {
bg: "setab",
fg: "setaf"
}
STYLE_MAP = {
bright: "bold",
bold: "bold",
dim: "dim",
underscore: "smul",
reverse: "rev",
italics: "sitm"
}
class ShellExec < Shell
def exec(cmd)
`#{cmd}`.chomp
end
end
@shell : Shell
@applied_styles : Hash(String, String)
@reset_sequence : String | Nil
def initialize(shell = ShellExec.new)
@shell = shell
@applied_styles = {} of String => String
end
def print(input, reset_styles_after = false)
@applied_styles = {} of String => String
output = ""
input.split(FORMAT_SEPARATOR).each do |format|
output += parse_format(format)
end
output += reset_sequence if reset_styles_after && !@applied_styles.empty?
output
end
def parse_format(format)
if format.match(/^(bg|fg)=/)
parse_color(format)
else
parse_style(format)
end
end
def parse_color(format)
match = format.match(/(?<layer>bg|fg)=(?<color>(colou?r(?<color_code>[0-9]+)|.*))/)
return "" unless match
layer = match["layer"]
color = match["color"]
color_code = match["color_code"] if match["color_code"]?
if match["color"] == "default"
@applied_styles.delete(layer)
return reset_to_applied_styles!
end
color_to_apply = color_code || COLOR_MAP[color]
result = shell.exec("tput #{LAYER_MAP[layer]} #{color_to_apply}")
@applied_styles[layer] = result
result
end
def parse_style(format)
match = format.match(/(?<remove>no)?(?<style>.*)/)
return "" unless match
should_remove_style = match["remove"]? && match["remove"] == "no"
style = match["style"]
result = shell.exec("tput #{STYLE_MAP[style]}")
if should_remove_style
@applied_styles.delete(style)
return reset_to_applied_styles!
end
@applied_styles[style] = result
result
end
def reset_to_applied_styles!
[reset_sequence, @applied_styles.values].join
end
def reset_sequence
@reset_sequence ||= shell.exec("tput sgr0").chomp
end
def shell
@shell
end
#private
#attr_reader :shell
end

6
shard.lock Normal file
View File

@ -0,0 +1,6 @@
version: 2.0
shards:
priority_queue:
git: https://github.com/amadanmath/priority_queue.cr.git
version: 0.1.0+git.commit.5ce7c82b876644f28715d384f3615a2dc7b2a66b

17
shard.yml Normal file
View File

@ -0,0 +1,17 @@
name: fingers
version: 0.1.0
authors:
- Jorge Morante <jorge@morante.eu>
targets:
tmux-fingers:
main: src/fingers.cr
dependencies:
priority_queue:
github: amadanmath/priority_queue.cr
crystal: 1.7.3
license: MIT

View File

@ -0,0 +1,60 @@
require "spec"
require "../../../lib/fingers/hinter"
record StateDouble, selected_hints : Array(String)
class TextOutput < ::Fingers::Printer
def initialize
@contents = ""
end
def print(msg)
self.contents += msg
end
def flush
end
property :contents
end
class TestFormatter < ::Fingers::Formatter
def format(hint, highlight, selected = nil, offset = nil)
"#{hint}#{highlight}"
end
end
describe Fingers::Hinter do
input = "
ola ke ase
ke ase ola
ke olaola ke
ke ola ase
beep beep
"
width = 40
output = TextOutput.new
formatter = TestFormatter.new
patterns = ["ola"]
alphabet = "asdf".split("")
hinter = Fingers::Hinter.new(
input: input,
width: width,
patterns: patterns,
alphabet: alphabet,
output: output,
formatter: formatter,
)
it "works" do
hinter.run
puts output.contents
end
end

View File

@ -0,0 +1,19 @@
require "spec"
require "../../../lib/fingers/input_socket"
describe Fingers::InputSocket do
it "works" do
spawn do
sleep 1
sender = Fingers::InputSocket.new
sender.send_message("hey")
end
listener = Fingers::InputSocket.new
listener.on_input do |msg|
msg.should eq("hey")
break
end
end
end

View File

@ -0,0 +1,57 @@
require "spec"
require "../../../lib/fingers/match_formatter"
def setup(
hint_format : String = "#[fg=yellow,bold]",
highlight_format : String = "#[fg=yellow]",
hint_position : String = "left",
selected_hint_format : String= "#[fg=green,bold]",
selected_highlight_format : String = "#[fg=green]",
selected : Bool = false,
offset : Tuple(Int32, Int32) | Nil = nil,
hint : String = "a",
highlight : String = "yolo",
)
formatter = Fingers::MatchFormatter.new(
highlight_format: highlight_format,
hint_format: hint_format,
selected_highlight_format: selected_highlight_format,
selected_hint_format: selected_hint_format,
hint_position: hint_position,
reset_sequence: "#[reset]"
)
formatter.format(hint: hint, highlight: highlight, selected: selected, offset: offset)
end
describe Fingers::MatchFormatter do
context "when hint position" do
context "is set to left" do
it "places the hint on the left side" do
result = setup(hint_position: "left")
result.should eq("#[fg=yellow,bold]a#[fg=yellow]olo#[reset]")
end
end
context "is set to right" do
it "places the hint on the right side" do
result = setup(hint_position: "right")
result.should eq("#[fg=yellow]yol#[fg=yellow,bold]a#[reset]")
end
end
end
context "when a hint is selected" do
it "selects the correct format" do
result = setup(selected: true)
result.should eq("#[fg=green,bold]a#[fg=green]olo#[reset]")
end
end
context "when offset is provided" do
it "only highlights at specified offset" do
result = setup(offset: {1, 5}, highlight: "yoloyoloyolo", hint: "a")
result.should eq("y#[fg=yellow,bold]a#[fg=yellow]loyo#[reset]loyolo")
end
end
end

View File

@ -0,0 +1,2 @@
require "spec"
require "../../../lib/fingers/view"

682
spec/lib/huffman_spec.cr Normal file
View File

@ -0,0 +1,682 @@
require "spec"
require "../../lib/huffman"
expected_5 = [
"s",
"d",
"f",
"aa",
"as",
]
expected_50 = [
"aaa",
"aas",
"aad",
"aaf",
"asa",
"ass",
"asd",
"asf",
"ada",
"ads",
"add",
"adf",
"afs",
"afd",
"aff",
"saa",
"sas",
"sad",
"saf",
"ssa",
"sss",
"ssd",
"ssf",
"sda",
"sds",
"sdd",
"sdf",
"sfa",
"afaa",
"afas",
"afad",
"afaf",
"sfsa",
"sfss",
"sfsd",
"sfsf",
"sfda",
"sfds",
"sfdd",
"sfdf",
"sffa",
"sffs",
"sfff",
"sffda",
"sffds",
"sffdf",
"sffdda",
"sffdds",
"sffddd",
"sffddf",
]
expected_alot = [
"aaaa",
"aaas",
"aaaf",
"aasa",
"aass",
"aasd",
"aasf",
"aada",
"aads",
"aadd",
"aadf",
"aafa",
"aafs",
"aafd",
"aaff",
"asaa",
"asad",
"asaf",
"assa",
"asss",
"assd",
"assf",
"asda",
"asds",
"asdd",
"asdf",
"asfa",
"asfs",
"asfd",
"asff",
"adaa",
"adad",
"adaf",
"adsa",
"adss",
"adsd",
"adsf",
"adda",
"adds",
"addd",
"addf",
"adfa",
"adfs",
"adfd",
"adff",
"afaa",
"afas",
"afaf",
"afsa",
"afss",
"afsd",
"afsf",
"afda",
"afds",
"afdd",
"afdf",
"affa",
"affs",
"affd",
"afff",
"saaa",
"saas",
"saad",
"sasa",
"sass",
"sasd",
"sasf",
"sada",
"sads",
"sadd",
"sadf",
"safa",
"safs",
"safd",
"saff",
"ssaa",
"ssas",
"ssaf",
"sssa",
"ssss",
"sssd",
"sssf",
"ssda",
"ssds",
"ssdd",
"ssdf",
"ssfa",
"ssfs",
"ssfd",
"ssff",
"sdaa",
"sdad",
"sdaf",
"sdsa",
"sdss",
"sdsd",
"sdsf",
"sdda",
"sdds",
"sddd",
"sddf",
"sdfa",
"sdfs",
"sdfd",
"sdff",
"sfaa",
"sfad",
"sfaf",
"sfsa",
"sfss",
"sfsd",
"sfsf",
"sfda",
"sfds",
"sfdd",
"sfdf",
"sffa",
"sffs",
"sffd",
"sfff",
"daaa",
"daas",
"daad",
"dasa",
"dass",
"dasd",
"dasf",
"dada",
"dads",
"dadd",
"dadf",
"dafa",
"dafs",
"dafd",
"daff",
"dsaa",
"dsad",
"dsaf",
"dssa",
"dsss",
"dssd",
"dssf",
"dsda",
"dsds",
"dsdd",
"dsdf",
"dsfa",
"dsfs",
"dsfd",
"dsff",
"ddas",
"ddad",
"ddaf",
"ddsa",
"ddss",
"ddsd",
"ddsf",
"ddda",
"ddds",
"dddd",
"dddf",
"ddfa",
"ddfs",
"ddfd",
"ddff",
"dfaa",
"dfas",
"dfaf",
"dfsa",
"dfss",
"dfsd",
"aaada",
"aaads",
"aaadd",
"aaadf",
"asasa",
"asasd",
"asasf",
"adasa",
"adass",
"adasd",
"adasf",
"afada",
"afads",
"afadd",
"afadf",
"saafa",
"saafs",
"saafd",
"saaff",
"ssada",
"ssads",
"ssadf",
"sdasa",
"sdass",
"sdasd",
"sdasf",
"sfasa",
"sfass",
"sfasd",
"sfasf",
"daafa",
"daafs",
"daafd",
"daaff",
"dsasa",
"dsasd",
"dsasf",
"ddaaa",
"ddaas",
"ddaad",
"ddaaf",
"dfada",
"dfads",
"dfadd",
"dfadf",
"dfsfa",
"dfsfs",
"dfsfd",
"dfsff",
"dfdas",
"dfdad",
"dfdaf",
"dfdsa",
"dfdss",
"dfdsd",
"dfdsf",
"dfdda",
"dfdds",
"dfddd",
"dfddf",
"dfdfa",
"dfdfs",
"dfdfd",
"dfdff",
"dffaa",
"dffad",
"dffaf",
"dffsa",
"dffss",
"dffsd",
"dffsf",
"dffda",
"dffds",
"dffdd",
"dffdf",
"dfffa",
"dfffs",
"dfffd",
"dffff",
"faaaa",
"faaas",
"faaaf",
"faasa",
"faass",
"faasd",
"faasf",
"faada",
"faads",
"faadd",
"faadf",
"faafa",
"faafs",
"faafd",
"faaff",
"fasaa",
"fasad",
"fasaf",
"fassa",
"fasss",
"fassd",
"fassf",
"fasda",
"fasds",
"fasdd",
"fasdf",
"fasfa",
"fasfs",
"fasfd",
"fasff",
"fadas",
"fadad",
"fadaf",
"fadsa",
"fadss",
"fadsd",
"fadsf",
"fadda",
"fadds",
"faddd",
"faddf",
"fadfa",
"fadfs",
"fadfd",
"fadff",
"fafaa",
"fafad",
"fafaf",
"fafsa",
"fafss",
"fafsd",
"fafsf",
"fafda",
"fafds",
"fafdd",
"fafdf",
"faffa",
"faffs",
"faffd",
"fafff",
"fsaaa",
"fsaas",
"fsaaf",
"fsasa",
"fsass",
"fsasd",
"fsasf",
"fsada",
"fsads",
"fsadd",
"fsadf",
"fsafa",
"fsafs",
"fsafd",
"fsaff",
"fssaa",
"fssas",
"fssad",
"fsssa",
"fssss",
"fsssd",
"fsssf",
"fssda",
"fssds",
"fssdd",
"fssdf",
"fssfa",
"fssfs",
"fssfd",
"fssff",
"fsdas",
"fsdad",
"fsdaf",
"fsdsa",
"fsdss",
"fsdsd",
"fsdsf",
"fsdda",
"fsdds",
"fsddd",
"fsddf",
"fsdfa",
"fsdfs",
"fsdfd",
"fsdff",
"fsfaa",
"fsfad",
"fsfaf",
"fsfsa",
"fsfss",
"fsfsd",
"fsfsf",
"fsfda",
"fsfds",
"fsfdd",
"fsfdf",
"fsffa",
"fsffs",
"fsffd",
"fsfff",
"fdaaa",
"fdaas",
"fdaaf",
"fdasa",
"fdass",
"fdasd",
"fdasf",
"fdada",
"fdads",
"fdadd",
"fdadf",
"fdafa",
"fdafs",
"fdafd",
"fdaff",
"fdsaa",
"fdsas",
"fdsaf",
"fdssa",
"fdsss",
"fdssd",
"fdssf",
"fdsda",
"fdsds",
"fdsdd",
"fdsdf",
"fdsfa",
"fdsfs",
"fdsfd",
"fdsff",
"fddaa",
"fddad",
"fddaf",
"fddsa",
"fddss",
"fddsd",
"fddsf",
"fddda",
"fddds",
"fdddd",
"fdddf",
"fddfa",
"fddfs",
"fddfd",
"fddff",
"fdfaa",
"fdfas",
"fdfaf",
"fdfsa",
"fdfss",
"fdfsd",
"fdfsf",
"fdfda",
"fdfds",
"fdfdd",
"fdfdf",
"fdffa",
"fdffs",
"fdffd",
"fdfff",
"ffaaa",
"ffaas",
"ffaaf",
"ffasa",
"ffass",
"ffasd",
"ffasf",
"ffada",
"ffads",
"ffadd",
"ffadf",
"ffafa",
"ffafs",
"ffafd",
"ffaff",
"ffsas",
"ffsad",
"ffsaf",
"ffssa",
"ffsss",
"ffssd",
"ffssf",
"ffsda",
"ffsds",
"ffsdd",
"ffsdf",
"ffsfa",
"ffsfs",
"ffsfd",
"ffsff",
"ffdas",
"ffdad",
"ffdaf",
"ffdsa",
"ffdss",
"ffdsd",
"ffdsf",
"ffdda",
"ffdds",
"ffddd",
"ffddf",
"ffdfa",
"ffdfs",
"ffdfd",
"ffdff",
"fffaa",
"fffad",
"fffaf",
"fffsa",
"fffss",
"fffsd",
"fffsf",
"fffda",
"fffds",
"fffdd",
"fffdf",
"ffffa",
"ffffs",
"ffffd",
"fffff",
"asassa",
"asasss",
"asassd",
"asassf",
"ssadda",
"ssadds",
"ssaddd",
"ssaddf",
"dsassa",
"dsassd",
"dsassf",
"dfdaaa",
"dfdaas",
"dfdaad",
"dfdaaf",
"dffasa",
"dffass",
"dffasd",
"dffasf",
"faaada",
"faaads",
"faaadd",
"faaadf",
"fasasa",
"fasasd",
"fasasf",
"fadaaa",
"fadaas",
"fadaad",
"fadaaf",
"fafasa",
"fafass",
"fafasd",
"fafasf",
"fsaada",
"fsaads",
"fsaadd",
"fsaadf",
"fssafa",
"fssafs",
"fssafd",
"fsdaaa",
"fsdaas",
"fsdaad",
"fsdaaf",
"fsfasa",
"fsfass",
"fsfasd",
"fsfasf",
"fdaada",
"fdaads",
"fdaadd",
"fdaadf",
"fdsada",
"fdsads",
"fdsadf",
"fddasa",
"fddass",
"fddasd",
"fddasf",
"fdfada",
"fdfads",
"fdfadd",
"fdfadf",
"ffaada",
"ffaads",
"ffaadd",
"ffaadf",
"ffsaas",
"ffsaad",
"ffsaaf",
"ffdaaa",
"ffdaas",
"ffdaad",
"ffdaaf",
"fffasa",
"fffass",
"fffasd",
"fffasf",
"dsasssa",
"dsassss",
"dsasssd",
"dsasssf",
"fasassa",
"fasasss",
"fasassd",
"fasassf",
"fssaffa",
"fssaffs",
"fssaffd",
"fssafff",
"fdsadda",
"fdsadds",
"fdsaddf",
"ffsaaaa",
"ffsaaas",
"ffsaaad",
"ffsaaaf",
"fdsaddda",
"fdsaddds",
"fdsadddf",
"fdsadddda",
"fdsadddds",
"fdsaddddd",
"fdsaddddf",
]
alphabet_a = ["a", "s", "d", "f"]
alphabet_b = ["a", "s", "d", "f", "j", "k", "l", "g", "h"]
describe Huffman do
it "should work for 5" do
huffman = Huffman.new
result = huffman.generate_hints(alphabet = alphabet_a, n = 5)
result.should eq expected_5
end
it "should work for 100" do
huffman = Huffman.new
result = huffman.generate_hints(alphabet = alphabet_a, n = 50)
result.should eq expected_50
# TODO make sure priority queue is popping the same elements
end
end

14
spec/lib/huffman_spec.rb Normal file
View File

@ -0,0 +1,14 @@
require 'spec_helper'
describe Huffman do
it 'transforms tmux status line format into escape sequences' do
huffman = Huffman.new
puts huffman.generate_hints(alphabet: "asdf".split(""), n: 5)
puts "----"
puts huffman.generate_hints(alphabet: "asdf".split(""), n: 50)
puts "----"
#puts huffman.generate_hints(alphabet: ["a", "s", "d", "f", "j", "k", "l", "g", "h"], n: 595)
end
end

View File

@ -0,0 +1,39 @@
require "spec"
require "../../lib/priority_queue"
describe PriorityQueue do
it "transforms tmux status line format into escape sequences" do
test = [
[6, "drink tea"],
[3, "Clear drains"],
[4, "Feed cat"],
[5, "Make tea"],
[6, "eat biscuit"],
[1, "Solve RC tasks"],
[2, "Tax return"],
]
results = [] of String
pq = PriorityQueue(String).new
test.each do |pair|
pr, str = pair
pq.push(pr.to_i, str.to_s)
end
until pq.empty?
results.push(pq.pop)
end
expected = [
"Solve RC tasks",
"Tax return",
"Clear drains",
"Feed cat",
"Make tea",
"drink tea",
"eat biscuit",
]
results.should eq expected
end
end

View File

@ -0,0 +1,27 @@
require "spec"
require "../../lib/tmux_format_printer"
class FakeShell < TmuxFormatPrinter::Shell
def exec(cmd)
"$(#{cmd})"
end
end
describe TmuxFormatPrinter do
it "transforms tmux status line format into escape sequences" do
printer = TmuxFormatPrinter.new(shell = FakeShell.new)
result = printer.print("bg=red,fg=yellow,bold", reset_styles_after: true)
expected = "$(tput setab 1)$(tput setaf 3)$(tput bold)$(tput sgr0)"
result.should eq expected
end
it "transforms tmux status line format into escape sequences" do
printer = TmuxFormatPrinter.new(shell = FakeShell.new)
result = printer.print("bg=red,fg=yellow,bold", reset_styles_after: true)
expected = "$(tput setab 1)$(tput setaf 3)$(tput bold)$(tput sgr0)"
result.should eq expected
end
end

12
spec/lib/tmux_spec.cr Normal file
View File

@ -0,0 +1,12 @@
require "spec"
require "../../lib/tmux"
describe Tmux do
it "transforms tmux status line format into escape sequences" do
tmux = Tmux.new
panes = tmux.panes
puts panes
end
end

1
spec/spec_helper.cr Normal file
View File

@ -0,0 +1 @@
require "spec"

8
src/fingers.cr Normal file
View File

@ -0,0 +1,8 @@
require "fingers/cli"
module Fingers
VERSION = "0.1.0"
cli = Cli.new
cli.run
end