exfuz 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exfuz
4
+ class FuzzyFinderCommand
5
+ attr_reader :selected
6
+
7
+ CMDS = { fzf: %w[fzf -m], peco: 'peco', percol: 'percol', sk: %w[sk -m] }.freeze
8
+
9
+ def initialize(command_type: :fzf)
10
+ @selected = ''
11
+ @cmd = CMDS[command_type] || CMDS[:fzf]
12
+ end
13
+
14
+ def run
15
+ stdio = IO.popen(@cmd, 'r+')
16
+ fiber = Fiber.new do |init_line|
17
+ puts_line(stdio, init_line)
18
+ loop do
19
+ line = Fiber.yield
20
+ puts_line(stdio, line)
21
+ end
22
+ end
23
+
24
+ yield fiber
25
+ ensure
26
+ stdio.close_write
27
+ @selected = stdio.each_line(chomp: true).to_a
28
+ stdio.close_read
29
+ end
30
+
31
+ private
32
+
33
+ def puts_line(stdio, line)
34
+ stdio.puts line
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exfuz
4
+ class Group
5
+ def initialize(positions, keys)
6
+ @data = {}
7
+ @raw_positions = positions
8
+ create(positions, keys)
9
+ end
10
+
11
+ def create(positions, keys)
12
+ indices = (0...positions.size).to_a
13
+ keys.each do |key|
14
+ @data[key] = indices.group_by { |idx| positions[idx].slice(key) }
15
+ .values
16
+ end
17
+ end
18
+
19
+ def key_positions(key)
20
+ @data[key].map do |grouping_indices|
21
+ @raw_positions[grouping_indices.first].slice(key)
22
+ end
23
+ end
24
+
25
+ def positions(key)
26
+ @data[key].each_with_object({}) do |grouping_indices, result|
27
+ key_position = @raw_positions[grouping_indices.first].slice(key)
28
+ result[key_position] = grouping_indices.map do |grouping_idx|
29
+ @raw_positions[grouping_idx]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/exfuz/jump.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Exfuz
6
+ class Jump
7
+ OPERATOR_PATH = Exfuz::Util.wsl_to_windows(File.join(__dir__, './operator.ps1'))
8
+ # powershellのホストから見て、wslがリモート扱いのためBypassを付与
9
+ OPERATOR_CMD = "PowerShell.exe -ExecutionPolicy Bypass '$Input | #{OPERATOR_PATH}'"
10
+
11
+ def initialize(positions)
12
+ @positions = positions
13
+ end
14
+
15
+ def run
16
+ data = @positions.map do |p|
17
+ { to: p.bottom_name, info: p.jump_info }
18
+ end
19
+
20
+ result = nil
21
+ IO.popen(OPERATOR_CMD, 'r+') do |io|
22
+ io.puts JSON.unparse(data)
23
+ io.close_write
24
+ result = io.read
25
+ end
26
+ result
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'curses'
4
+
5
+ module Exfuz
6
+ class KeyMap
7
+ def initialize
8
+ @kmap = {}
9
+ end
10
+
11
+ def add_event_handler(key, obj, func: :update)
12
+ @kmap[key] ||= Exfuz::Event.new
13
+ @kmap[key].add_event_handler(obj, func)
14
+ end
15
+
16
+ def pressed(key, *args)
17
+ return unless @kmap.key?(key)
18
+
19
+ @kmap[key].fired(*args)
20
+ end
21
+ end
22
+
23
+ module Key
24
+ private_constant
25
+
26
+ # input_to_name_and_charで、文字と区別するためにシンボルで定義
27
+ CTRL_A = :ctrl_a
28
+ CTRL_E = :ctrl_e
29
+ CTRL_R = :ctrl_r
30
+ CTRL_H = :ctrl_h
31
+ CTRL_L = :ctrl_l
32
+ ESC = :esc
33
+ F1 = :f1
34
+ F2 = :f2
35
+ F3 = :f3
36
+ F4 = :f4
37
+ F5 = :f5
38
+ F6 = :f6
39
+ F7 = :f7
40
+ F8 = :f8
41
+ F9 = :f9
42
+ F10 = :f10
43
+ F11 = :f11
44
+ F12 = :f12
45
+ UP = :down
46
+ DOWN = :up
47
+ RIGHT = :right
48
+ LEFT = :left
49
+ BACKSPACE = :backspace
50
+ ENTER = :enter
51
+ CHAR = :char
52
+
53
+ # メモ化してもカーソル移動が若干遅い
54
+ INPUT_TO_SPECIAL_KEY_NAME = {
55
+ 'OP' => F1,
56
+ 'OQ' => F2,
57
+ 'OR' => F3,
58
+ 'OS' => F4,
59
+ '[15' => F5,
60
+ '[17' => F6,
61
+ '[18' => F7,
62
+ '[19' => F8,
63
+ '[20' => F9,
64
+ '[21' => F10,
65
+ '[23' => F11,
66
+ '[24' => F12,
67
+ '[A' => UP,
68
+ '[B' => DOWN,
69
+ '[C' => RIGHT,
70
+ '[D' => LEFT,
71
+ Curses::Key::ENTER => ENTER,
72
+ 10 => ENTER,
73
+ 27 => ESC,
74
+ Curses::KEY_CTRL_E => CTRL_E,
75
+ Curses::KEY_CTRL_R => CTRL_R,
76
+ Curses::KEY_CTRL_H => CTRL_H,
77
+ Curses::KEY_CTRL_L => CTRL_L,
78
+ 127 => BACKSPACE # 現状は開発環境の値に合わせる (127 or 263)
79
+ }.freeze
80
+
81
+ module_function
82
+
83
+ def input_to_name_and_char(input)
84
+ case input
85
+ when Array
86
+ first = input.first
87
+ if special_key?(first)
88
+ input_to_special_key_name(input.slice(1, input.size - 1).join)
89
+ else
90
+ [CHAR, multibytes_to_char(input)]
91
+ end
92
+ when Integer
93
+ input_to_special_key_name(input)
94
+ when String
95
+ [CHAR, input]
96
+ end
97
+ end
98
+
99
+ def input_to_special_key_name(str_or_number)
100
+ INPUT_TO_SPECIAL_KEY_NAME[str_or_number]
101
+ end
102
+
103
+ def can_convert_to_name_and_char?(input)
104
+ name = input_to_name_and_char(input)
105
+ !name.nil? && name != ESC
106
+ end
107
+
108
+ def special_key?(number)
109
+ number == 27
110
+ end
111
+
112
+ def multibytes_to_char(multi_bytes)
113
+ multi_bytes.pack('C*').force_encoding(Encoding::UTF_8)
114
+ end
115
+ end
116
+ end
data/lib/exfuz/main.rb ADDED
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ def read_data(xlsxs, candidates = [], status)
4
+ xlsxs.each_with_index do |xlsx, _idx|
5
+ p = Exfuz::Parser.new(xlsx)
6
+ p.parse
7
+ p.each_cell_with_all do |cell|
8
+ b = Exfuz::BookName.new(cell[:book_name])
9
+ s = Exfuz::SheetName.new(cell[:sheet_name])
10
+ c = Exfuz::Cell.new(address: cell[:cell], value: cell[:value])
11
+ candidates.push(Exfuz::Position.new([{ book_name: b }, { sheet_name: s }, { textable: c }]))
12
+ end
13
+
14
+ status.update(1)
15
+ sleep 1
16
+ end
17
+ candidates.close_push
18
+ end
19
+
20
+ def candidate_by(candidates, line, sep: ':')
21
+ lnum = line.split(sep)[0]
22
+ candidates[lnum.to_i - 1]
23
+ end
24
+
25
+ def main
26
+ xlsxs = Dir.glob('**/[^~$]*.xlsx')
27
+
28
+ status = Exfuz::Status.new(xlsxs.size)
29
+ candidates = Exfuz::Candidates.new
30
+ key_map = Exfuz::KeyMap.new
31
+ screen = Exfuz::Screen.new(status, key_map, candidates)
32
+
33
+ screen.init
34
+ Curses.close_screen
35
+ screen.init
36
+
37
+ Thread.new do
38
+ sleep 0.01
39
+ read_data(xlsxs, candidates, status)
40
+ end
41
+
42
+ loop do
43
+ unless Thread.list.find { |t| t.name == 'wating_for_input' }
44
+ in_t = Thread.new do
45
+ screen.rerender if screen.changed_state?
46
+ end
47
+ in_t.name = 'wating_for_input'
48
+ end
49
+
50
+ screen.wait_input
51
+ break if screen.closed?
52
+ end
53
+ end
54
+
55
+ if $PROGRAM_NAME == __FILE__
56
+ $LOAD_PATH.unshift(File.expand_path('..', __dir__))
57
+ require_relative '../exfuz'
58
+ main
59
+ end
@@ -0,0 +1,62 @@
1
+ param(
2
+ [Parameter(ValueFromPipeline=$true)][string]$in
3
+ )
4
+ $excel = New-Object -ComObject Excel.Application
5
+
6
+ $excel.Visible = $true
7
+ Function Jump($to, [ref]$info) {
8
+ switch ($to) {
9
+ 'book_name' { $book = JumpBook $info.Value.book_name }
10
+ 'sheet_name' {
11
+ $book = JumpBook $info.Value.book_name
12
+ if ($book) {
13
+ $sheet = JumpSheet ([ref]$book) $info.Value.sheet_name
14
+ }
15
+ }
16
+ 'textable' {
17
+ $book = JumpBook $info.Value.book_name
18
+ if ($book) {
19
+ $sheet = JumpSheet ([ref]$book) $info.Value.sheet_name
20
+ $cell = JumpCell ([ref]$sheet) ([ref]$info.Value.textable)
21
+ }
22
+ }
23
+ Default {}
24
+ }
25
+ }
26
+
27
+ Function IsOpen($path) {
28
+ $openedPaths = @($excel.Workbooks | % {$_.Path})
29
+ if ($null -eq $openedPaths) {
30
+ return $false
31
+ }
32
+ return $openedPaths.Contains($path)
33
+ }
34
+
35
+ Function JumpBook($path) {
36
+ if(IsOpen $path) {
37
+ return $null
38
+ }
39
+ $book = $excel.Workbooks.Open($path)
40
+ return $book
41
+ }
42
+
43
+ Function JumpSheet([ref]$book, $sheetName) {
44
+ $sheet = $book.Value.Worksheets($sheetName)
45
+ $sheet.Activate()
46
+ return $sheet
47
+ }
48
+
49
+ Function JumpCell([ref]$sheet, [ref]$cellPosition) {
50
+ $cell = $sheet.Value.Cells($cellPosition.Value.row, $cellPosition.Value.col)
51
+ $cell.Activate()
52
+ return $cell
53
+ }
54
+
55
+ $targets = ConvertFrom-Json $in
56
+ try {
57
+ foreach ($target in $targets) {
58
+ $result = Jump $target.to ([ref]$target.info)
59
+ }
60
+ } finally {
61
+ #$excel.Quit()
62
+ }
@@ -0,0 +1,67 @@
1
+ require 'xsv'
2
+
3
+ module Exfuz
4
+ class Parser
5
+ EXTENSIONS = ['.xlsx', '.xls', '.xlsm'].freeze
6
+
7
+ attr_reader :sheet_names
8
+
9
+ def initialize(path)
10
+ raise 'not exsits file' unless File.exist?(path)
11
+
12
+ extname = File.extname(path)
13
+ raise 'no match extension name' unless EXTENSIONS.include?(extname)
14
+
15
+ @absolute_path = File.absolute_path(path)
16
+ @book = nil
17
+ @sheet_names = []
18
+ end
19
+
20
+ def parse
21
+ @book = Xsv.open(@absolute_path)
22
+ @sheet_names = @book.sheets.map { |s| s.name.force_encoding(Encoding::UTF_8) }
23
+ end
24
+
25
+ def book_name
26
+ @absolute_path
27
+ end
28
+
29
+ def each_cell_with_all
30
+ @book.sheets.each do |sheet|
31
+ sheet_name = sheet.name
32
+ sheet.each_with_index do |row, r_i|
33
+ row.each_with_index do |val, c_i|
34
+ next if val.nil?
35
+
36
+ cell = {
37
+ book_name: book_name,
38
+ sheet_name: sheet_name,
39
+ cell: to_address(r_i + 1, c_i + 1),
40
+ value: val
41
+ }
42
+ yield cell
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def to_address(r_idx, c_idx)
51
+ "$#{to_alphabet(c_idx)}$#{r_idx}"
52
+ end
53
+
54
+ def to_alphabet(idx)
55
+ large_a_z = ('A'..'Z').to_a
56
+ alphabet = ''
57
+ q = idx
58
+
59
+ until q.zero?
60
+ q, r = (q - 1).divmod(26)
61
+ alphabet.concat(large_a_z[r])
62
+ end
63
+
64
+ alphabet
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exfuz
4
+ class Position
5
+ def initialize(hierarchy)
6
+ @key_to_obj = {}
7
+ @hierarchy = hierarchy
8
+ hierarchy.each do |h|
9
+ k = h.keys[0]
10
+ @key_to_obj[k] = h[k]
11
+
12
+ instance_eval "@#{k} = h[k]", __FILE__, __LINE__
13
+ self.class.send(:attr_reader, k)
14
+ end
15
+ end
16
+
17
+ def bottom_name
18
+ @key_to_obj.keys.last
19
+ end
20
+
21
+ def slice(key)
22
+ keys = @key_to_obj.keys
23
+ remaining = keys[0, (keys.find_index { |k| k == key }) + 1]
24
+ args = @hierarchy.filter do |h|
25
+ remaining.include?(h.keys[0])
26
+ end
27
+ Exfuz::Position.new(args)
28
+ end
29
+
30
+ def slice_keys(key)
31
+ keys = @key_to_obj.keys
32
+ keys[0, (keys.find_index { |k| k == key }) + 1]
33
+ end
34
+
35
+ def match?(conditions)
36
+ conditions.each do |key, value|
37
+ unless obj = @key_to_obj[key]
38
+ raise 'not exist key'
39
+ end
40
+
41
+ return false unless obj.match?(value)
42
+ end
43
+ true
44
+ end
45
+
46
+ def jump_info
47
+ keys = @key_to_obj.keys
48
+ keys.each_with_object({}) do |key, result|
49
+ obj = send(key)
50
+ result.merge!(obj.jump_info)
51
+ end
52
+ end
53
+
54
+ def ==(other)
55
+ other.class === self && other.hash == hash
56
+ end
57
+ alias eql? ==
58
+
59
+ def hash
60
+ @key_to_obj.values.hash
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'unicode/display_width'
4
+ require 'unicode/display_width/string_ext'
5
+
6
+ module Exfuz
7
+ class Query
8
+ attr_reader :line, :caret
9
+
10
+ MAX_UTF_8_BYTES = 3
11
+
12
+ def initialize(caret)
13
+ @chars = []
14
+ @line = ''
15
+ @caret = caret
16
+ @offset = caret[1]
17
+ end
18
+
19
+ def text
20
+ @chars.join
21
+ end
22
+
23
+ def add(ch_or_chs)
24
+ if ch_or_chs.instance_of?(Array)
25
+ ch_or_chs.each do |ch|
26
+ insert_at_caret(ch)
27
+ right
28
+ end
29
+ elsif ch_or_chs.instance_of?(String)
30
+ insert_at_caret(ch_or_chs)
31
+ right
32
+ end
33
+ end
34
+
35
+ def delete
36
+ return if left_end?(@caret[1])
37
+
38
+ left
39
+ remove_at_caret
40
+ end
41
+
42
+ def right(direction = 1)
43
+ col = 0
44
+
45
+ col += 1 while direction > col && next_caret
46
+ end
47
+
48
+ def left(direction = 1)
49
+ col = 0
50
+
51
+ col += 1 while direction > col && prev_caret
52
+ end
53
+
54
+ private
55
+
56
+ # ['あ', nil]
57
+ # caret 0 -> 2 (right end)
58
+ def next_caret
59
+ return if right_end?(@caret[1])
60
+
61
+ idx = current - @offset
62
+ idx += 1 while idx < @chars.size - 1 && @chars[idx + 1].nil?
63
+ @caret[1] = @offset + idx + 1
64
+ end
65
+
66
+ # ['あ', nil]
67
+ # caret 2 -> 0 (left end)
68
+ def prev_caret
69
+ return if left_end?(@caret[1])
70
+
71
+ idx = current - @offset
72
+ idx -= 1 while !idx.zero? && @chars[idx - 1].nil?
73
+ @caret[1] = @offset + idx - 1
74
+ end
75
+
76
+ def insert_at_caret(ch)
77
+ r_arr = @chars.slice!((current - @offset)..-1)
78
+ @chars.concat([ch] + [nil] * (ch.display_width - 1) + r_arr)
79
+ @line = @chars * ''
80
+ end
81
+
82
+ # [a, あ, nil, い, nil] -> [a, い, nil]
83
+ # caret 1 -> 0
84
+ def remove_at_caret
85
+ r_arr = @chars.slice!((current - @offset)..-1)
86
+ deleted = r_arr.delete_at(0)
87
+ @chars.concat(r_arr.drop_while(&:nil?))
88
+ @line = @chars.clone.push(' ' * deleted.display_width) * ''
89
+ end
90
+
91
+ def current
92
+ @caret[1]
93
+ end
94
+
95
+ def left_end?(col)
96
+ @offset == col
97
+ end
98
+
99
+ def right_end?(col)
100
+ @chars.size + @offset == col
101
+ end
102
+ end
103
+ end