fstui 0.5.5

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: c70d3bdbca2796d540ec5518bccfcdcb01cea95b4ccb844780affe7aaf38ddf7
4
+ data.tar.gz: 97c8de10fa9948830b068c801b4fc580610e694f33517c588f121f5e72474897
5
+ SHA512:
6
+ metadata.gz: 0efb84d357330c920760c40cfd66f9ddafcb893ee20098ffa8e255ac1cb5f98cf47267e59b05bd9aa9c2736e92f441c772ad47e0d7af7e03d63f20faa848246f
7
+ data.tar.gz: 84c669222f21fc903d2c279824ae3ca1f67131be94ddfdff6b8f39459d573372ed72a0e74fb74e1f8789de23dfc779b074a11980b5dbf53dd2aa54dcca0068b8
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # ruby-fstui
2
+ Full Screen Text UI library for Ruby
@@ -0,0 +1,39 @@
1
+ module FSTui
2
+ class DataScreen
3
+ attr_accessor :title, :subtitle, :data
4
+
5
+ def initialize(screen, title, subtitle, data)
6
+ @screen = screen
7
+ @title = title
8
+ @subtitle = subtitle
9
+ @data = data
10
+ end
11
+
12
+ def get_response(options: nil)
13
+ loop do
14
+ @win = Window.new(@data.lines.length+3, stdscr.maxx, 5, 0)
15
+ @win.addstr @title.center(@win.maxx)
16
+ @win.addstr @subtitle.center(@win.maxx) + "\n"
17
+
18
+ # figure out which line is the longest and center it
19
+ longest_line = 0
20
+ @data.each_line do |line|
21
+ longest_line = line.length if line.length > longest_line
22
+ end
23
+ padding = " " * (@win.maxx / 2 - longest_line / 2)
24
+
25
+ # draw the data block, with the longest line centered and all others left justified with it
26
+ @data.each_line do |line|
27
+ @win.addstr "#{padding}#{line.strip}\n"
28
+ end
29
+
30
+ @win.noutrefresh
31
+ selection = @screen.inputbar.get_selection(options: options)
32
+ @win.clear
33
+ @win.noutrefresh
34
+ @win.close
35
+ return selection
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,195 @@
1
+ module FSTui
2
+ class FieldScreen
3
+ attr_accessor :title, :subtitle, :fields
4
+
5
+ def initialize(screen, title, subtitle, fields)
6
+ @screen = screen
7
+ @title = title
8
+ @subtitle = subtitle
9
+ @fields = fields
10
+ @field_rows = []
11
+ end
12
+
13
+ def clear_fields
14
+ @fields.each do |field|
15
+ field.value = nil
16
+ end
17
+ end
18
+
19
+ def get_response(options: nil)
20
+ loop do
21
+ @win = Window.new(stdscr.maxy-4, stdscr.maxx, 2, 0)
22
+ @win.addstr @title.center(@win.maxx)
23
+ @win.addstr @subtitle.center(@win.maxx) + "\n"
24
+
25
+ # draw the fields on the form
26
+ @fields.each_index do |idx|
27
+ field = @fields[idx]
28
+ # keep track of which row each field starts on, for later edits
29
+ @field_rows[idx] = @win.cury
30
+ @win.addstr " #{idx+1} - #{field.display_name}: "
31
+ @win.attron Curses::A_UNDERLINE
32
+ @win.addstr "#{field.value}"
33
+ @win.attroff Curses::A_UNDERLINE
34
+ @win.addstr "\n\n"
35
+ end
36
+
37
+ @screen.inputbar.instructions = "#=Enter Data ENTER=Submit Form A=Abort"
38
+ @win.noutrefresh
39
+ selection = @screen.inputbar.get_selection(options: options)
40
+ case selection
41
+ when "1", "2", "3", "4", "5", "6", "7", "8", "9"
42
+ edit_field selection.to_i-1
43
+ when :enter
44
+ # check that all required fields have data
45
+ field_errors = 0
46
+ @fields.each_index do |idx|
47
+ field = @fields[idx]
48
+ next unless field.is_required?
49
+ next unless field.value == "" or field.value == nil
50
+ field_row = 3+idx*2
51
+
52
+ @win.setpos field_row, 0
53
+ @win.addstr " #{idx+1} X #{field.display_name}: "
54
+ @win.standout
55
+ @win.addstr " "
56
+ @win.standend
57
+ @win.clrtoeol
58
+ @win.setpos field_row, 8+field.display_name.length
59
+ field_errors += 1
60
+ end
61
+
62
+ if(field_errors == 0) then
63
+ # everything looks good, end here and return the collected data
64
+ @win.clear
65
+ @win.noutrefresh
66
+ @win.close
67
+ @screen.inputbar.instructions = ""
68
+ return @fields
69
+ else
70
+ # TODO: consider blink or something, this isn't very obvious
71
+ @screen.inputbar.instructions = "Missing data in required field!"
72
+ @win.noutrefresh
73
+ @screen.redraw
74
+ doupdate
75
+ sleep 3
76
+ end
77
+ when 'A', 'a'
78
+ @win.clear
79
+ @win.noutrefresh
80
+ @win.close
81
+ @screen.inputbar.instructions = ""
82
+ return nil
83
+ end
84
+ end
85
+ end
86
+
87
+ def edit_field(field_nbr)
88
+ field = @fields[field_nbr]
89
+ value = ""
90
+ value = field.value if field.value
91
+ insert_pos = value.length
92
+ @win.setpos @field_rows[field_nbr], 0
93
+ @win.addstr " #{field_nbr+1} - #{field.display_name}: "
94
+ @win.standout
95
+ # TODO: fix the hardcoded 33-long fields :)
96
+ @win.addstr value.ljust(33)
97
+ @win.clrtoeol
98
+ @win.setpos @field_rows[field_nbr], 8+field.display_name.length + value.length
99
+ noecho
100
+ @win.refresh
101
+ @win.keypad(true)
102
+ loop do
103
+ char = @win.getch
104
+ case char
105
+ when 10, 13, "\n", "\r"
106
+ break
107
+ when 8, KEY_BACKSPACE, "\b", 127
108
+ next unless insert_pos > 0
109
+ row = @win.cury
110
+ col = @win.curx
111
+ if(insert_pos == 1) then
112
+ value = ""
113
+ insert_pos = 0
114
+ else
115
+ value = value[0..insert_pos-2] + value[insert_pos..-1]
116
+ insert_pos -= 1
117
+ end
118
+ if(col==0) then
119
+ @win.setpos(row-1, @win.maxx-1)
120
+ @win.addstr value[insert_pos..-1] + ' '
121
+ @win.setpos(row-1, @win.maxx-1)
122
+ else
123
+ @win.setpos(row, col-1)
124
+ @win.addstr value[insert_pos..-1] + ' '
125
+ @win.setpos(row, col-1)
126
+ end
127
+ when KEY_DC # delete
128
+ next unless insert_pos < value.length
129
+ value = value[0..insert_pos-1] + value[insert_pos+1..-1]
130
+ row = @win.cury
131
+ col = @win.curx
132
+ @win.addstr value[insert_pos..-1] + ' '
133
+ @win.setpos(row, col)
134
+ when KEY_LEFT
135
+ next unless insert_pos > 0
136
+ row = @win.cury
137
+ col = @win.curx
138
+ if(col == 0) then
139
+ @win.setpos(row-1, @win.maxx-1)
140
+ else
141
+ @win.setpos(row, col-1)
142
+ end
143
+ insert_pos -= 1
144
+ when KEY_RIGHT
145
+ next unless insert_pos < value.length
146
+ row = @win.cury
147
+ col = @win.curx
148
+ if(col == @win.maxx-1) then
149
+ @win.setpos(row+1, 0)
150
+ else
151
+ @win.setpos(row, col+1)
152
+ end
153
+ insert_pos += 1
154
+ when KEY_DOWN, KEY_UP
155
+ else
156
+ value.insert(insert_pos, char)
157
+ insert_pos += 1
158
+ if(insert_pos == value.length) then
159
+ # adding chars at the end, easy
160
+ @win.addch char
161
+ else
162
+ # adding chars in the middle, need to redraw the rest of the field
163
+ row = @win.cury
164
+ col = @win.curx
165
+ @win.addstr value[insert_pos-1..-1]
166
+ @win.setpos(row, col+1)
167
+ end
168
+ end
169
+ end
170
+ @win.keypad(false)
171
+ echo
172
+ @win.standend
173
+
174
+ @fields[field_nbr].value = value
175
+ end
176
+ end
177
+
178
+ class Field
179
+ attr_accessor :name, :required, :value
180
+ def initialize(name:, required: true)
181
+ @name = name
182
+ @required = required
183
+ end
184
+
185
+ def display_name
186
+ disp_name = @name
187
+ disp_name += " (opt)" unless @required
188
+ disp_name
189
+ end
190
+
191
+ def is_required?
192
+ @required
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,118 @@
1
+ module FSTui
2
+ class InfoList
3
+ attr_accessor :infoline, :left_heading, :right_heading
4
+
5
+ def initialize(screen, infoline, lines_per_item)
6
+ @infoline = infoline
7
+ @lines_per_item = lines_per_item
8
+ @items = Array.new
9
+ @screen = screen
10
+ end
11
+
12
+ def add_item(item)
13
+ @items.push item
14
+ end
15
+
16
+ def <<(item)
17
+ @items.push item
18
+ end
19
+
20
+ def clear_items
21
+ @items = []
22
+ end
23
+
24
+ def get_response(extra_options: [])
25
+ page = 1
26
+ loop do
27
+ @win = Window.new(stdscr.maxy - 5, stdscr.maxx, 2, 0)
28
+ @win.addstr @infoline
29
+ @win.setpos 2, 0
30
+ @win.attron A_REVERSE
31
+ @win.addstr " "*5 + @left_heading + " " * (stdscr.maxx - 5 - 5 - @left_heading.length - @right_heading.length) + @right_heading + " "*5
32
+ @win.attroff A_REVERSE
33
+
34
+ items_per_page = (@win.maxy - 6) / (@lines_per_item + 1)
35
+ total_pages = @items.length / items_per_page
36
+ start_item = items_per_page * (page-1)
37
+
38
+ idx = start_item + 1
39
+ @items[start_item..start_item + items_per_page].each do |item|
40
+ # figure out which line has the longest right-hand side so we know how much to trim the left so it doesn't interfere
41
+ longest_rhs = 0
42
+ item.rows.each do |row|
43
+ longest_rhs = row.rhs.length if row.rhs.length > longest_rhs
44
+ end
45
+ lhs = truncate(item.rows[0].lhs, stdscr.maxx - 6 - longest_rhs - 3)
46
+ @win.addstr " #{idx}. #{lhs}" + " "*(stdscr.maxx - 5 - 1 - idx.to_s.length - lhs.length - item.rows[0].rhs.length) + "#{item.rows[0].rhs}\n"
47
+ if(item.rows.length > 1)
48
+ item.rows[1..-1].each do |row|
49
+ lhs = truncate(row.lhs, stdscr.maxx - 9 - longest_rhs - 3)
50
+ @win.addstr " #{lhs}" + " "*(stdscr.maxx - 7 - 2 - lhs.length - row.rhs.length) + "#{row.rhs}\n"
51
+ end
52
+ end
53
+ @win.addstr "\n"
54
+ idx += 1
55
+ end
56
+ morepages = page < total_pages ? "More on Next Screen" : "End of List"
57
+ @win.addstr "\n** #{@items.length} Items UNSORTED - Page #{page} of #{total_pages} - #{morepages} **"
58
+
59
+ @win.noutrefresh
60
+ old_instructions = @screen.inputbar.instructions
61
+ instructions = []
62
+ extra_options.each do |key, desc|
63
+ instructions << "#{key}=#{desc}"
64
+ end
65
+ instructions.push "B=Back" if page > 1
66
+ instructions.push "<Enter>=Next Screen" if page < total_pages
67
+ @screen.inputbar.instructions = instructions.join(", ")
68
+ selection = @screen.inputbar.get_string
69
+ @screen.inputbar.instructions = old_instructions
70
+ @win.clear
71
+ @win.noutrefresh
72
+ @win.close
73
+
74
+ if(selection == "" and page < total_pages)
75
+ page += 1
76
+ elsif(selection == "b" or selection == "B" and page > 1)
77
+ page -= 1
78
+ elsif(extra_options.keys.include?(selection.upcase) or extra_options.keys.include?(selection.downcase))
79
+ return selection
80
+ else
81
+ return @items[selection.to_i - 1].key if selection.to_i > 0 and selection.to_i < @items.length + 1
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def truncate(str, len)
89
+ return str if str.length <= len
90
+ "#{str[0..len-3]}..."
91
+ end
92
+
93
+ end
94
+ class InfoItem
95
+ attr_accessor :key, :rows
96
+
97
+ def initialize(key:, rows:)
98
+ @key = key
99
+ @rows = rows
100
+ end
101
+ end
102
+
103
+ class InfoRow
104
+ attr_accessor :lhs, :rhs
105
+
106
+ def initialize(lhs:, rhs:)
107
+ @lhs = lhs
108
+ @rhs = rhs
109
+ end
110
+
111
+ def [](idx)
112
+ return @lhs if idx == 0
113
+ return @rhs if idx == 1
114
+
115
+ nil
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,22 @@
1
+ module FSTui
2
+ class InputBox
3
+ attr_accessor :title
4
+ def initialize(screen, title, subtitle)
5
+ @title = title
6
+ @subtitle = subtitle
7
+ @screen = screen
8
+ end
9
+
10
+ def get_entry
11
+ @win = Window.new(5, stdscr.maxx, stdscr.maxy / 2 - 3, 0)
12
+ @win.addstr @title.center(@win.maxx)
13
+ @win.addstr @subtitle.center(@win.maxx) + "\n"
14
+
15
+ @win.refresh
16
+ @screen.redraw
17
+ selection = @screen.inputbar.get_string
18
+ @win.close
19
+ selection
20
+ end
21
+ end
22
+ end
data/lib/fstui/menu.rb ADDED
@@ -0,0 +1,53 @@
1
+ module FSTui
2
+ class Menu
3
+ attr_accessor :title
4
+ def initialize(screen, title, subtitle)
5
+ @title = title
6
+ @subtitle = subtitle
7
+ @items = Array.new
8
+ @screen = screen
9
+ end
10
+
11
+ def add_item(key, desc)
12
+ @items.push MenuItem.new(key, desc)
13
+ end
14
+
15
+ def get_response
16
+ loop do
17
+ @win = Window.new(@items.length + 4, stdscr.maxx, 5, 0)
18
+ @win.clear
19
+ @win.addstr @title.center(@win.maxx)
20
+ @win.addstr @subtitle.center(@win.maxx) + "\n"
21
+
22
+ # figure out which item is the longest and center it
23
+ longest_item = 0
24
+ @items.each do |item|
25
+ longest_item = item.desc.length if item.desc.length > longest_item
26
+ end
27
+ padding = " " * (@win.maxx / 2 - longest_item / 2)
28
+
29
+ # draw the menu, with the longest item centered and all others left justified with it
30
+ @items.each_index do |idx|
31
+ menu_text = "#{padding}#{idx + 1}. #{@items[idx].desc}\n"
32
+ @win.addstr menu_text
33
+ end
34
+
35
+ @win.noutrefresh
36
+ selection = @screen.inputbar.get_selection
37
+ @win.clear
38
+ @win.noutrefresh
39
+ @win.close
40
+ return @items[selection.to_i - 1].key if(("0".."9").include?(selection) and selection.to_i <= @items.length)
41
+ end
42
+ end
43
+
44
+ class MenuItem
45
+ attr_accessor :key, :desc
46
+
47
+ def initialize(key, desc)
48
+ @key = key
49
+ @desc = desc
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ module FSTui
2
+ class MessageBox
3
+ attr_accessor :title
4
+ def initialize(screen, title, subtitle)
5
+ @title = title
6
+ @subtitle = subtitle
7
+ @screen = screen
8
+ end
9
+
10
+ def show
11
+ @win = Window.new(5, stdscr.maxx, stdscr.maxy / 2 - 3, 0)
12
+ @win.addstr @title.center(@win.maxx)
13
+ @win.addstr @subtitle.center(@win.maxx) + "\n"
14
+ @win.refresh
15
+ @screen.inputbar.hide
16
+ end
17
+
18
+ def hide
19
+ @win.clear
20
+ @win.refresh
21
+ @win.close
22
+ @screen.redraw
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,153 @@
1
+ module FSTui
2
+ class Screen
3
+ attr_accessor :titlebar, :inputbar
4
+
5
+ def initialize
6
+ init_screen
7
+ start_color
8
+ crmode
9
+
10
+ @titlebar = TitleBar.new(self)
11
+ @inputbar = InputBar.new(self)
12
+ end
13
+
14
+ def destroy
15
+ close_screen
16
+ end
17
+
18
+ def redraw
19
+ @titlebar.redraw
20
+ @inputbar.redraw
21
+ end
22
+
23
+ def new_menu(title, subtitle = "Please select one of the options below.")
24
+ Menu.new(self, title, subtitle)
25
+ end
26
+
27
+ def new_inputbox(title, subtitle = "Please enter your selection below.")
28
+ InputBox.new(self, title, subtitle)
29
+ end
30
+
31
+ def new_messagebox(title, subtitle = "")
32
+ MessageBox.new(self, title, subtitle)
33
+ end
34
+
35
+ def pop_messagebox(title, subtitle = "", timeout = 3)
36
+ msg = MessageBox.new(self, title, subtitle)
37
+ msg.show
38
+ sleep timeout
39
+ msg.hide
40
+ end
41
+
42
+ def new_infolist(infoline, lines_per_item)
43
+ InfoList.new(self, infoline, lines_per_item)
44
+ end
45
+
46
+ def new_datascreen(title, subtitle = "", data)
47
+ DataScreen.new(self, title, subtitle, data)
48
+ end
49
+
50
+ def new_fieldscreen(title, subtitle = "", fields)
51
+ FieldScreen.new(self, title, subtitle, fields)
52
+ end
53
+ end
54
+
55
+ class TitleBar
56
+ def initialize(screen, pad_chars = 2)
57
+ @win = Window.new(1, stdscr.maxx, 0, 0)
58
+ @win.attrset A_REVERSE
59
+ @win.noutrefresh
60
+ @left = @center = @right = ""
61
+ @pad_chars = pad_chars
62
+ redraw
63
+ end
64
+
65
+ def center=(center)
66
+ @center = center
67
+ redraw
68
+ end
69
+
70
+ def left=(left)
71
+ @left = left
72
+ redraw
73
+ end
74
+
75
+ def right=(right)
76
+ @right = right
77
+ redraw
78
+ end
79
+
80
+ def redraw
81
+ @win.clear
82
+ @win.addstr (" " * @pad_chars) + @left + @center.center(@win.maxx - (@left.length + @pad_chars) - (@right.length + @pad_chars)) + @right + (" " * @pad_chars)
83
+ @win.noutrefresh
84
+ end
85
+ end
86
+
87
+ class InputBar
88
+ attr_accessor :prompt, :instructions
89
+
90
+ def initialize(screen, prompt = " Enter your selection(s) and press <Enter> : ", instructions = "")
91
+ @prompt = prompt
92
+ @instructions = instructions
93
+ window_setup
94
+ end
95
+
96
+ def window_setup
97
+ # TODO: this could use a lot of simplification
98
+ @promptwin = Window.new(1, @prompt.length, stdscr.maxy - 2, 0)
99
+ @promptwin.attrset A_REVERSE
100
+ @entrywin = Window.new(1, stdscr.maxx - @prompt.length, stdscr.maxy - 2, @prompt.length + 1)
101
+ @instructionwin = Window.new(1, stdscr.maxx, stdscr.maxy-1, 0)
102
+ end
103
+
104
+ def prompt=(prompt)
105
+ @prompt = prompt
106
+ destroy
107
+ window_setup
108
+ end
109
+
110
+ def destroy
111
+ @promptwin.close
112
+ @entrywin.close
113
+ end
114
+
115
+ def redraw
116
+ @promptwin.clear
117
+ @promptwin.addstr @prompt
118
+ @promptwin.noutrefresh
119
+ @instructionwin.clear
120
+ @instructionwin.addstr @instructions
121
+ @instructionwin.noutrefresh
122
+ end
123
+
124
+ def hide
125
+ @promptwin.clear
126
+ @promptwin.noutrefresh
127
+ end
128
+
129
+ def get_selection(options: nil)
130
+ while(true) do
131
+ @entrywin.setpos 0, 0
132
+ redraw
133
+ # getch will doupdate() for us as well
134
+ entry = @entrywin.getch
135
+ @entrywin.clear
136
+ @entrywin.noutrefresh
137
+ entry = :enter if entry == 10 or entry == 13
138
+ break if options == nil or options.include? entry
139
+ end
140
+ entry
141
+ end
142
+
143
+ def get_string
144
+ @entrywin.setpos 0, 0
145
+ redraw
146
+ # getstr will doupdate() for us as well
147
+ entry = @entrywin.getstr
148
+ @entrywin.clear
149
+ @entrywin.noutrefresh
150
+ entry
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,3 @@
1
+ module FSTui
2
+ VERSION = '0.5.5'
3
+ end
data/lib/fstui.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "curses";
2
+ include Curses
3
+
4
+ require 'fstui/screen'
5
+ require 'fstui/menu'
6
+ require 'fstui/inputbox'
7
+ require 'fstui/messagebox'
8
+ require 'fstui/infolist'
9
+ require 'fstui/datascreen'
10
+ require 'fstui/fieldscreen'
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fstui
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.5
5
+ platform: ruby
6
+ authors:
7
+ - sarahemm
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: curses
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.2'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 1.2.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '1.2'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 1.2.0
32
+ description: Full screen text UI library
33
+ email: github@sen.cx
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - README.md
39
+ - lib/fstui.rb
40
+ - lib/fstui/datascreen.rb
41
+ - lib/fstui/fieldscreen.rb
42
+ - lib/fstui/infolist.rb
43
+ - lib/fstui/inputbox.rb
44
+ - lib/fstui/menu.rb
45
+ - lib/fstui/messagebox.rb
46
+ - lib/fstui/screen.rb
47
+ - lib/fstui/version.rb
48
+ homepage: http://github.com/sarahemm/ruby-fstui
49
+ licenses:
50
+ - MIT
51
+ metadata: {}
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 4.0.21
67
+ specification_version: 4
68
+ summary: Full Screen TUI library
69
+ test_files: []