terminal-file-picker 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36bd9443b474cff7cc402fb518e1a42f8f098a871da283384d74b5fb7cf2510a
4
- data.tar.gz: b82f7ce18c958d285d9b55b5898e23c821bf6b49dfc2fe99984a88956cd1277d
3
+ metadata.gz: cd5a569d7a6b4c67c640aef2acf33f800441b44bd780cd3d3330b396fdb0ff7a
4
+ data.tar.gz: 6fe26dd58337aa9e21a535c52bf2d96c582c5fd3c531a6e67cd4f69503211f80
5
5
  SHA512:
6
- metadata.gz: bf21ba6ce88b30cb26cccbcf81b095d5c094e279ea8558a4dd8071fc88c5da39a256a0e080fe48ae2879238f671ae8fbeb33ab7de6526a652a566d598032180e
7
- data.tar.gz: bfc0933ba0facedfe964f1008060331611b6d48762bc769d2b7fb1d957529f30f567fa654f4dd9f7525e6da97203c46fd4e3990bc5b3c00845e455b2aaf67ec1
6
+ metadata.gz: 6b857a6b4a7d337d5d080d399135957323480b6ef0e76f3c89ece5951f064fd314d3b287ea7893e14ed7e91979042a97d205bc432e8e261494f2c488cb86b807
7
+ data.tar.gz: 27c8dfc8ed0ce2208bf0f169795fac5d528757d2c4bed3d019bf33320cffeab67fadaa71760e4cf77df639b2f8f2727f53e59800d17d9ece24a6342fc3b36140
data/README.md CHANGED
@@ -1,11 +1,27 @@
1
1
  # Terminal File Picker
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/terminal-file-picker.svg)](https://badge.fury.io/rb/terminal-file-picker)
4
+
5
+ <img alt="Gif showing usage" src="https://raw.githubusercontent.com/ilkutkutlar/terminal-file-picker/master/usage.gif" width=70%>
6
+
3
7
  This gem shows an interactive terminal file picker to user, allowing them to browse their files with arrow keys and file. The picked file path is then returned to the calling program. The file picker is completely text-based and can be used with a terminal or terminal emulator. It is essentially a terminal version of a GUI file choosing dialogue.
4
8
 
5
9
  The gem is useful when your program needs to accept a file path as input and you want a more user-friendly option than asking user to find out the file path and enter it as text.
6
10
 
7
11
  The gem does not rely on the Curses library and instead uses the [TTY toolkit](https://github.com/piotrmurach/tty), which has cross platform support and support for many types of terminals/terminal emulators. Therefore this gem should also have the same level of support.
8
12
 
13
+ # Installation
14
+
15
+ ```rb
16
+ gem install 'terminal-file-picker'
17
+ ```
18
+
19
+ or add it to your project's `Gemfile`:
20
+
21
+ ```rb
22
+ gem 'terminal-file-picker'
23
+ ```
24
+
9
25
  # Simple Usage
10
26
 
11
27
  ```rb
@@ -23,25 +39,6 @@ picker = FilePicker.new('.')
23
39
  puts(picker.pick_file)
24
40
  ```
25
41
 
26
- The simple file picker looks like this:
27
-
28
- ```
29
- Page: 1/2 | Directory: .
30
-
31
- Name Size (B) Date modified Time modified
32
- ----------------------------------------------------------------------------
33
- . 4096 21/05/2020 00:53
34
- .. 4096 18/05/2020 22:45
35
- terminal-file-picker.rb 113 21/05/2020 00:19
36
- .gitignore 20 17/05/2020 01:04
37
- LICENSE 1068 17/05/2020 22:42
38
- README.md 916 21/05/2020 00:53
39
- .rubocop.yml 497 17/05/2020 19:15
40
- terminal-file-picker.gemspec 581 17/05/2020 22:24
41
- .git/ 4096 21/05/2020 00:51
42
- spec/ 4096 21/05/2020 00:47
43
- ```
44
-
45
42
  # Options
46
43
 
47
44
  There are some options to customise the look and feel of the file picker (all options do have default values, so all are optional)
@@ -1,146 +1 @@
1
- require 'tty-cursor'
2
- require 'tty-reader'
3
- require_relative 'file_browser_view'
4
- require_relative 'helper'
5
-
6
- # Responsible for keeping the state of the interactive file picker.
7
- # Also responds to user input to modify the state and redraw
8
- # file picker to reflect new state.
9
- class FilePicker
10
- def initialize(dir_path, options = {})
11
- @root_path = dir_path
12
- @dir = FileBrowserView.new(options)
13
- @reader = TTY::Reader.new(interrupt: :exit)
14
- @reader.subscribe(self)
15
- @user_have_picked = false
16
- @page = 0
17
- @cursor = TTY::Cursor
18
-
19
- @date_format = options.fetch(:date_format, '%d/%m/%Y')
20
- @time_format = options.fetch(:time_format, '%H:%M')
21
- change_directory(dir_path)
22
- end
23
-
24
- def pick_file
25
- @user_have_picked = false
26
- redraw(false)
27
-
28
- @cursor.invisible do
29
- @reader.read_keypress until @user_have_picked
30
- end
31
-
32
- print(@cursor.clear_screen_down)
33
- full_path_of_selected
34
- end
35
-
36
- def keydown(_event)
37
- @selected += 1 unless selected_at_bottom?
38
- if selected_below_page?
39
- @page += 1
40
- print(@cursor.clear_screen_down)
41
- end
42
- redraw(true)
43
- end
44
-
45
- def keyup(_event)
46
- @selected -= 1 unless selected_at_top?
47
- if selected_above_page?
48
- @page -= 1
49
- print(@cursor.clear_screen_down)
50
- end
51
- redraw(true)
52
- end
53
-
54
- def keypress(event)
55
- case event.value
56
- when "\r"
57
- if File.directory?(full_path_of_selected)
58
- change_directory(full_path_of_selected)
59
- print(@cursor.clear_screen_down)
60
- # Cache keeps a rendering of current directory.
61
- # Going to a new directory, so needs to refresh
62
- # the cache.
63
- redraw(false)
64
- else
65
- @user_have_picked = true
66
- end
67
- end
68
- end
69
-
70
- private
71
-
72
- def redraw(use_cache)
73
- rendered = @dir.render(@current_path, @files, @selected, @page, use_cache)
74
- Helper.print_in_place(rendered)
75
- end
76
-
77
- def selected_at_top?
78
- @selected.zero?
79
- end
80
-
81
- def selected_at_bottom?
82
- @selected == @files.length - 1
83
- end
84
-
85
- def selected_above_page?
86
- @selected < (@page * @dir.files_per_page)
87
- end
88
-
89
- def selected_below_page?
90
- @selected > (@page * @dir.files_per_page) + @dir.files_per_page - 1
91
- end
92
-
93
- def files_in_dir(dir_path)
94
- Dir.entries(dir_path).map do |f|
95
- size_bytes = File.size(full_path(f))
96
-
97
- mtime = File.mtime(full_path(f))
98
- date_mod = mtime.strftime(@date_format)
99
- time_mod = mtime.strftime(@time_format)
100
-
101
- name = file_display_name(f)
102
-
103
- [name, size_bytes, date_mod, time_mod]
104
- end
105
- end
106
-
107
- def change_directory(full_path)
108
- @page = 0
109
- @selected = 0
110
- @current_path = full_path
111
- @files = order_files(files_in_dir(@current_path))
112
- end
113
-
114
- def order_files(files)
115
- # Put "." and ".." at the start
116
- groups = files.group_by do |f|
117
- if f.first == '.' || f.first == '..'
118
- :dots
119
- else
120
- :files
121
- end
122
- end
123
-
124
- # Sort so that "." comes before ".."
125
- (groups[:dots] || []).sort + (groups[:files] || [])
126
- end
127
-
128
- def full_path(file_name)
129
- return @current_path if file_name == '.'
130
- return "#{@current_path}#{file_name}" if @current_path[-1] == '/'
131
-
132
- "#{@current_path}/#{file_name}"
133
- end
134
-
135
- def full_path_of_selected
136
- full_path(@files[@selected].first)
137
- end
138
-
139
- def file_display_name(file_name)
140
- if File.directory?(full_path(file_name))
141
- return "#{file_name}/" unless ['.', '..'].include?(file_name)
142
- end
143
-
144
- file_name
145
- end
146
- end
1
+ require_relative 'terminal-file-picker/file_picker'
@@ -1,6 +1,6 @@
1
- require 'tty-table'
2
1
  require 'tty-screen'
3
2
  require_relative 'helper'
3
+ require_relative 'table'
4
4
 
5
5
  # Renders a directory view with pagination support. Allows
6
6
  # highlighting an item in the directory to indicate its selected.
@@ -9,8 +9,8 @@ class FileBrowserView
9
9
  attr_reader :files_per_page
10
10
 
11
11
  def initialize(options = {})
12
- @header = options.fetch(:header,
13
- ['Name', 'Size (B)', 'Date modified', 'Time modified'])
12
+ default_header = ['Name', 'Size (B)', 'Date modified', 'Time modified']
13
+ @header = options.fetch(:header, default_header)
14
14
  @dir_label = options.fetch(:dir_label, 'Directory')
15
15
  @page_label = options.fetch(:page_label, 'Page')
16
16
  @left_pad = options.fetch(:left_pad, 2)
@@ -18,17 +18,11 @@ class FileBrowserView
18
18
  @files_per_page = options.fetch(:files_per_page, 10)
19
19
  @show_info_line = options.fetch(:show_info_line, true)
20
20
  @info_line_position = options.fetch(:info_line_position, :top)
21
- @screen_width = TTY::Screen.width
22
-
23
21
  @cache = ''
24
22
  end
25
23
 
26
24
  def render(dir_path, files, selected_index, page, use_cache = false)
27
- if !use_cache || (use_cache && @cache.empty?)
28
- @cache = render_files_table(files)
29
- end
30
-
31
- files_table = @cache
25
+ files_table = files_table_string(files, use_cache)
32
26
  file_browser = render_file_browser(files_table, selected_index, page)
33
27
 
34
28
  return file_browser unless @show_info_line
@@ -54,15 +48,15 @@ class FileBrowserView
54
48
  "#{header}\n#{body}"
55
49
  end
56
50
 
57
- def render_files_table(files)
58
- table = TTY::Table.new(@header, files)
51
+ def files_table_string(files, use_cache)
52
+ @table = Table.new(@header, files, @left_pad, @right_pad)
59
53
 
60
- table.render(:basic) do |r|
61
- r.width = @screen_width
62
- r.resize = table_width_overflowed?(table.width)
63
- table_padding(r)
64
- table_border(r)
65
- end
54
+ @cache = @table.render if !use_cache || (use_cache && @cache.empty?)
55
+ return @cache unless table_width_overflowed?
56
+
57
+ @cache.split("\n").map do |line|
58
+ line[0..(screen_width - 1)]
59
+ end.join("\n")
66
60
  end
67
61
 
68
62
  def highlight_row(rows, row_no)
@@ -73,7 +67,12 @@ class FileBrowserView
73
67
 
74
68
  def info_bar(total_files, page, dir_path)
75
69
  page_count = (total_files.to_f / @files_per_page).ceil
76
- "#{@page_label}: #{page + 1}/#{page_count} | #{@dir_label}: #{dir_path}"
70
+ page_info = "#{@page_label}: #{page + 1}/#{page_count}"
71
+ dir_info = "#{@dir_label}: #{dir_path}"
72
+ info_bar = "#{page_info} | #{dir_info}"
73
+ return info_bar unless info_bar.length > screen_width
74
+
75
+ info_bar[0..screen_width - 1]
77
76
  end
78
77
 
79
78
  def paginate_table_body(body_rows, page_no)
@@ -83,21 +82,11 @@ class FileBrowserView
83
82
  body_rows[page_start..page_end]
84
83
  end
85
84
 
86
- def table_width_overflowed?(table_content_width)
87
- table_padding = @header.length * (@left_pad + @right_pad) + 1
88
- full_table_length = table_content_width + table_padding
89
-
90
- full_table_length >= @screen_width
91
- end
92
-
93
- def table_padding(renderer)
94
- renderer.padding = [0, @right_pad, 0, @left_pad]
85
+ def table_width_overflowed?
86
+ @table.total_row_size >= screen_width
95
87
  end
96
88
 
97
- def table_border(renderer)
98
- renderer.border do
99
- center ''
100
- mid '-'
101
- end
89
+ def screen_width
90
+ TTY::Screen.width
102
91
  end
103
92
  end
@@ -0,0 +1,146 @@
1
+ require 'tty-cursor'
2
+ require 'tty-reader'
3
+ require_relative 'file_browser_view'
4
+ require_relative 'helper'
5
+
6
+ # Responsible for keeping the state of the interactive file picker.
7
+ # Also responds to user input to modify the state and redraw
8
+ # file picker to reflect new state.
9
+ class FilePicker
10
+ def initialize(dir_path, options = {})
11
+ @root_path = dir_path
12
+ @dir = FileBrowserView.new(options)
13
+ @reader = TTY::Reader.new(interrupt: :exit)
14
+ @reader.subscribe(self)
15
+ @user_have_picked = false
16
+ @page = 0
17
+ @cursor = TTY::Cursor
18
+
19
+ @date_format = options.fetch(:date_format, '%d/%m/%Y')
20
+ @time_format = options.fetch(:time_format, '%H:%M')
21
+ change_directory(dir_path)
22
+ end
23
+
24
+ def pick_file
25
+ @user_have_picked = false
26
+ redraw(false)
27
+
28
+ @cursor.invisible do
29
+ @reader.read_keypress until @user_have_picked
30
+ end
31
+
32
+ print(@cursor.clear_screen_down)
33
+ full_path_of_selected
34
+ end
35
+
36
+ def keydown(_event)
37
+ @selected += 1 unless selected_at_bottom?
38
+ if selected_below_page?
39
+ @page += 1
40
+ print(@cursor.clear_screen_down)
41
+ end
42
+ redraw(true)
43
+ end
44
+
45
+ def keyup(_event)
46
+ @selected -= 1 unless selected_at_top?
47
+ if selected_above_page?
48
+ @page -= 1
49
+ print(@cursor.clear_screen_down)
50
+ end
51
+ redraw(true)
52
+ end
53
+
54
+ def keypress(event)
55
+ case event.value
56
+ when "\r"
57
+ if File.directory?(full_path_of_selected)
58
+ change_directory(full_path_of_selected)
59
+ print(@cursor.clear_screen_down)
60
+ # Cache keeps a rendering of current directory.
61
+ # Going to a new directory, so needs to refresh
62
+ # the cache.
63
+ redraw(false)
64
+ else
65
+ @user_have_picked = true
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def redraw(use_cache)
73
+ rendered = @dir.render(@current_path, @files, @selected, @page, use_cache)
74
+ Helper.print_in_place(rendered)
75
+ end
76
+
77
+ def selected_at_top?
78
+ @selected.zero?
79
+ end
80
+
81
+ def selected_at_bottom?
82
+ @selected == @files.length - 1
83
+ end
84
+
85
+ def selected_above_page?
86
+ @selected < (@page * @dir.files_per_page)
87
+ end
88
+
89
+ def selected_below_page?
90
+ @selected > (@page * @dir.files_per_page) + @dir.files_per_page - 1
91
+ end
92
+
93
+ def files_in_dir(dir_path)
94
+ Dir.entries(dir_path).map do |f|
95
+ size_bytes = File.size(full_path(f))
96
+
97
+ mtime = File.mtime(full_path(f))
98
+ date_mod = mtime.strftime(@date_format)
99
+ time_mod = mtime.strftime(@time_format)
100
+
101
+ name = file_display_name(f)
102
+
103
+ [name, size_bytes, date_mod, time_mod]
104
+ end
105
+ end
106
+
107
+ def change_directory(full_path)
108
+ @page = 0
109
+ @selected = 0
110
+ @current_path = full_path
111
+ @files = order_files(files_in_dir(@current_path))
112
+ end
113
+
114
+ def order_files(files)
115
+ # Put "." and ".." at the start
116
+ groups = files.group_by do |f|
117
+ if f.first == '.' || f.first == '..'
118
+ :dots
119
+ else
120
+ :files
121
+ end
122
+ end
123
+
124
+ # Sort so that "." comes before ".."
125
+ (groups[:dots] || []).sort + (groups[:files] || [])
126
+ end
127
+
128
+ def full_path(file_name)
129
+ return @current_path if file_name == '.'
130
+ return "#{@current_path}#{file_name}" if @current_path[-1] == '/'
131
+
132
+ "#{@current_path}/#{file_name}"
133
+ end
134
+
135
+ def full_path_of_selected
136
+ full_path(@files[@selected].first)
137
+ end
138
+
139
+ def file_display_name(file_name)
140
+ if File.directory?(full_path(file_name))
141
+ return "#{file_name}/" unless ['.', '..'].include?(file_name)
142
+ end
143
+
144
+ file_name
145
+ end
146
+ end
@@ -0,0 +1,48 @@
1
+ # Styles given data to make it suitable for printing on a screen and
2
+ # look like justified columns of a table.
3
+ class Table
4
+ def initialize(header, data, left_pad, right_pad)
5
+ @header = header
6
+ @data = data
7
+ @left_pad = left_pad
8
+ @right_pad = right_pad
9
+ @col_sizes = table_column_sizes
10
+ end
11
+
12
+ def render
13
+ rendered_header = render_row(@header)
14
+ rendered_data = @data.map { |row| render_row(row) }.join("\n")
15
+ border = '-' * total_row_size
16
+ [rendered_header, border, rendered_data].join("\n")
17
+ end
18
+
19
+ def total_row_size
20
+ total_padding_size = (@left_pad + @right_pad) * @header.length
21
+ @col_sizes.sum + total_padding_size
22
+ end
23
+
24
+ private
25
+
26
+ def render_row(row)
27
+ left_pad_str = ' ' * @left_pad
28
+ right_pad_str = ' ' * @right_pad
29
+
30
+ justified_cols = row.zip(@col_sizes).map do |col, col_size|
31
+ col.to_s.ljust(col_size)
32
+ end
33
+
34
+ justified_cols_str = justified_cols.join("#{left_pad_str}#{right_pad_str}")
35
+ "#{left_pad_str}#{justified_cols_str}#{right_pad_str}"
36
+ end
37
+
38
+ def table_column_sizes
39
+ table_data = [@header] + @data
40
+ col_count = table_data.first.length
41
+
42
+ (0..col_count - 1).map do |col|
43
+ table_data.map do |row|
44
+ row[col].to_s.length
45
+ end.max
46
+ end
47
+ end
48
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terminal-file-picker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ilkut Kutlar
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-23 00:00:00.000000000 Z
11
+ date: 2020-06-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pastel
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.7.1
69
- - !ruby/object:Gem::Dependency
70
- name: tty-table
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: 0.11.0
76
- type: :runtime
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: 0.11.0
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: rspec
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -118,9 +104,11 @@ extensions: []
118
104
  extra_rdoc_files: []
119
105
  files:
120
106
  - README.md
121
- - lib/file_browser_view.rb
122
- - lib/helper.rb
123
107
  - lib/terminal-file-picker.rb
108
+ - lib/terminal-file-picker/file_browser_view.rb
109
+ - lib/terminal-file-picker/file_picker.rb
110
+ - lib/terminal-file-picker/helper.rb
111
+ - lib/terminal-file-picker/table.rb
124
112
  homepage: https://github.com/ilkutkutlar/terminal-file-picker
125
113
  licenses:
126
114
  - MIT
@@ -133,7 +121,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
133
121
  requirements:
134
122
  - - ">="
135
123
  - !ruby/object:Gem::Version
136
- version: '0'
124
+ version: 2.0.0
137
125
  required_rubygems_version: !ruby/object:Gem::Requirement
138
126
  requirements:
139
127
  - - ">="