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 +7 -0
- data/README.md +94 -0
- data/lib/file_browser_view.rb +103 -0
- data/lib/helper.rb +23 -0
- data/lib/terminal-file-picker.rb +146 -0
- metadata +147 -0
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: []
|