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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9b0a899fb5743a6cd65c434e4be0853019ddcf1856bdd27515518fe5a04e9716
4
+ data.tar.gz: a8d191ed60ffcba306c483027351176b90a800b494017ca049c4a9382d68ea01
5
+ SHA512:
6
+ metadata.gz: 879270cc6ab2a5e08d979d9d88995362f09ea5be623f9a9b95d54aca8e4fe35ea997625da92bfb56740d31c8a44edbe781255ba7d514bd793e086b6ffad31006
7
+ data.tar.gz: 3eebc206ab2ad1780a4c9254642fe33e8635ed4f8b09cb083c775c8fe10eb9e15d25ab875067acac4c9f3f9a10944450b77e29275a5f89e9e29cc76776f719fe
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in exfuz.gemspec
6
+ gemspec
7
+
8
+ gem 'curses'
9
+ gem 'rake', '~> 13.0'
10
+ gem 'rspec', '~> 3.0'
11
+ gem 'thor'
12
+ gem 'unicode-display_width'
13
+ gem 'xsv'
data/Gemfile.lock ADDED
@@ -0,0 +1,44 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ exfuz (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ curses (1.4.4)
10
+ diff-lcs (1.5.0)
11
+ rake (13.0.3)
12
+ rspec (3.11.0)
13
+ rspec-core (~> 3.11.0)
14
+ rspec-expectations (~> 3.11.0)
15
+ rspec-mocks (~> 3.11.0)
16
+ rspec-core (3.11.0)
17
+ rspec-support (~> 3.11.0)
18
+ rspec-expectations (3.11.0)
19
+ diff-lcs (>= 1.2.0, < 2.0)
20
+ rspec-support (~> 3.11.0)
21
+ rspec-mocks (3.11.0)
22
+ diff-lcs (>= 1.2.0, < 2.0)
23
+ rspec-support (~> 3.11.0)
24
+ rspec-support (3.11.0)
25
+ rubyzip (2.3.2)
26
+ thor (1.2.1)
27
+ unicode-display_width (2.3.0)
28
+ xsv (1.1.1)
29
+ rubyzip (>= 1.3, < 3)
30
+
31
+ PLATFORMS
32
+ x86_64-darwin-19
33
+
34
+ DEPENDENCIES
35
+ curses
36
+ exfuz!
37
+ rake (~> 13.0)
38
+ rspec (~> 3.0)
39
+ thor
40
+ unicode-display_width
41
+ xsv
42
+
43
+ BUNDLED WITH
44
+ 2.2.24
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # exfuz
2
+
3
+ exfuz is able to fuzzy search for excel by using fuzzy finder tools such as fzf, peco
4
+
5
+ ## Features
6
+
7
+ - fuzzy search is possible for excel workbook, sheet, cells
8
+ - note: In the current version, exfuz cannot be used if the Ruby environment is Windows.
9
+ - can jump to positions selected lines by fuzzy finder tools
10
+ - note: In the current version, the jump function is only available in wsl.
11
+
12
+ ## Demo
13
+ ![demo](https://user-images.githubusercontent.com/85052152/213947172-8917d9a8-1f88-42a2-9fc1-53c8de25e309.gif)
14
+
15
+ ## Installation
16
+
17
+ ```sh
18
+ gem install exfuz
19
+ ```
20
+
21
+ Install the fuzzy finder tool.[^1]
22
+ [^1]:One of the following fuzzy finder tools must be installed in advance.</br>
23
+ ・[fzf](https://github.com/junegunn/fzf)</br>
24
+ ・[peco](https://github.com/peco/peco)</br>
25
+ ・[percol](https://github.com/mooz/percol)</br>
26
+ ・[skim](https://github.com/lotabout/skim)
27
+
28
+ ## Usage
29
+
30
+ ```sh
31
+ exfuz start
32
+ ```
33
+
34
+ Sources to be searched are xlsx files under the current directory hierarchy.
35
+
36
+ ### Query
37
+ It is possible to enter a query on the initial screen. Only cells matching the query will be subject to fuzzy search.</br>
38
+ For example, in the figure below, enter the regular expression `test.*data` in the query area.
39
+ ![image](https://user-images.githubusercontent.com/85052152/215886143-6c2750a9-72d9-4b3d-ad86-334569f88642.png)
40
+
41
+ ### Item Format
42
+ The format of the items read by fuzzy finder tool is as follows.</br>
43
+
44
+ ```sh
45
+ LineNumber:FileName:SheetName:CellAddress:CellValue
46
+ ```
47
+
48
+ `LineNumber` is the order of the data passed to the standard input. Also, the delimiter character for each content is`:`.
49
+
50
+
51
+ ## Key binding
52
+
53
+ | Key | Description |
54
+ |-------------|-----------------------------------------|
55
+ | CTRL-R | Launch Fuzzy Finder tool |
56
+ | CTRL-E | Stop exfuz |
57
+ | CTRL-H | Back to previous page of selection list |
58
+ | CTRL-L | Go to next page of selection list |
59
+ | Right Arrow | Move Query cursor to right |
60
+ | Left Arrow | Move Query cursor to left |
61
+ | BACKSPACE | Delete one character in Query |
62
+
63
+ ## Options
64
+
65
+ Setting file is `.exfuz.json` .
66
+
67
+ The priority for reading the configuration is as follows.
68
+
69
+ 1. `./.exfuz.json`
70
+ 2. `~/.config/exfuz/.exfuz.json`
71
+ 3. Default settings
72
+
73
+ | Option | Values / Default Value | Description |
74
+ |---------------------------|-----------------------------------------------------------------------------------|------------------------------------------------------------------------------|
75
+ | book_name_path_type | [”relative”, “absolute”] </br> Default Value: "relative" | Format to display book name. </br> relative path or absolute path. |
76
+ | cell_position_format | [”index”, “address”] </br> Default Value: "address" | Format to display cell position </br> ex) index: $3$4, address: $C$4 |
77
+ | line_sep | Arbitary character </br> Default Value: ":" | Delimiter char </br> ex) book1.xlsx:sheet1:$A$1:value if delimiter char is : |
78
+ | split_new_line | [true, false] </br> Default Value: false | Whether to escape line breaks in cells |
79
+ | fuzzy_finder_command_type | ["fzf", "peco", "percol", "sk"][^2] </br> Default Value: "fzf" | Which fuzzy finder tool to use |
80
+ [^2]: Value to fuzzy finder tool is as followeds. </br>
81
+ ・"fzf": fzf</br>
82
+ ・"peco": peco</br>
83
+ ・"percol": percol</br>
84
+ ・"sk": skim
85
+
86
+ ## License
87
+
88
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "exfuz"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/exfuz ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "exfuz"
4
+ Exfuz::Command.start
data/exfuz.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('./lib', __FILE__))
4
+ require_relative "lib/exfuz/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "exfuz"
8
+ spec.version = Exfuz::VERSION
9
+ spec.authors = ["wata00913"]
10
+ spec.email = ["175d8639@gmail.com"]
11
+
12
+ spec.summary = "excel fuzzy finder"
13
+ spec.description = "Fuzzy finder excel. This uses a fuzzy finder such as peco or fzf."
14
+ spec.homepage = "https://github.com/wata00913/exfuz"
15
+ spec.required_ruby_version = ">= 2.4.0"
16
+
17
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = ["exfuz"]
22
+ spec.require_paths = ["lib"]
23
+
24
+ end
data/lib/exfuz/body.rb ADDED
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exfuz
4
+ class Body
5
+ attr_reader :top, :bottom
6
+
7
+ def initialize(texts: [], top: 0, bottom: 10)
8
+ @top = top
9
+ @bottom = bottom
10
+
11
+ @prev_row_to_text = {}
12
+
13
+ @size = bottom - top + 1
14
+ @texts = init_texts(texts)
15
+ init_page(@texts, @size)
16
+ end
17
+
18
+ def move_next
19
+ return if @current_page == @max_page
20
+
21
+ @current_page += 1
22
+
23
+ offset = (@current_page - 1) * @size
24
+ next_texts = @texts.slice(offset, @size).each_with_index.each_with_object({}) do |(text, idx), result|
25
+ result[idx] = text
26
+ end
27
+ change_some(next_texts)
28
+ end
29
+
30
+ def move_prev
31
+ return if @current_page == 1
32
+
33
+ @current_page -= 1
34
+
35
+ offset = (@current_page - 1) * @size
36
+ prev_texts = @texts.slice(offset, @size).each_with_index.each_with_object({}) do |(text, idx), result|
37
+ result[idx] = text
38
+ end
39
+ change_some(prev_texts)
40
+ end
41
+
42
+ def change(texts)
43
+ change_some(texts)
44
+ end
45
+
46
+ def change_some(texts)
47
+ texts.each do |line_num, text|
48
+ @prev_row_to_text[row(line_num)] = @row_to_text[row(line_num)] if @row_to_text.key?(row(line_num))
49
+ @row_to_text[row(line_num)] = text
50
+ end
51
+ end
52
+
53
+ def change_all(texts)
54
+ @texts = init_texts(texts)
55
+ @prev_row_to_text = @row_to_text
56
+ init_page(texts, @size)
57
+ end
58
+
59
+ def lines(overwrite: false)
60
+ return @row_to_text unless overwrite
61
+
62
+ overwrite_lines = @prev_row_to_text.each_with_object({}) do |(row, prev_text), result|
63
+ current_text = @row_to_text[row] || ''
64
+ result[row] = overwrite(current: current_text, prev: prev_text)
65
+ end
66
+
67
+ @row_to_text.merge(overwrite_lines)
68
+ end
69
+
70
+ def clear_prev
71
+ @prev_row_to_text = {}
72
+ end
73
+
74
+ def bottom=(bottom)
75
+ @bottom = bottom
76
+ @size = @bottom - top + 1
77
+ end
78
+
79
+ private
80
+
81
+ def row(idx)
82
+ idx + @top
83
+ end
84
+
85
+ def init_texts(texts)
86
+ @texts = texts + [''] * (@size - (texts.size % @size))
87
+ end
88
+
89
+ def init_page(texts, size)
90
+ @max_page = texts.size.zero? ? 1 : (texts.size.to_f / size).ceil
91
+ @current_page = 1
92
+ @row_to_text = texts.slice(0, size).each_with_index.each_with_object({}) do |(text, idx), result|
93
+ result[row(idx)] = text
94
+ end
95
+ end
96
+
97
+ def overwrite(current:, prev:)
98
+ if current.size < prev.size
99
+ current.ljust(prev.size)
100
+ else
101
+ current
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Exfuz
6
+ class BookName
7
+ def self.name
8
+ :book_name
9
+ end
10
+
11
+ attr_reader :absolute_path
12
+
13
+ include Exfuz::Util
14
+
15
+ def initialize(file_name)
16
+ @absolute_path = @text = File.absolute_path(file_name)
17
+ end
18
+
19
+ def ==(other)
20
+ absolute_path == other.absolute_path
21
+ end
22
+
23
+ def hash
24
+ absolute_path.hash
25
+ end
26
+
27
+ def relative_path
28
+ Pathname.new(@absolute_path).relative_path_from(Dir.pwd).to_s
29
+ end
30
+
31
+ def jump_info
32
+ path = wsl? ? wsl_to_windows(@absolute_path) : @absolute_path
33
+ { Exfuz::BookName.name => path }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Exfuz
6
+ class Candidate
7
+ def initialize(book_name:, sheet_name:, textable:)
8
+ @book_name = book_name
9
+ @sheet_name = sheet_name
10
+ @textable = textable
11
+ end
12
+
13
+ def value
14
+ @textable.value
15
+ end
16
+
17
+ def to_line
18
+ @conf ||= Exfuz::Configuration.instance
19
+ [
20
+ book_name_line,
21
+ @sheet_name,
22
+ textable_position_line,
23
+ value_line
24
+ ].join(@conf.line_sep)
25
+ end
26
+
27
+ private
28
+
29
+ def book_name_line
30
+ pname = Pathname.new(@book_name)
31
+ case @conf.book_name_path_type
32
+ when :relative
33
+ pname.relative_path_from(@conf.dirname.to_s)
34
+ when :absolute
35
+ @book_name
36
+ end
37
+ end
38
+
39
+ def textable_position_line
40
+ @textable.position_s(format: @conf.cell_position_format)
41
+ end
42
+
43
+ def value_line
44
+ text = @textable.value.to_s
45
+ @conf.split_new_line ? text : text.gsub(/\R+/) { '\n' }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exfuz
4
+ class Candidates
5
+ @@queue = Queue.new
6
+ @@raw = []
7
+
8
+ def initialize(items = [])
9
+ @processed = items
10
+ end
11
+
12
+ def push(item)
13
+ @@queue << item
14
+ end
15
+
16
+ def suspend_push
17
+ @@queue << nil
18
+ end
19
+
20
+ def close_push?
21
+ @@queue.closed?
22
+ end
23
+
24
+ def close_push
25
+ @@queue << nil
26
+ @@queue.close
27
+ end
28
+
29
+ def positions
30
+ @processed.empty? ? @@raw : @processed
31
+ end
32
+
33
+ def filter(conditions)
34
+ pipeline
35
+ filtered = positions.filter do |position|
36
+ position.match?(conditions)
37
+ end
38
+ Exfuz::Candidates.new(filtered)
39
+ end
40
+
41
+ def group_by(keys)
42
+ pipeline
43
+ Exfuz::Group.new(positions, keys)
44
+ end
45
+
46
+ private
47
+ def pipeline
48
+ while data = @@queue.pop
49
+ @processed << data
50
+ end
51
+ end
52
+ end
53
+ end
data/lib/exfuz/cell.rb ADDED
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exfuz
4
+ class Cell
5
+ def self.name
6
+ :textable
7
+ end
8
+
9
+ attr_reader :row, :col, :value
10
+
11
+ include Exfuz::Util
12
+
13
+ def initialize(value:, row: nil, col: nil, address: nil)
14
+ if row && col
15
+ @row = row
16
+ @col = col
17
+ elsif address
18
+ @row, @col = to_idx(address)
19
+ else
20
+ raise "argument error. row: #{row}, col: #{col}, address: #{address}"
21
+ end
22
+
23
+ @value = value
24
+ # valueはString型以外も含むのでマッチ用の@textは文字列に変換させる
25
+ @text = @value.to_s
26
+ end
27
+
28
+ def position_s(format: :address)
29
+ case format
30
+ when :address
31
+ to_address(@col, row)
32
+ when :index
33
+ "$#{col}$#{row}"
34
+ end
35
+ end
36
+
37
+ def ==(other)
38
+ return false if other.nil? || !other.instance_of?(Exfuz::Cell)
39
+
40
+ [@row, @col, @value] == [other.row, other.col, other.value]
41
+ end
42
+
43
+ def hash
44
+ [@row, @col, @value].hash
45
+ end
46
+
47
+ def jump_info
48
+ { Exfuz::Cell.name => { row: @row, col: @col } }
49
+ end
50
+
51
+ private
52
+
53
+ def to_idx(address)
54
+ _, c_alph, r_str = address.split('$')
55
+
56
+ c = 0
57
+ large_a_z = ('A'..'Z')
58
+ c_alph.chars.reverse.each_with_index do |char, i|
59
+ c += (26**i * (large_a_z.find_index(char) + 1))
60
+ end
61
+
62
+ [r_str.to_i, c]
63
+ end
64
+
65
+ def to_address(col, row)
66
+ "$#{to_alphabet(col)}$#{row}"
67
+ end
68
+
69
+ def to_alphabet(idx)
70
+ large_a_z = ('A'..'Z').to_a
71
+ alphabet = ''
72
+ q = idx
73
+
74
+ until q.zero?
75
+ q, r = (q - 1).divmod(26)
76
+ alphabet += large_a_z[r]
77
+ end
78
+
79
+ alphabet
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,11 @@
1
+ require 'thor'
2
+ require_relative 'main'
3
+
4
+ module Exfuz
5
+ class Command < Thor
6
+ desc "start", "start exfuz"
7
+ def start
8
+ main
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'pathname'
5
+ require 'json'
6
+
7
+ module Exfuz
8
+ class Configuration
9
+ include Singleton
10
+
11
+ GLOBAL_DIRS = [File.join(Dir.home, '.config/exfuz')].freeze
12
+ FILE_NAME = '.exfuz.json'
13
+
14
+ attr_reader :dirname
15
+
16
+ private_constant :FILE_NAME, :GLOBAL_DIRS
17
+
18
+ def initialize
19
+ @dirname = Pathname.new(Dir.pwd)
20
+ set_data
21
+ end
22
+
23
+ def book_name_path_type
24
+ @data[:book_name_path_type]
25
+ end
26
+
27
+ def cell_position_format
28
+ @data[:cell_position_format]
29
+ end
30
+
31
+ def line_sep
32
+ @data[:line_sep]
33
+ end
34
+
35
+ def split_new_line
36
+ @data[:split_new_line]
37
+ end
38
+
39
+ def jump_positions?
40
+ @data[:jump_positions]
41
+ end
42
+
43
+ def fuzzy_finder_command_type
44
+ @data[:fuzzy_finder_command_type].to_sym
45
+ end
46
+
47
+ private
48
+
49
+ def set_data
50
+ @data = default.clone
51
+ @data.merge!(read_from_global || {})
52
+ .merge!(read_from_local(@dirname) || {})
53
+ end
54
+
55
+ def read_from_local(dirname)
56
+ fpath = File.join(dirname.to_s, FILE_NAME)
57
+ return nil unless File.exist?(fpath)
58
+
59
+ read(fpath)
60
+ end
61
+
62
+ def read_from_global
63
+ fpath = GLOBAL_DIRS.map { |d| File.join(d, FILE_NAME) }
64
+ .find { |f| File.exist?(f) }
65
+ return nil unless fpath
66
+
67
+ read(fpath)
68
+ end
69
+
70
+ def read(path)
71
+ data = JSON.parse(File.read(path), symbolize_names: true)
72
+ data.transform_values { |v| v.instance_of?(String) ? v.to_sym : v }
73
+ end
74
+
75
+ def default
76
+ {
77
+ book_name_path_type: :relative,
78
+ cell_position_format: :index,
79
+ line_sep: ':',
80
+ split_new_line: false,
81
+ jump_positions: false,
82
+ fuzzy_finder_command_type: 'fzf'
83
+ }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'observer'
4
+
5
+ module Exfuz
6
+ class Event
7
+ include Observable
8
+ def add_event_handler(obj, func)
9
+ add_observer(obj, func)
10
+ end
11
+
12
+ def fired(*args)
13
+ changed
14
+ args.empty? ? notify_observers : notify_observers(*args)
15
+ end
16
+ end
17
+ end