potty 0.0.1
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 +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE.txt +21 -0
- data/README.md +270 -0
- data/bin/potty_demo +128 -0
- data/examples/test_view.rb +87 -0
- data/lib/potty/animator.rb +127 -0
- data/lib/potty/application.rb +136 -0
- data/lib/potty/border.rb +51 -0
- data/lib/potty/events.rb +46 -0
- data/lib/potty/keys.rb +71 -0
- data/lib/potty/layout.rb +47 -0
- data/lib/potty/sprite.rb +49 -0
- data/lib/potty/sprites/sample.rb +36 -0
- data/lib/potty/style.rb +14 -0
- data/lib/potty/surface.rb +46 -0
- data/lib/potty/surfaces/curses_surface.rb +114 -0
- data/lib/potty/surfaces/inline_surface.rb +148 -0
- data/lib/potty/theme.rb +82 -0
- data/lib/potty/version.rb +5 -0
- data/lib/potty/view.rb +132 -0
- data/lib/potty/widgets/base.rb +114 -0
- data/lib/potty/widgets/button.rb +52 -0
- data/lib/potty/widgets/checkbox_group.rb +101 -0
- data/lib/potty/widgets/colored_fields_item.rb +56 -0
- data/lib/potty/widgets/container.rb +113 -0
- data/lib/potty/widgets/countdown.rb +81 -0
- data/lib/potty/widgets/flash_message.rb +69 -0
- data/lib/potty/widgets/label.rb +37 -0
- data/lib/potty/widgets/list.rb +192 -0
- data/lib/potty/widgets/list_item.rb +120 -0
- data/lib/potty/widgets/panel.rb +49 -0
- data/lib/potty/widgets/progress_bar.rb +55 -0
- data/lib/potty/widgets/radio_group.rb +121 -0
- data/lib/potty/widgets/spinner.rb +84 -0
- data/lib/potty/widgets/status_bar.rb +56 -0
- data/lib/potty/widgets/text_input.rb +138 -0
- data/lib/potty/widgets/toggle.rb +65 -0
- data/lib/potty/window_manager.rb +55 -0
- data/lib/potty.rb +35 -0
- metadata +112 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'curses'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative '../keys'
|
|
6
|
+
|
|
7
|
+
module Potty
|
|
8
|
+
module Widgets
|
|
9
|
+
# N mutually exclusive options, one row each. Up/down move the cursor;
|
|
10
|
+
# Space/Enter selects the option under the cursor. Renders "(\u25CF) label"
|
|
11
|
+
# for the chosen option and "(\u25CB) label" for the rest.
|
|
12
|
+
#
|
|
13
|
+
# Note the two distinct positions: the *cursor* (highlight, moved by
|
|
14
|
+
# arrows) and the *selection* (the committed value). They diverge while
|
|
15
|
+
# the user is navigating and reconverge on select.
|
|
16
|
+
class RadioGroup < Base
|
|
17
|
+
attr_accessor :on_change
|
|
18
|
+
|
|
19
|
+
def initialize(app, options: [], selected: nil, on_change: nil)
|
|
20
|
+
super(app)
|
|
21
|
+
@options = normalize(options)
|
|
22
|
+
@on_change = on_change
|
|
23
|
+
@selected = selected.nil? ? @options.first&.fetch(:value) : selected
|
|
24
|
+
@cursor = index_of(@selected) || 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def can_focus?
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def options
|
|
32
|
+
@options
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def options=(opts)
|
|
36
|
+
@options = normalize(opts)
|
|
37
|
+
@cursor = @cursor.clamp(0, [@options.size - 1, 0].max)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def selected
|
|
41
|
+
@selected
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def selected=(value)
|
|
45
|
+
idx = index_of(value)
|
|
46
|
+
return unless idx
|
|
47
|
+
|
|
48
|
+
@selected = value
|
|
49
|
+
@cursor = idx
|
|
50
|
+
@on_change&.call(@selected)
|
|
51
|
+
emit(:change, @selected)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def preferred_height(_width)
|
|
55
|
+
@options.size
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def handle_key(ch)
|
|
59
|
+
case ch
|
|
60
|
+
when Keys::UP
|
|
61
|
+
move(-1)
|
|
62
|
+
when Keys::DOWN
|
|
63
|
+
move(1)
|
|
64
|
+
when Keys::SPACE, *Keys::ENTERS
|
|
65
|
+
choose(@cursor)
|
|
66
|
+
else
|
|
67
|
+
return false
|
|
68
|
+
end
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render(window)
|
|
73
|
+
return unless @visible && @rect
|
|
74
|
+
|
|
75
|
+
@options.each_with_index do |opt, i|
|
|
76
|
+
break if i >= @rect.height
|
|
77
|
+
|
|
78
|
+
marker = opt[:value] == @selected ? "(\u25CF)" : "(\u25CB)"
|
|
79
|
+
on_cursor = @focused && i == @cursor
|
|
80
|
+
attr = on_cursor ? theme.attr(:selected, bold: true) : theme[:normal]
|
|
81
|
+
text = "#{marker} #{opt[:label]}"[0, @rect.width]
|
|
82
|
+
|
|
83
|
+
window.setpos(@rect.y + i, @rect.x)
|
|
84
|
+
window.attron(attr) { window.addstr(text) }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def normalize(opts)
|
|
91
|
+
(opts || []).map do |o|
|
|
92
|
+
if o.is_a?(Hash)
|
|
93
|
+
{ value: o[:value], label: (o[:label] || o[:value]).to_s }
|
|
94
|
+
else
|
|
95
|
+
{ value: o, label: o.to_s }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def move(delta)
|
|
101
|
+
return if @options.empty?
|
|
102
|
+
|
|
103
|
+
@cursor = (@cursor + delta) % @options.size
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def choose(idx)
|
|
107
|
+
opt = @options[idx]
|
|
108
|
+
return unless opt
|
|
109
|
+
return if opt[:value] == @selected
|
|
110
|
+
|
|
111
|
+
@selected = opt[:value]
|
|
112
|
+
@on_change&.call(@selected)
|
|
113
|
+
emit(:change, @selected)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def index_of(value)
|
|
117
|
+
@options.index { |o| o[:value] == value }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../animator'
|
|
5
|
+
require_relative '../sprites/sample'
|
|
6
|
+
|
|
7
|
+
module Potty
|
|
8
|
+
module Widgets
|
|
9
|
+
# Single-line activity indicator: an animated braille spinner, a live
|
|
10
|
+
# mutable label, and a trailing state. While active the glyph is the
|
|
11
|
+
# spinner; complete!(result) freezes it to a fixed glyph and flips the
|
|
12
|
+
# color. Passive (no focus/input). Tick-driven via an internal Animator.
|
|
13
|
+
#
|
|
14
|
+
# s = Spinner.new(app, label: "daemon - running")
|
|
15
|
+
# s.label = "daemon - surrendering 16 children" # live update
|
|
16
|
+
# s.complete!(:success) # glyph -> checkmark, color -> :success
|
|
17
|
+
class Spinner < Base
|
|
18
|
+
STATE_GLYPHS = { success: "\u2713", failure: "\u2717", cancelled: "\u23F9" }.freeze
|
|
19
|
+
STATE_COLORS = { success: :success, failure: :error, cancelled: :dim }.freeze
|
|
20
|
+
|
|
21
|
+
attr_accessor :label, :prefix
|
|
22
|
+
attr_reader :state, :color
|
|
23
|
+
|
|
24
|
+
def initialize(app, label: '', color: :info, prefix: ' ')
|
|
25
|
+
super(app)
|
|
26
|
+
@label = label
|
|
27
|
+
@color = color
|
|
28
|
+
@prefix = prefix
|
|
29
|
+
@state = :active
|
|
30
|
+
@animator = Animator.new(app)
|
|
31
|
+
@animator << Sprites::Sample.spinner
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def active?
|
|
35
|
+
@state == :active
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Freeze the spinner to a terminal state. Idempotent: only the first
|
|
39
|
+
# call takes effect, so repeated lifecycle events are harmless.
|
|
40
|
+
def complete!(result = :success)
|
|
41
|
+
return self unless active?
|
|
42
|
+
|
|
43
|
+
@state = result
|
|
44
|
+
@color = STATE_COLORS.fetch(result, @color)
|
|
45
|
+
@animator.stop
|
|
46
|
+
emit(:complete, result)
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def preferred_height(_width)
|
|
51
|
+
1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tick(now)
|
|
55
|
+
@animator.tick(now) if active?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render(window)
|
|
59
|
+
return unless @visible && @rect
|
|
60
|
+
|
|
61
|
+
glyph = active? ? current_frame : STATE_GLYPHS.fetch(@state, '?')
|
|
62
|
+
text = truncate("#{@prefix}#{glyph} #{@label}", @rect.width)
|
|
63
|
+
window.setpos(@rect.y, @rect.x)
|
|
64
|
+
# theme.style (not theme[]) so we render in colour on either surface —
|
|
65
|
+
# curses resolves the Style to a pair, inline to ANSI SGR.
|
|
66
|
+
window.attron(theme.style(@color)) { window.addstr(text) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def current_frame
|
|
72
|
+
sprite = @animator.sprite
|
|
73
|
+
sprite ? sprite.frame_lines(@animator.frame_index).first : ' '
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def truncate(str, width)
|
|
77
|
+
return str if str.length <= width
|
|
78
|
+
return str[0, width] || '' if width < 2
|
|
79
|
+
|
|
80
|
+
"#{str[0, width - 1]}\u2026"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Potty
|
|
6
|
+
module Widgets
|
|
7
|
+
# Status bar at bottom of screen
|
|
8
|
+
class StatusBar < Base
|
|
9
|
+
attr_accessor :left_text, :center_text, :right_text
|
|
10
|
+
|
|
11
|
+
def initialize(app)
|
|
12
|
+
super
|
|
13
|
+
@left_text = ""
|
|
14
|
+
@center_text = ""
|
|
15
|
+
@right_text = ""
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def preferred_height(width)
|
|
19
|
+
1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render(window)
|
|
23
|
+
return unless @rect
|
|
24
|
+
|
|
25
|
+
window.setpos(@rect.y, @rect.x)
|
|
26
|
+
window.attron(theme[:status]) do
|
|
27
|
+
# Clear line with background color
|
|
28
|
+
window.addstr(" " * @rect.width)
|
|
29
|
+
|
|
30
|
+
# Left-aligned
|
|
31
|
+
if @left_text && !@left_text.empty?
|
|
32
|
+
window.setpos(@rect.y, @rect.x)
|
|
33
|
+
max_left = @rect.width / 3
|
|
34
|
+
window.addstr(@left_text[0, max_left])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Center-aligned
|
|
38
|
+
if @center_text && !@center_text.empty?
|
|
39
|
+
center_x = @rect.x + (@rect.width - @center_text.length) / 2
|
|
40
|
+
center_x = [center_x, @rect.x].max
|
|
41
|
+
window.setpos(@rect.y, center_x)
|
|
42
|
+
window.addstr(@center_text[0, @rect.width])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Right-aligned
|
|
46
|
+
if @right_text && !@right_text.empty?
|
|
47
|
+
right_x = @rect.x + @rect.width - @right_text.length
|
|
48
|
+
right_x = [right_x, @rect.x].max
|
|
49
|
+
window.setpos(@rect.y, right_x)
|
|
50
|
+
window.addstr(@right_text)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'curses'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative '../keys'
|
|
6
|
+
|
|
7
|
+
module Potty
|
|
8
|
+
module Widgets
|
|
9
|
+
# Single-line editable text field. Shows a block cursor when focused,
|
|
10
|
+
# a dim placeholder when empty and unfocused, and scrolls horizontally
|
|
11
|
+
# when the text outgrows the field.
|
|
12
|
+
#
|
|
13
|
+
# Emits :change(text) on every edit. ASCII input only for now (matches
|
|
14
|
+
# the rest of the framework); UTF-8 entry would need multibyte getch.
|
|
15
|
+
class TextInput < Base
|
|
16
|
+
attr_reader :text
|
|
17
|
+
attr_accessor :placeholder, :max_length, :on_change
|
|
18
|
+
|
|
19
|
+
def initialize(app, text: '', placeholder: '', max_length: nil, on_change: nil)
|
|
20
|
+
super(app)
|
|
21
|
+
@text = text.dup
|
|
22
|
+
@placeholder = placeholder
|
|
23
|
+
@max_length = max_length
|
|
24
|
+
@on_change = on_change
|
|
25
|
+
@cursor = @text.length
|
|
26
|
+
@scroll = 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def can_focus?
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def text=(value)
|
|
34
|
+
@text = value.to_s.dup
|
|
35
|
+
@cursor = [@cursor, @text.length].min
|
|
36
|
+
notify_change
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def preferred_height(_width)
|
|
40
|
+
1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def handle_key(ch)
|
|
44
|
+
case ch
|
|
45
|
+
when Keys::LEFT
|
|
46
|
+
@cursor = [@cursor - 1, 0].max
|
|
47
|
+
when Keys::RIGHT
|
|
48
|
+
@cursor = [@cursor + 1, @text.length].min
|
|
49
|
+
when Keys::HOME, Keys::CTRL_A
|
|
50
|
+
@cursor = 0
|
|
51
|
+
when Keys::END_, Keys::CTRL_E
|
|
52
|
+
@cursor = @text.length
|
|
53
|
+
when Keys::DEL_ASCII, Keys::CTRL_H, Keys::BACKSPACE
|
|
54
|
+
backspace
|
|
55
|
+
when Keys::DELETE, Keys::CTRL_D
|
|
56
|
+
delete_forward
|
|
57
|
+
when Keys::SPACE..(Keys::DEL_ASCII - 1)
|
|
58
|
+
insert(ch.chr)
|
|
59
|
+
else
|
|
60
|
+
return false
|
|
61
|
+
end
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render(window)
|
|
66
|
+
return unless @visible && @rect
|
|
67
|
+
|
|
68
|
+
width = @rect.width
|
|
69
|
+
adjust_scroll(width)
|
|
70
|
+
|
|
71
|
+
if @text.empty? && !@focused
|
|
72
|
+
window.setpos(@rect.y, @rect.x)
|
|
73
|
+
window.attron(theme[:dim]) do
|
|
74
|
+
window.addstr(@placeholder.to_s[0, width].to_s.ljust(width))
|
|
75
|
+
end
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
visible = (@text[@scroll, width] || '').ljust(width)
|
|
80
|
+
window.setpos(@rect.y, @rect.x)
|
|
81
|
+
window.attron(theme[:normal]) { window.addstr(visible) }
|
|
82
|
+
|
|
83
|
+
return unless @focused
|
|
84
|
+
|
|
85
|
+
# Block cursor: reverse-video the cell under the caret.
|
|
86
|
+
col = @cursor - @scroll
|
|
87
|
+
return if col.negative? || col >= width
|
|
88
|
+
|
|
89
|
+
char_under = @text[@cursor] || ' '
|
|
90
|
+
window.setpos(@rect.y, @rect.x + col)
|
|
91
|
+
window.attron(theme[:normal] | ::Curses::A_REVERSE) do
|
|
92
|
+
window.addstr(char_under)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def insert(str)
|
|
99
|
+
return if @max_length && @text.length >= @max_length
|
|
100
|
+
|
|
101
|
+
@text.insert(@cursor, str)
|
|
102
|
+
@cursor += str.length
|
|
103
|
+
notify_change
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def backspace
|
|
107
|
+
return if @cursor.zero?
|
|
108
|
+
|
|
109
|
+
@text.slice!(@cursor - 1)
|
|
110
|
+
@cursor -= 1
|
|
111
|
+
notify_change
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def delete_forward
|
|
115
|
+
return if @cursor >= @text.length
|
|
116
|
+
|
|
117
|
+
@text.slice!(@cursor)
|
|
118
|
+
notify_change
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def adjust_scroll(width)
|
|
122
|
+
return if width <= 0
|
|
123
|
+
|
|
124
|
+
@scroll = @cursor - width + 1 if @cursor - @scroll >= width
|
|
125
|
+
@scroll = @cursor if @cursor < @scroll
|
|
126
|
+
@scroll = [@scroll, 0].max
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def notify_change
|
|
130
|
+
# Hand listeners a snapshot, not the live internal buffer, so a
|
|
131
|
+
# consumer that stores the value doesn't see it mutate underfoot.
|
|
132
|
+
snapshot = @text.dup
|
|
133
|
+
@on_change&.call(snapshot)
|
|
134
|
+
emit(:change, snapshot)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../keys'
|
|
5
|
+
|
|
6
|
+
module Potty
|
|
7
|
+
module Widgets
|
|
8
|
+
# Boolean on/off control. Space (or Enter) flips it when focused.
|
|
9
|
+
# Renders "[\u25CF] label" when on, "[\u25CB] label" when off.
|
|
10
|
+
# Emits :change(value) when toggled.
|
|
11
|
+
class Toggle < Base
|
|
12
|
+
attr_reader :value
|
|
13
|
+
attr_accessor :label, :on_change
|
|
14
|
+
|
|
15
|
+
def initialize(app, label: '', value: false, on_change: nil)
|
|
16
|
+
super(app)
|
|
17
|
+
@label = label
|
|
18
|
+
@value = value
|
|
19
|
+
@on_change = on_change
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def can_focus?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def value=(val)
|
|
27
|
+
val = val ? true : false
|
|
28
|
+
return if val == @value
|
|
29
|
+
|
|
30
|
+
@value = val
|
|
31
|
+
@on_change&.call(@value)
|
|
32
|
+
emit(:change, @value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def toggle
|
|
36
|
+
self.value = !@value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def preferred_height(_width)
|
|
40
|
+
1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def handle_key(ch)
|
|
44
|
+
case ch
|
|
45
|
+
when Keys::SPACE, *Keys::ENTERS
|
|
46
|
+
toggle
|
|
47
|
+
true
|
|
48
|
+
else
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render(window)
|
|
54
|
+
return unless @visible && @rect
|
|
55
|
+
|
|
56
|
+
knob = @value ? "[\u25CF]" : "[\u25CB]"
|
|
57
|
+
text = "#{knob} #{@label}"[0, @rect.width]
|
|
58
|
+
attr = @focused ? theme.attr(:selected, bold: true) : theme[:normal]
|
|
59
|
+
|
|
60
|
+
window.setpos(@rect.y, @rect.x)
|
|
61
|
+
window.attron(attr) { window.addstr(text) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'curses'
|
|
4
|
+
|
|
5
|
+
module Potty
|
|
6
|
+
# Manages curses window lifecycle and refresh coordination
|
|
7
|
+
class WindowManager
|
|
8
|
+
attr_reader :stdscr, :max_y, :max_x
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@windows = {}
|
|
12
|
+
@stdscr = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Called during application setup
|
|
16
|
+
def setup(stdscr)
|
|
17
|
+
@stdscr = stdscr
|
|
18
|
+
update_dimensions
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def update_dimensions
|
|
22
|
+
@max_y, @max_x = @stdscr.maxy, @stdscr.maxx
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Create a new window
|
|
26
|
+
def create_window(name, height, width, y, x)
|
|
27
|
+
win = ::Curses::Window.new(height, width, y, x)
|
|
28
|
+
@windows[name] = win
|
|
29
|
+
win
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get existing window
|
|
33
|
+
def get_window(name)
|
|
34
|
+
@windows[name]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Destroy window
|
|
38
|
+
def destroy_window(name)
|
|
39
|
+
@windows[name]&.close
|
|
40
|
+
@windows.delete(name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Refresh all windows efficiently
|
|
44
|
+
def refresh_all
|
|
45
|
+
@stdscr.noutrefresh
|
|
46
|
+
@windows.values.each(&:noutrefresh)
|
|
47
|
+
::Curses.doupdate
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clear_all
|
|
51
|
+
@stdscr.clear
|
|
52
|
+
@windows.values.each(&:clear)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/potty.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'potty/version'
|
|
4
|
+
require_relative 'potty/keys'
|
|
5
|
+
require_relative 'potty/events'
|
|
6
|
+
require_relative 'potty/style'
|
|
7
|
+
require_relative 'potty/application'
|
|
8
|
+
require_relative 'potty/theme'
|
|
9
|
+
require_relative 'potty/layout'
|
|
10
|
+
require_relative 'potty/border'
|
|
11
|
+
require_relative 'potty/window_manager'
|
|
12
|
+
require_relative 'potty/view'
|
|
13
|
+
require_relative 'potty/widgets/base'
|
|
14
|
+
require_relative 'potty/widgets/container'
|
|
15
|
+
require_relative 'potty/widgets/panel'
|
|
16
|
+
require_relative 'potty/widgets/list'
|
|
17
|
+
require_relative 'potty/widgets/list_item'
|
|
18
|
+
require_relative 'potty/widgets/colored_fields_item'
|
|
19
|
+
require_relative 'potty/widgets/flash_message'
|
|
20
|
+
require_relative 'potty/widgets/status_bar'
|
|
21
|
+
require_relative 'potty/widgets/progress_bar'
|
|
22
|
+
require_relative 'potty/widgets/label'
|
|
23
|
+
require_relative 'potty/widgets/button'
|
|
24
|
+
require_relative 'potty/widgets/text_input'
|
|
25
|
+
require_relative 'potty/widgets/toggle'
|
|
26
|
+
require_relative 'potty/widgets/radio_group'
|
|
27
|
+
require_relative 'potty/widgets/checkbox_group'
|
|
28
|
+
require_relative 'potty/widgets/countdown'
|
|
29
|
+
require_relative 'potty/widgets/spinner'
|
|
30
|
+
require_relative 'potty/sprite'
|
|
31
|
+
require_relative 'potty/animator'
|
|
32
|
+
require_relative 'potty/sprites/sample'
|
|
33
|
+
|
|
34
|
+
module Potty
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: potty
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- TwilightCoders
|
|
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.4'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.4'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.12'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.12'
|
|
40
|
+
description: Provides views, widgets, layout, theming, and frame-based animation for
|
|
41
|
+
building curses TUI applications.
|
|
42
|
+
email:
|
|
43
|
+
- info@twilightcoders.net
|
|
44
|
+
executables:
|
|
45
|
+
- potty_demo
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- CHANGELOG.md
|
|
50
|
+
- LICENSE.txt
|
|
51
|
+
- README.md
|
|
52
|
+
- bin/potty_demo
|
|
53
|
+
- examples/test_view.rb
|
|
54
|
+
- lib/potty.rb
|
|
55
|
+
- lib/potty/animator.rb
|
|
56
|
+
- lib/potty/application.rb
|
|
57
|
+
- lib/potty/border.rb
|
|
58
|
+
- lib/potty/events.rb
|
|
59
|
+
- lib/potty/keys.rb
|
|
60
|
+
- lib/potty/layout.rb
|
|
61
|
+
- lib/potty/sprite.rb
|
|
62
|
+
- lib/potty/sprites/sample.rb
|
|
63
|
+
- lib/potty/style.rb
|
|
64
|
+
- lib/potty/surface.rb
|
|
65
|
+
- lib/potty/surfaces/curses_surface.rb
|
|
66
|
+
- lib/potty/surfaces/inline_surface.rb
|
|
67
|
+
- lib/potty/theme.rb
|
|
68
|
+
- lib/potty/version.rb
|
|
69
|
+
- lib/potty/view.rb
|
|
70
|
+
- lib/potty/widgets/base.rb
|
|
71
|
+
- lib/potty/widgets/button.rb
|
|
72
|
+
- lib/potty/widgets/checkbox_group.rb
|
|
73
|
+
- lib/potty/widgets/colored_fields_item.rb
|
|
74
|
+
- lib/potty/widgets/container.rb
|
|
75
|
+
- lib/potty/widgets/countdown.rb
|
|
76
|
+
- lib/potty/widgets/flash_message.rb
|
|
77
|
+
- lib/potty/widgets/label.rb
|
|
78
|
+
- lib/potty/widgets/list.rb
|
|
79
|
+
- lib/potty/widgets/list_item.rb
|
|
80
|
+
- lib/potty/widgets/panel.rb
|
|
81
|
+
- lib/potty/widgets/progress_bar.rb
|
|
82
|
+
- lib/potty/widgets/radio_group.rb
|
|
83
|
+
- lib/potty/widgets/spinner.rb
|
|
84
|
+
- lib/potty/widgets/status_bar.rb
|
|
85
|
+
- lib/potty/widgets/text_input.rb
|
|
86
|
+
- lib/potty/widgets/toggle.rb
|
|
87
|
+
- lib/potty/window_manager.rb
|
|
88
|
+
homepage: https://github.com/TwilightCoders/potty
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata:
|
|
92
|
+
source_code_uri: https://github.com/TwilightCoders/potty
|
|
93
|
+
changelog_uri: https://github.com/TwilightCoders/potty/blob/main/CHANGELOG.md
|
|
94
|
+
rubygems_mfa_required: 'true'
|
|
95
|
+
rdoc_options: []
|
|
96
|
+
require_paths:
|
|
97
|
+
- lib
|
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 2.7.0
|
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - ">="
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '0'
|
|
108
|
+
requirements: []
|
|
109
|
+
rubygems_version: 3.6.9
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: A curses-based terminal UI framework for Ruby
|
|
112
|
+
test_files: []
|