terminal-file-picker 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 36bd9443b474cff7cc402fb518e1a42f8f098a871da283384d74b5fb7cf2510a
4
+ data.tar.gz: b82f7ce18c958d285d9b55b5898e23c821bf6b49dfc2fe99984a88956cd1277d
5
+ SHA512:
6
+ metadata.gz: bf21ba6ce88b30cb26cccbcf81b095d5c094e279ea8558a4dd8071fc88c5da39a256a0e080fe48ae2879238f671ae8fbeb33ab7de6526a652a566d598032180e
7
+ data.tar.gz: bfc0933ba0facedfe964f1008060331611b6d48762bc769d2b7fb1d957529f30f567fa654f4dd9f7525e6da97203c46fd4e3990bc5b3c00845e455b2aaf67ec1
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Terminal File Picker
2
+
3
+ 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
+
5
+ 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
+
7
+ 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
+
9
+ # Simple Usage
10
+
11
+ ```rb
12
+ # Only required argument is the "root path". The user starts
13
+ # navigating from the root path and the returned path of
14
+ # the chosen file is relative to this root path.
15
+ picker = FilePicker.new('.')
16
+
17
+ # This brings up the interactive file picker. Once the user
18
+ # has picked a file, the picker is cleared from screen and
19
+ # the chosen file path returned.
20
+
21
+ # If user picks a directory, instead of returning its path,
22
+ # the files inside the chosen directory is shown instead.
23
+ puts(picker.pick_file)
24
+ ```
25
+
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
+ # Options
46
+
47
+ There are some options to customise the look and feel of the file picker (all options do have default values, so all are optional)
48
+
49
+ The table headers can be changed (e.g. for internationalisation of if you want shorter headers).
50
+ ```rb
51
+ FilePicker.new('.', header: ["Nm.", "Sz.", "Date md.", "Time md."])
52
+ ```
53
+
54
+ You can change the current directory label which by default says "Directory" in the info line.
55
+
56
+ ```rb
57
+ FilePicker.new('.', dir_label: 'Dir')
58
+ ```
59
+
60
+ You can change current page label which by default says "Page" in the info line.
61
+
62
+ ```rb
63
+ FilePicker.new('.', page_label: 'Pg')
64
+ ```
65
+
66
+ You can change paddings of the table (in terms of number of spaces)
67
+
68
+ ```rb
69
+ FilePicker.new('.', left_pad: 1, right_pad: 1)
70
+ ```
71
+
72
+ The file picker automatically paginates files. You can set how many files there should be in each page (10 by default)
73
+
74
+ ```rb
75
+ FilePicker.new('.', files_per_page: 20)
76
+ ```
77
+
78
+ You can choose to hide the info line (the line which shows the current directory and current page)
79
+
80
+ ```rb
81
+ FilePicker.new('.', show_info_line: false)
82
+ ```
83
+
84
+ You can also change the position of the info line (valid values are :top or :bottom. :top is the default)
85
+
86
+ ```rb
87
+ FilePicker.new('.', info_line_position: :bottom)
88
+ ```
89
+
90
+ You can change the date and time format of date modified and time modified columns. The format can be specified using the same format accepted by the built-in `strftime` function.
91
+
92
+ ```rb
93
+ FilePicker.new('.', date_format: '%d-%m-%Y', time_format: '%H.%m')
94
+ ```
@@ -0,0 +1,103 @@
1
+ require 'tty-table'
2
+ require 'tty-screen'
3
+ require_relative 'helper'
4
+
5
+ # Renders a directory view with pagination support. Allows
6
+ # highlighting an item in the directory to indicate its selected.
7
+ # Instance stores directory view rendering options.
8
+ class FileBrowserView
9
+ attr_reader :files_per_page
10
+
11
+ def initialize(options = {})
12
+ @header = options.fetch(:header,
13
+ ['Name', 'Size (B)', 'Date modified', 'Time modified'])
14
+ @dir_label = options.fetch(:dir_label, 'Directory')
15
+ @page_label = options.fetch(:page_label, 'Page')
16
+ @left_pad = options.fetch(:left_pad, 2)
17
+ @right_pad = options.fetch(:right_pad, 2)
18
+ @files_per_page = options.fetch(:files_per_page, 10)
19
+ @show_info_line = options.fetch(:show_info_line, true)
20
+ @info_line_position = options.fetch(:info_line_position, :top)
21
+ @screen_width = TTY::Screen.width
22
+
23
+ @cache = ''
24
+ end
25
+
26
+ 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
32
+ file_browser = render_file_browser(files_table, selected_index, page)
33
+
34
+ return file_browser unless @show_info_line
35
+
36
+ info_line = info_bar(files.length, page, dir_path)
37
+ return "#{info_line}\n\n#{file_browser}" if @info_line_position == :top
38
+
39
+ "#{file_browser}\n\n#{info_line}"
40
+ end
41
+
42
+ private
43
+
44
+ def render_file_browser(rendered_files_table, selected_index, page)
45
+ rendered_rows = rendered_files_table.split("\n")
46
+
47
+ header = rendered_rows[0..1].join("\n")
48
+
49
+ body_rows = rendered_rows[2..]
50
+ body_rows = highlight_row(body_rows, selected_index)
51
+ body_rows = paginate_table_body(body_rows, page)
52
+ body = body_rows.join("\n")
53
+
54
+ "#{header}\n#{body}"
55
+ end
56
+
57
+ def render_files_table(files)
58
+ table = TTY::Table.new(@header, files)
59
+
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
66
+ end
67
+
68
+ def highlight_row(rows, row_no)
69
+ rows[row_no] = Helper.highlight(rows[row_no])
70
+
71
+ rows
72
+ end
73
+
74
+ def info_bar(total_files, page, dir_path)
75
+ page_count = (total_files.to_f / @files_per_page).ceil
76
+ "#{@page_label}: #{page + 1}/#{page_count} | #{@dir_label}: #{dir_path}"
77
+ end
78
+
79
+ def paginate_table_body(body_rows, page_no)
80
+ page_start = page_no * @files_per_page
81
+ page_end = page_start + (@files_per_page - 1)
82
+
83
+ body_rows[page_start..page_end]
84
+ end
85
+
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]
95
+ end
96
+
97
+ def table_border(renderer)
98
+ renderer.border do
99
+ center ''
100
+ mid '-'
101
+ end
102
+ end
103
+ end
data/lib/helper.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'pastel'
2
+ require 'tty-cursor'
3
+
4
+ # Misc. helper methods which accomplish some common tasks.
5
+ module Helper
6
+ class << self
7
+ def print_in_place(text)
8
+ line_count = text.count("\n")
9
+ cursor = TTY::Cursor
10
+
11
+ in_place = cursor.column(1)
12
+ in_place << text
13
+ in_place << cursor.up(line_count)
14
+ in_place << cursor.column(1)
15
+
16
+ print(in_place)
17
+ end
18
+
19
+ def highlight(text)
20
+ Pastel.new.decorate(text, :inverse)
21
+ end
22
+ end
23
+ 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
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: terminal-file-picker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ilkut Kutlar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pastel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.7.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.7.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: tty-cursor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.7.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.7.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-reader
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.7.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.7.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: tty-screen
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.7.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
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
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.9'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.9'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.82.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.82.0
111
+ description: This gem shows an interactive terminal file picker to user, allowing
112
+ them to browse their files with arrow keys. The picked file path is then returned
113
+ to the calling program.
114
+ email:
115
+ - ilkutkutlar@gmail.com
116
+ executables: []
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - README.md
121
+ - lib/file_browser_view.rb
122
+ - lib/helper.rb
123
+ - lib/terminal-file-picker.rb
124
+ homepage: https://github.com/ilkutkutlar/terminal-file-picker
125
+ licenses:
126
+ - MIT
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubygems_version: 3.0.3
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: Interactive terminal file picker
147
+ test_files: []