potty 0.0.1 → 0.0.2
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 +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +16 -9
- data/bin/potty_inline_demo +78 -0
- data/lib/potty/ansi.rb +36 -0
- data/lib/potty/application.rb +6 -2
- data/lib/potty/input/decoder.rb +113 -0
- data/lib/potty/layout.rb +0 -23
- data/lib/potty/line_editor.rb +53 -0
- data/lib/potty/mouth.rb +217 -0
- data/lib/potty/surfaces/curses_surface.rb +13 -19
- data/lib/potty/surfaces/inline_surface.rb +62 -29
- data/lib/potty/theme.rb +14 -29
- data/lib/potty/version.rb +1 -1
- data/lib/potty/view.rb +15 -4
- data/lib/potty/widgets/button.rb +1 -1
- data/lib/potty/widgets/checkbox_group.rb +1 -1
- data/lib/potty/widgets/colored_fields_item.rb +1 -1
- data/lib/potty/widgets/countdown.rb +1 -1
- data/lib/potty/widgets/flash_message.rb +4 -4
- data/lib/potty/widgets/label.rb +1 -1
- data/lib/potty/widgets/list.rb +7 -7
- data/lib/potty/widgets/list_item.rb +22 -23
- data/lib/potty/widgets/panel.rb +1 -1
- data/lib/potty/widgets/radio_group.rb +9 -1
- data/lib/potty/widgets/status_bar.rb +1 -1
- data/lib/potty/widgets/text_input.rb +38 -54
- data/lib/potty/widgets/toggle.rb +1 -1
- data/lib/potty/window_manager.rb +6 -29
- data/lib/potty.rb +5 -0
- metadata +7 -1
|
@@ -8,21 +8,18 @@ require_relative '../keys'
|
|
|
8
8
|
module Potty
|
|
9
9
|
module Surfaces
|
|
10
10
|
# The default render target: a full-screen curses display. Wraps the
|
|
11
|
-
# WindowManager / stdscr
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# through). That dual acceptance is what lets un-migrated widgets — and
|
|
16
|
-
# direct-to-window consumers like a host's own paint code — keep working
|
|
17
|
-
# untouched while new widgets go render-target-agnostic.
|
|
11
|
+
# WindowManager / stdscr. It resolves a Style to a curses colour pair +
|
|
12
|
+
# attributes (allocating pairs on demand); a raw integer is passed through
|
|
13
|
+
# unchanged, so a host that draws to the surface with its own curses attrs
|
|
14
|
+
# still works.
|
|
18
15
|
class CursesSurface < Surface
|
|
19
16
|
def initialize(window_manager, theme, tick_interval: nil)
|
|
20
17
|
super()
|
|
21
18
|
@wm = window_manager
|
|
22
19
|
@theme = theme
|
|
23
20
|
@tick_interval = tick_interval
|
|
24
|
-
@next_pair =
|
|
25
|
-
@pairs =
|
|
21
|
+
@next_pair = 1
|
|
22
|
+
@pairs = {}
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def start
|
|
@@ -36,7 +33,10 @@ module Potty
|
|
|
36
33
|
::Curses.cbreak
|
|
37
34
|
stdscr.keypad(true)
|
|
38
35
|
stdscr.timeout = @tick_interval if @tick_interval
|
|
39
|
-
|
|
36
|
+
if ::Curses.has_colors?
|
|
37
|
+
::Curses.start_color
|
|
38
|
+
::Curses.use_default_colors # enables -1 = terminal default
|
|
39
|
+
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def finalize
|
|
@@ -88,21 +88,15 @@ module Potty
|
|
|
88
88
|
attr
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# Allocate (once) and cache a curses colour pair per (fg, bg) combo the
|
|
92
|
+
# theme's Styles use. The palette is small, so this stays well within
|
|
93
|
+
# the terminal's pair budget.
|
|
91
94
|
def color_pair(fg, bg)
|
|
92
95
|
return 0 unless ::Curses.has_colors?
|
|
93
96
|
|
|
94
|
-
(@pairs ||= seed_pairs)
|
|
95
97
|
@pairs[[fg, bg]] ||= allocate_pair(fg, bg)
|
|
96
98
|
end
|
|
97
99
|
|
|
98
|
-
# Reuse the pairs Theme already allocated for its palette combos so we
|
|
99
|
-
# don't burn a second pair on every colour we share with theme[].
|
|
100
|
-
def seed_pairs
|
|
101
|
-
map = {}
|
|
102
|
-
@theme.palette.each { |name, c| map[[c[:fg], c[:bg]]] = @theme[name] }
|
|
103
|
-
map
|
|
104
|
-
end
|
|
105
|
-
|
|
106
100
|
def allocate_pair(fg, bg)
|
|
107
101
|
n = @next_pair
|
|
108
102
|
@next_pair += 1
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'io/console'
|
|
3
4
|
require_relative '../surface'
|
|
5
|
+
require_relative '../ansi'
|
|
6
|
+
require_relative '../input/decoder'
|
|
4
7
|
|
|
5
8
|
module Potty
|
|
6
9
|
module Surfaces
|
|
@@ -15,25 +18,21 @@ module Potty
|
|
|
15
18
|
# then cursor back to the top). finalize freezes the last frame and drops
|
|
16
19
|
# the cursor to the line below so the next prompt lands cleanly.
|
|
17
20
|
class InlineSurface < Surface
|
|
18
|
-
|
|
19
|
-
default: 39, black: 30, red: 31, green: 32, yellow: 33,
|
|
20
|
-
blue: 34, magenta: 35, cyan: 36, white: 37, bright_black: 90
|
|
21
|
-
}.freeze
|
|
22
|
-
SGR_BG = {
|
|
23
|
-
default: 49, black: 40, red: 41, green: 42, yellow: 43,
|
|
24
|
-
blue: 44, magenta: 45, cyan: 46, white: 47, bright_black: 100
|
|
25
|
-
}.freeze
|
|
26
|
-
|
|
27
|
-
def initialize(theme:, lines: nil, tick_interval: 40, out: $stdout)
|
|
21
|
+
def initialize(theme:, lines: nil, tick_interval: 40, out: $stdout, listen: false, input: $stdin)
|
|
28
22
|
super()
|
|
29
23
|
@theme = theme
|
|
30
24
|
@rows = [lines || 1, 1].max
|
|
31
25
|
@tick_interval = tick_interval
|
|
32
26
|
@out = out
|
|
27
|
+
@listen = listen
|
|
28
|
+
@input = input
|
|
33
29
|
@cols = detect_cols
|
|
34
30
|
@cursor = [0, 0]
|
|
35
31
|
@cur_style = nil
|
|
36
32
|
@primed = false
|
|
33
|
+
@raw = false
|
|
34
|
+
@decoder = (Input::Decoder.new if listen)
|
|
35
|
+
@queue = []
|
|
37
36
|
erase
|
|
38
37
|
end
|
|
39
38
|
|
|
@@ -44,11 +43,16 @@ module Potty
|
|
|
44
43
|
def start
|
|
45
44
|
@out.write("\e[?25l") # hide cursor
|
|
46
45
|
@out.flush
|
|
46
|
+
enter_raw if @listen
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def finalize
|
|
50
50
|
present # freeze the final frame
|
|
51
|
-
|
|
51
|
+
restore_cooked
|
|
52
|
+
# Explicit CR+LF: present leaves the cursor at the end of the last
|
|
53
|
+
# rendered row, and in raw mode \n alone is a bare line-feed (no
|
|
54
|
+
# column reset), which would indent whatever the host prints next.
|
|
55
|
+
@out.write("\r\n") # drop below the region, at column 0
|
|
52
56
|
@out.write("\e[?25h") # restore cursor
|
|
53
57
|
@out.flush
|
|
54
58
|
end
|
|
@@ -97,15 +101,56 @@ module Potty
|
|
|
97
101
|
@out.flush
|
|
98
102
|
end
|
|
99
103
|
|
|
100
|
-
#
|
|
101
|
-
#
|
|
104
|
+
# In listen mode: drain raw stdin (non-blocking, waiting up to one tick
|
|
105
|
+
# for the first byte), decode to Keys codes, and return them one per
|
|
106
|
+
# call (queueing the rest). Without listening (or off a real TTY): just
|
|
107
|
+
# pace the loop and return nil, leaving input alone. Ctrl-C arrives as a
|
|
108
|
+
# byte the decoder passes through as Keys::CTRL_C; the event loop raises.
|
|
102
109
|
def read_key
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
return @queue.shift unless @queue.empty?
|
|
111
|
+
|
|
112
|
+
if @raw
|
|
113
|
+
fill_queue
|
|
114
|
+
@queue.shift
|
|
115
|
+
else
|
|
116
|
+
sleep(@tick_interval / 1000.0) if @tick_interval
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
105
119
|
end
|
|
106
120
|
|
|
107
121
|
private
|
|
108
122
|
|
|
123
|
+
def enter_raw
|
|
124
|
+
return unless @input.respond_to?(:raw!) && tty_input?
|
|
125
|
+
|
|
126
|
+
@input.raw! # no echo, no canonical, no signal processing
|
|
127
|
+
@raw = true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def restore_cooked
|
|
131
|
+
return unless @raw
|
|
132
|
+
|
|
133
|
+
@input.cooked!
|
|
134
|
+
@raw = false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def tty_input?
|
|
138
|
+
@input.respond_to?(:tty?) && @input.tty?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def fill_queue
|
|
142
|
+
seconds = (@tick_interval || 40) / 1000.0
|
|
143
|
+
bytes = +''
|
|
144
|
+
if IO.select([@input], nil, nil, seconds)
|
|
145
|
+
begin
|
|
146
|
+
loop { bytes << @input.read_nonblock(256) }
|
|
147
|
+
rescue IO::WaitReadable, EOFError
|
|
148
|
+
# drained for now
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
@queue.concat(@decoder.feed(bytes, Time.now))
|
|
152
|
+
end
|
|
153
|
+
|
|
109
154
|
def render_row(cells)
|
|
110
155
|
last = cells.rindex { |ch, _| ch != ' ' } || -1
|
|
111
156
|
return '' if last.negative?
|
|
@@ -115,27 +160,15 @@ module Potty
|
|
|
115
160
|
(0..last).each do |i|
|
|
116
161
|
ch, style = cells[i]
|
|
117
162
|
if style != emitted
|
|
118
|
-
out << sgr(style)
|
|
163
|
+
out << Ansi.sgr(style)
|
|
119
164
|
emitted = style
|
|
120
165
|
end
|
|
121
166
|
out << ch
|
|
122
167
|
end
|
|
123
|
-
out <<
|
|
168
|
+
out << Ansi::RESET if emitted
|
|
124
169
|
out
|
|
125
170
|
end
|
|
126
171
|
|
|
127
|
-
def sgr(style)
|
|
128
|
-
return "\e[0m" if style.nil?
|
|
129
|
-
|
|
130
|
-
codes = []
|
|
131
|
-
codes << 1 if style.bold?
|
|
132
|
-
codes << 4 if style.underline?
|
|
133
|
-
codes << 7 if style.reverse?
|
|
134
|
-
codes << SGR_FG.fetch(style.fg, 39)
|
|
135
|
-
codes << SGR_BG.fetch(style.bg, 49)
|
|
136
|
-
"\e[#{codes.join(';')}m"
|
|
137
|
-
end
|
|
138
|
-
|
|
139
172
|
def detect_cols
|
|
140
173
|
return @out.winsize[1] if @out.respond_to?(:winsize) && @out.tty?
|
|
141
174
|
|
data/lib/potty/theme.rb
CHANGED
|
@@ -4,18 +4,18 @@ require 'curses'
|
|
|
4
4
|
require_relative 'style'
|
|
5
5
|
|
|
6
6
|
module Potty
|
|
7
|
-
# Theme maps semantic names to
|
|
7
|
+
# Theme maps semantic names to a render-target-agnostic Style (symbolic
|
|
8
|
+
# colours + attributes). It is pure data — it does NOT touch curses. Each
|
|
9
|
+
# Surface resolves a Style to its concrete form (CursesSurface to a colour
|
|
10
|
+
# pair, InlineSurface to ANSI SGR), which is why a single Theme drives both
|
|
11
|
+
# rendering modes and why every widget that asks the theme for an attribute
|
|
12
|
+
# works in either mode with no per-widget special-casing.
|
|
8
13
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# theme[:info] => a curses attribute integer (a colour pair) —
|
|
12
|
-
# back-compat for code that draws straight to a
|
|
13
|
-
# Curses window. Only meaningful once curses is up.
|
|
14
|
-
#
|
|
15
|
-
# The symbolic PALETTE is the source of truth; the curses pairs are derived
|
|
16
|
-
# from it in setup_colors, so the two dialects never drift.
|
|
14
|
+
# `style`, `[]`, and `attr` all return a Style — `[]`/`attr` are kept as
|
|
15
|
+
# ergonomic aliases (attr adds bold/underline).
|
|
17
16
|
class Theme
|
|
18
17
|
# Symbolic colour names -> curses colour numbers (-1 = terminal default).
|
|
18
|
+
# Used by CursesSurface when it resolves a Style to a colour pair.
|
|
19
19
|
COLORS = {
|
|
20
20
|
default: -1,
|
|
21
21
|
black: ::Curses::COLOR_BLACK, red: ::Curses::COLOR_RED,
|
|
@@ -41,24 +41,11 @@ module Potty
|
|
|
41
41
|
status: { fg: :black, bg: :cyan }
|
|
42
42
|
}.freeze
|
|
43
43
|
|
|
44
|
-
attr_reader :palette
|
|
44
|
+
attr_reader :palette
|
|
45
45
|
|
|
46
46
|
# Pass a partial palette ({ name => { fg:, bg: } }) to override entries.
|
|
47
47
|
def initialize(palette = nil)
|
|
48
48
|
@palette = palette ? PALETTE.merge(palette) : PALETTE
|
|
49
|
-
@colors = {}
|
|
50
|
-
setup_colors if ::Curses.has_colors?
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Allocate a curses colour pair per palette entry (curses mode only).
|
|
54
|
-
def setup_colors
|
|
55
|
-
::Curses.start_color
|
|
56
|
-
::Curses.use_default_colors
|
|
57
|
-
@palette.each_with_index do |(name, c), idx|
|
|
58
|
-
pair = idx + 1
|
|
59
|
-
::Curses.init_pair(pair, COLORS.fetch(c[:fg], -1), COLORS.fetch(c[:bg], -1))
|
|
60
|
-
@colors[name] = ::Curses.color_pair(pair)
|
|
61
|
-
end
|
|
62
49
|
end
|
|
63
50
|
|
|
64
51
|
# Semantic style — symbolic colours + attributes, resolved by a Surface.
|
|
@@ -67,16 +54,14 @@ module Potty
|
|
|
67
54
|
Style.new(fg: c[:fg], bg: c[:bg], bold: bold, underline: underline, reverse: reverse)
|
|
68
55
|
end
|
|
69
56
|
|
|
70
|
-
#
|
|
57
|
+
# Ergonomic aliases — both return a Style, so widgets can use whichever
|
|
58
|
+
# reads best and still render in any mode.
|
|
71
59
|
def [](name)
|
|
72
|
-
|
|
60
|
+
style(name)
|
|
73
61
|
end
|
|
74
62
|
|
|
75
63
|
def attr(name, bold: false, underline: false)
|
|
76
|
-
|
|
77
|
-
a |= ::Curses::A_BOLD if bold
|
|
78
|
-
a |= ::Curses::A_UNDERLINE if underline
|
|
79
|
-
a
|
|
64
|
+
style(name, bold: bold, underline: underline)
|
|
80
65
|
end
|
|
81
66
|
end
|
|
82
67
|
end
|
data/lib/potty/version.rb
CHANGED
data/lib/potty/view.rb
CHANGED
|
@@ -37,13 +37,20 @@ module Potty
|
|
|
37
37
|
rows, cols = @app.surface.size
|
|
38
38
|
container = Layout::Rect.new(0, 0, cols, rows)
|
|
39
39
|
|
|
40
|
-
# Simple stack layout
|
|
41
|
-
|
|
40
|
+
# Simple stack layout. Override #spacing to pack tighter — inline views
|
|
41
|
+
# in particular want 0 so their height matches the region exactly.
|
|
42
|
+
rects = Layout.stack(container, @widgets, spacing: spacing)
|
|
42
43
|
@widgets.zip(rects).each do |widget, rect|
|
|
43
44
|
widget.layout(rect)
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
# Rows of blank space the default layout leaves between top-level widgets.
|
|
49
|
+
# Override to change it (e.g. 0 for a tightly-packed inline region).
|
|
50
|
+
def spacing
|
|
51
|
+
1
|
|
52
|
+
end
|
|
53
|
+
|
|
47
54
|
# Draw the widget tree onto the application's surface. The surface frame
|
|
48
55
|
# (erase/present) is owned by Application#refresh_all, so this just paints.
|
|
49
56
|
def render
|
|
@@ -63,9 +70,13 @@ module Potty
|
|
|
63
70
|
# Delegate to focused widget first
|
|
64
71
|
return true if focused_widget&.handle_key(ch)
|
|
65
72
|
|
|
66
|
-
# Handle view-level keys
|
|
73
|
+
# Handle view-level keys. Enter advances focus like Tab when the
|
|
74
|
+
# focused widget didn't consume it — so a form flows field -> field ->
|
|
75
|
+
# button (where the button finally consumes Enter and fires). A view
|
|
76
|
+
# that wants Enter for itself (e.g. a prompt that submits) intercepts it
|
|
77
|
+
# before delegating to super.
|
|
67
78
|
case ch
|
|
68
|
-
when Keys::TAB
|
|
79
|
+
when Keys::TAB, *Keys::ENTERS
|
|
69
80
|
cycle_focus(1)
|
|
70
81
|
true
|
|
71
82
|
when Keys::SHIFT_TAB
|
data/lib/potty/widgets/button.rb
CHANGED
|
@@ -43,7 +43,7 @@ module Potty
|
|
|
43
43
|
return unless @visible && @rect
|
|
44
44
|
|
|
45
45
|
text = "[ #{@label} ]"[0, @rect.width]
|
|
46
|
-
attr = @focused ? theme.
|
|
46
|
+
attr = @focused ? theme.style(:selected, bold: true) : theme.style(@color)
|
|
47
47
|
window.setpos(@rect.y, @rect.x)
|
|
48
48
|
window.attron(attr) { window.addstr(text) }
|
|
49
49
|
end
|
|
@@ -63,7 +63,7 @@ module Potty
|
|
|
63
63
|
|
|
64
64
|
mark = selected?(opt[:value]) ? "[\u2713]" : "[ ]"
|
|
65
65
|
on_cursor = @focused && i == @cursor
|
|
66
|
-
attr = on_cursor ? theme.
|
|
66
|
+
attr = on_cursor ? theme.style(:selected, bold: true) : theme.style(:normal)
|
|
67
67
|
window.setpos(@rect.y + i, @rect.x)
|
|
68
68
|
window.attron(attr) { window.addstr("#{mark} #{opt[:label]}"[0, @rect.width]) }
|
|
69
69
|
end
|
|
@@ -45,10 +45,10 @@ module Potty
|
|
|
45
45
|
# Show message if present
|
|
46
46
|
if @message
|
|
47
47
|
attr = case @type
|
|
48
|
-
when :success then theme
|
|
49
|
-
when :error then theme
|
|
50
|
-
when :warning then theme
|
|
51
|
-
else theme
|
|
48
|
+
when :success then theme.style(:success)
|
|
49
|
+
when :error then theme.style(:error)
|
|
50
|
+
when :warning then theme.style(:warning)
|
|
51
|
+
else theme.style(:info)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
window.setpos(@rect.y, @rect.x)
|
data/lib/potty/widgets/label.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Potty
|
|
|
28
28
|
def render(window)
|
|
29
29
|
return unless @visible && @rect
|
|
30
30
|
|
|
31
|
-
attr =
|
|
31
|
+
attr = theme.style(@color, bold: @bold)
|
|
32
32
|
window.setpos(@rect.y, @rect.x)
|
|
33
33
|
window.attron(attr) { window.addstr(@text.to_s[0, @rect.width] || '') }
|
|
34
34
|
end
|
data/lib/potty/widgets/list.rb
CHANGED
|
@@ -55,7 +55,7 @@ module Potty
|
|
|
55
55
|
# Show empty message if no items
|
|
56
56
|
if @items.empty?
|
|
57
57
|
window.setpos(@rect.y + 1, @rect.x + 2)
|
|
58
|
-
window.attron(theme
|
|
58
|
+
window.attron(theme.style(:dim)) do
|
|
59
59
|
window.addstr("No items")
|
|
60
60
|
end
|
|
61
61
|
return
|
|
@@ -94,7 +94,7 @@ module Potty
|
|
|
94
94
|
private
|
|
95
95
|
|
|
96
96
|
def draw_border(window)
|
|
97
|
-
Border.draw(window, @rect, attr: theme
|
|
97
|
+
Border.draw(window, @rect, attr: theme.style(:normal))
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def render_item(window, item, index, y, x)
|
|
@@ -105,7 +105,7 @@ module Potty
|
|
|
105
105
|
window.setpos(y, x)
|
|
106
106
|
|
|
107
107
|
# Selection prefix
|
|
108
|
-
prefix_attr = is_selected && @focused ? theme.
|
|
108
|
+
prefix_attr = is_selected && @focused ? theme.style(:selected, bold: true) : theme.style(:normal)
|
|
109
109
|
window.attron(prefix_attr) do
|
|
110
110
|
prefix = is_selected ? "\u2192 " : " "
|
|
111
111
|
window.addstr(prefix)
|
|
@@ -118,13 +118,13 @@ module Potty
|
|
|
118
118
|
|
|
119
119
|
# Default single-color rendering
|
|
120
120
|
attr = if is_selected && @focused
|
|
121
|
-
theme.
|
|
121
|
+
theme.style(:selected, bold: true)
|
|
122
122
|
elsif is_disabled
|
|
123
|
-
theme
|
|
123
|
+
theme.style(:dim)
|
|
124
124
|
elsif item.color
|
|
125
|
-
theme
|
|
125
|
+
theme.style(item.color)
|
|
126
126
|
else
|
|
127
|
-
theme
|
|
127
|
+
theme.style(:normal)
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
window.attron(attr) do
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'curses'
|
|
4
4
|
require_relative '../keys'
|
|
5
|
+
require_relative '../line_editor'
|
|
5
6
|
|
|
6
7
|
module Potty
|
|
7
8
|
module Widgets
|
|
@@ -59,46 +60,44 @@ module Potty
|
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
62
|
-
# Text input item
|
|
63
|
+
# Text input item — inline text editing within a List. Shares the
|
|
64
|
+
# LineEditor model with the TextInput widget.
|
|
63
65
|
class InputItem < ListItem
|
|
64
|
-
attr_accessor :input_value
|
|
65
|
-
|
|
66
66
|
def initialize(label, default: "", &on_submit)
|
|
67
67
|
super(label)
|
|
68
|
-
@
|
|
68
|
+
@editor = LineEditor.new(default)
|
|
69
69
|
@on_submit = on_submit
|
|
70
|
-
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Back-compat accessor for the entered text.
|
|
73
|
+
def input_value
|
|
74
|
+
@editor.text
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def input_value=(value)
|
|
78
|
+
@editor.text = value
|
|
71
79
|
end
|
|
72
80
|
|
|
73
81
|
def display_text
|
|
74
|
-
|
|
75
|
-
"#{@text}: #{@input_value}#{cursor}"
|
|
82
|
+
"#{@text}: #{@editor.text}_"
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
def handle_key(ch)
|
|
79
86
|
case ch
|
|
80
87
|
when *Keys::ENTERS
|
|
81
|
-
@on_submit&.call(@
|
|
82
|
-
true
|
|
88
|
+
@on_submit&.call(@editor.text)
|
|
83
89
|
when *Keys::BACKSPACES
|
|
84
|
-
|
|
85
|
-
@input_value[@cursor_pos - 1] = ''
|
|
86
|
-
@cursor_pos -= 1
|
|
87
|
-
end
|
|
88
|
-
true
|
|
90
|
+
@editor.backspace
|
|
89
91
|
when Keys::LEFT
|
|
90
|
-
@
|
|
91
|
-
true
|
|
92
|
+
@editor.left
|
|
92
93
|
when Keys::RIGHT
|
|
93
|
-
@
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@input_value.insert(@cursor_pos, ch.chr)
|
|
97
|
-
@cursor_pos += 1
|
|
98
|
-
true
|
|
94
|
+
@editor.right
|
|
95
|
+
when Keys::SPACE..(Keys::DEL_ASCII - 1) # Printable ASCII
|
|
96
|
+
@editor.insert(ch.chr)
|
|
99
97
|
else
|
|
100
|
-
false
|
|
98
|
+
return false
|
|
101
99
|
end
|
|
100
|
+
true
|
|
102
101
|
end
|
|
103
102
|
end
|
|
104
103
|
|
data/lib/potty/widgets/panel.rb
CHANGED
|
@@ -41,7 +41,7 @@ module Potty
|
|
|
41
41
|
def render(window)
|
|
42
42
|
return unless @visible && @rect
|
|
43
43
|
|
|
44
|
-
Border.draw(window, @rect, style: @style, attr: theme
|
|
44
|
+
Border.draw(window, @rect, style: @style, attr: theme.style(@color), title: @title)
|
|
45
45
|
super # render children inside the frame
|
|
46
46
|
end
|
|
47
47
|
end
|
|
@@ -41,6 +41,14 @@ module Potty
|
|
|
41
41
|
@selected
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# The value under the cursor (the highlighted row), which may differ
|
|
45
|
+
# from the committed selection while navigating. Choosers commit this
|
|
46
|
+
# on Enter.
|
|
47
|
+
def cursor_value
|
|
48
|
+
opt = @options[@cursor]
|
|
49
|
+
opt && opt[:value]
|
|
50
|
+
end
|
|
51
|
+
|
|
44
52
|
def selected=(value)
|
|
45
53
|
idx = index_of(value)
|
|
46
54
|
return unless idx
|
|
@@ -77,7 +85,7 @@ module Potty
|
|
|
77
85
|
|
|
78
86
|
marker = opt[:value] == @selected ? "(\u25CF)" : "(\u25CB)"
|
|
79
87
|
on_cursor = @focused && i == @cursor
|
|
80
|
-
attr = on_cursor ? theme.
|
|
88
|
+
attr = on_cursor ? theme.style(:selected, bold: true) : theme.style(:normal)
|
|
81
89
|
text = "#{marker} #{opt[:label]}"[0, @rect.width]
|
|
82
90
|
|
|
83
91
|
window.setpos(@rect.y + i, @rect.x)
|