reterm 0.4.2 → 0.5.0
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/designer/src/ComponentParams.rb +89 -42
- data/designer/src/ComponentsList.rb +26 -25
- data/designer/src/CreatedList.rb +9 -0
- data/designer/src/Designer.rb +23 -1
- data/designer/src/ToggleArea.rb +20 -3
- data/designer/src/about.rb +35 -0
- data/designer/src/component_factory.rb +18 -0
- data/designer/src/component_map.rb +7 -0
- data/designer/src/file_chooser.rb +25 -0
- data/designer/src/glade/Designer.glade +1 -19
- data/designer/src/images/{slist.png → scroll_list.png} +0 -0
- data/lib/reterm.rb +10 -1
- data/lib/reterm/color_pair.rb +166 -10
- data/lib/reterm/component.rb +89 -2
- data/lib/reterm/components.rb +16 -1
- data/lib/reterm/components/alphalist.rb +45 -0
- data/lib/reterm/components/ascii_text.rb +14 -1
- data/lib/reterm/components/asciimator.rb +1 -0
- data/lib/reterm/components/button.rb +35 -5
- data/lib/reterm/components/button_box.rb +108 -0
- data/lib/reterm/components/close_button.rb +22 -0
- data/lib/reterm/components/cmd_output.rb +69 -0
- data/lib/reterm/components/dial.rb +11 -2
- data/lib/reterm/components/dialog.rb +41 -1
- data/lib/reterm/components/drop_down_menu.rb +140 -0
- data/lib/reterm/components/entry.rb +42 -14
- data/lib/reterm/components/histogram.rb +55 -0
- data/lib/reterm/components/hslider.rb +9 -0
- data/lib/reterm/components/image.rb +9 -0
- data/lib/reterm/components/isometric.rb +38 -0
- data/lib/reterm/components/label.rb +20 -3
- data/lib/reterm/components/matrix.rb +9 -0
- data/lib/reterm/components/multi_line_entry.rb +54 -0
- data/lib/reterm/components/password_entry.rb +15 -0
- data/lib/reterm/components/radio.rb +10 -0
- data/lib/reterm/components/revealing_label.rb +126 -0
- data/lib/reterm/components/rocker.rb +21 -11
- data/lib/reterm/components/scroll_list.rb +96 -0
- data/lib/reterm/components/scrolling_area.rb +50 -0
- data/lib/reterm/components/select_list.rb +67 -0
- data/lib/reterm/components/splash.rb +85 -0
- data/lib/reterm/components/template.rb +12 -1
- data/lib/reterm/components/treeview.rb +1 -0
- data/lib/reterm/components/vslider.rb +11 -2
- data/lib/reterm/components/youtube.rb +20 -0
- data/lib/reterm/config.rb +22 -0
- data/lib/reterm/init.rb +131 -6
- data/lib/reterm/layout.rb +147 -11
- data/lib/reterm/layouts.rb +1 -0
- data/lib/reterm/layouts/grid.rb +69 -0
- data/lib/reterm/layouts/horizontal.rb +25 -4
- data/lib/reterm/layouts/vertical.rb +26 -5
- data/lib/reterm/loader.rb +2 -2
- data/lib/reterm/mixins/button_helpers.rb +7 -0
- data/lib/reterm/mixins/cdk_component.rb +66 -2
- data/lib/reterm/mixins/common_controls.rb +15 -0
- data/lib/reterm/mixins/common_keys.rb +20 -0
- data/lib/reterm/mixins/component_input.rb +62 -14
- data/lib/reterm/mixins/event_dispatcher.rb +2 -0
- data/lib/reterm/mixins/item_helpers.rb +8 -0
- data/lib/reterm/mixins/key_bindings.rb +23 -0
- data/lib/reterm/mixins/log_helpers.rb +13 -0
- data/lib/reterm/mixins/mouse_input.rb +58 -0
- data/lib/reterm/mixins/nav_controls.rb +33 -0
- data/lib/reterm/mixins/nav_input.rb +161 -69
- data/lib/reterm/terminal.rb +6 -2
- data/lib/reterm/util.rb +121 -0
- data/lib/reterm/version.rb +1 -1
- data/lib/reterm/window.rb +295 -29
- metadata +33 -17
- data/designer/src/images/orig/Check.png +0 -0
- data/designer/src/images/orig/ascii_text.png +0 -0
- data/designer/src/images/orig/button.png +0 -0
- data/designer/src/images/orig/dial.png +0 -0
- data/designer/src/images/orig/entry.png +0 -0
- data/designer/src/images/orig/hslider.png +0 -0
- data/designer/src/images/orig/label.png +0 -0
- data/designer/src/images/orig/matrix.png +0 -0
- data/designer/src/images/orig/radio.png +0 -0
- data/designer/src/images/orig/rocker.png +0 -0
- data/designer/src/images/orig/slist.png +0 -0
- data/designer/src/images/orig/vslider.png +0 -0
- data/lib/reterm/components/slist.rb +0 -32
- data/lib/reterm/menu.rb +0 -81
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module RETerm
|
4
|
+
class Config
|
5
|
+
CONFIG_FILE = File.expand_path("~/.reterm")
|
6
|
+
|
7
|
+
def self.get
|
8
|
+
@config ||=
|
9
|
+
begin
|
10
|
+
return {} unless File.exist?(CONFIG_FILE)
|
11
|
+
JSON.parse(File.read(CONFIG_FILE))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.load_plugins
|
16
|
+
return unless get.key?("plugins")
|
17
|
+
get["plugins"].each { |p|
|
18
|
+
require p
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end # class Config
|
22
|
+
end # module RETerm
|
data/lib/reterm/init.rb
CHANGED
@@ -2,6 +2,10 @@ require 'ncursesw'
|
|
2
2
|
require_relative './resize'
|
3
3
|
|
4
4
|
module RETerm
|
5
|
+
SYNC_TIMEOUT = 150 # in milliseconds if enabled
|
6
|
+
|
7
|
+
###############################
|
8
|
+
|
5
9
|
# Initializes the RETerm subsystem before invoking block.
|
6
10
|
# After block is finished regardless of execution state
|
7
11
|
# (return, exception, etc) this will cleanup the terminal
|
@@ -17,6 +21,11 @@ module RETerm
|
|
17
21
|
# }
|
18
22
|
#
|
19
23
|
def init_reterm(opts={}, &bl)
|
24
|
+
@@shutdown = false
|
25
|
+
@@reterm_opts = opts
|
26
|
+
|
27
|
+
err = nil
|
28
|
+
|
20
29
|
begin
|
21
30
|
Terminal.load # XXX: not sure why but loading terminal
|
22
31
|
# info after changing terminal settings via
|
@@ -25,25 +34,43 @@ module RETerm
|
|
25
34
|
# without a 'reset' (see below).
|
26
35
|
# So just preload terminal info here.
|
27
36
|
|
37
|
+
# shorten up the esc delay time
|
38
|
+
ENV["ESCDELAY"] = 100.to_s
|
39
|
+
|
28
40
|
stdscr = Ncurses::initscr
|
29
41
|
Ncurses::start_color
|
30
42
|
Ncurses::noecho
|
31
43
|
Ncurses::cbreak
|
32
|
-
|
44
|
+
disable_cursor! unless !!opts[:cursor]
|
33
45
|
Ncurses::keypad(stdscr, true)
|
34
46
|
|
47
|
+
no_mouse = opts[:nomouse] || (opts.key?(:mouse) && !opts[:mouse])
|
48
|
+
Ncurses::mousemask(MouseInput::ALL_EVENTS, []) unless no_mouse
|
49
|
+
|
35
50
|
track_resize if opts[:resize]
|
36
51
|
|
37
52
|
bl.call
|
38
53
|
|
39
|
-
|
54
|
+
rescue => e
|
55
|
+
err = e
|
56
|
+
|
57
|
+
ensure
|
40
58
|
stop_track_resize
|
41
59
|
Ncurses.curs_set(1)
|
42
|
-
Window.
|
60
|
+
Window.top.each { |w| w.finalize! }
|
43
61
|
CDK::SCREEN.endCDK if cdk_enabled?
|
44
|
-
Ncurses.endwin
|
62
|
+
Ncurses.endwin
|
45
63
|
#`reset -Q` # XXX only way to guarantee a full reset (see above)
|
46
64
|
end
|
65
|
+
|
66
|
+
if err
|
67
|
+
puts "Unhandled error:"
|
68
|
+
puts " " + err.to_s
|
69
|
+
puts err.backtrace
|
70
|
+
.collect { |b|
|
71
|
+
" " + b
|
72
|
+
}.join("\n")
|
73
|
+
end
|
47
74
|
end
|
48
75
|
|
49
76
|
# This method resyncs the visible terminal with the internal
|
@@ -58,18 +85,74 @@ module RETerm
|
|
58
85
|
# win.getch # wait for input
|
59
86
|
# }
|
60
87
|
#
|
61
|
-
def update_reterm
|
62
|
-
|
88
|
+
def update_reterm(force_refresh=false)
|
89
|
+
if force_refresh
|
90
|
+
Window.top.each { |w|
|
91
|
+
w.erase
|
92
|
+
w.draw!
|
93
|
+
}
|
94
|
+
else
|
95
|
+
Window.top.each { |w| w.noutrefresh }
|
96
|
+
end
|
97
|
+
|
63
98
|
Ncurses::Panel.update_panels
|
64
99
|
Ncurses.doupdate
|
65
100
|
end
|
66
101
|
|
102
|
+
# Return copy of options specified to init_reterm
|
103
|
+
def reterm_opts
|
104
|
+
@@_reterm_opts ||= Hash[@@reterm_opts]
|
105
|
+
end
|
106
|
+
|
67
107
|
# XXX Ncurses is exposed so that users
|
68
108
|
# may employ any constants if desired.
|
69
109
|
# This should not be needed though and
|
70
110
|
# is discouraged to maintain portability
|
71
111
|
NC = Ncurses
|
72
112
|
|
113
|
+
###
|
114
|
+
|
115
|
+
# Cursor
|
116
|
+
|
117
|
+
def cursor?
|
118
|
+
!(@@cursor_disabled ||= false)
|
119
|
+
end
|
120
|
+
|
121
|
+
def disable_cursor!
|
122
|
+
# store cursor state
|
123
|
+
o = cursor?
|
124
|
+
|
125
|
+
# set cursor state
|
126
|
+
@@cursor_disabled = true
|
127
|
+
Ncurses::curs_set(0)
|
128
|
+
|
129
|
+
# invoke block if given
|
130
|
+
return unless block_given?
|
131
|
+
yield
|
132
|
+
|
133
|
+
# restore cursor state after block
|
134
|
+
enable_cursor! if o
|
135
|
+
end
|
136
|
+
|
137
|
+
def enable_cursor!
|
138
|
+
# store cursor state
|
139
|
+
o = cursor?
|
140
|
+
|
141
|
+
# set cursor state
|
142
|
+
@@cursor_disabled = false
|
143
|
+
Ncurses::curs_set(1)
|
144
|
+
|
145
|
+
# invoke block if given
|
146
|
+
return unless block_given?
|
147
|
+
yield
|
148
|
+
|
149
|
+
# restore cursor state after block
|
150
|
+
disable_cursor! unless o
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
###############################
|
155
|
+
|
73
156
|
# Enable the CDK subsystem. Used by CDKbased components
|
74
157
|
#
|
75
158
|
# @see #cdk_enabled?
|
@@ -92,4 +175,46 @@ module RETerm
|
|
92
175
|
def cdk_enabled?
|
93
176
|
!!@cdk_enabled
|
94
177
|
end
|
178
|
+
|
179
|
+
###############################
|
180
|
+
|
181
|
+
# Enables the input timeout and component syncronization.
|
182
|
+
#
|
183
|
+
# Used internally by components that need to periodically
|
184
|
+
# be updated outside of user input.
|
185
|
+
def activate_sync!
|
186
|
+
@@sync_activated = true
|
187
|
+
Window.all.each { |w|
|
188
|
+
w.win.timeout(SYNC_TIMEOUT)
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
# Boonean indicating if component syncronization is
|
193
|
+
# enabled
|
194
|
+
def sync_enabled?
|
195
|
+
defined?(@@sync_activated) && !!@@sync_activated
|
196
|
+
end
|
197
|
+
|
198
|
+
# Run the sync process, used internally
|
199
|
+
def run_sync!
|
200
|
+
return unless sync_enabled?
|
201
|
+
|
202
|
+
Window.all.each { |w|
|
203
|
+
w.sync!
|
204
|
+
}
|
205
|
+
|
206
|
+
update_reterm
|
207
|
+
end
|
208
|
+
|
209
|
+
###############################
|
210
|
+
|
211
|
+
# Use to halt all operation and cleanup.
|
212
|
+
def shutdown!
|
213
|
+
@@shutdown = true
|
214
|
+
end
|
215
|
+
|
216
|
+
# Boolean indicating if app in being halted
|
217
|
+
def shutdown?
|
218
|
+
!!@@shutdown
|
219
|
+
end
|
95
220
|
end # module RETerm
|
data/lib/reterm/layout.rb
CHANGED
@@ -7,57 +7,193 @@ module RETerm
|
|
7
7
|
class Layout < Component
|
8
8
|
include NavInput
|
9
9
|
|
10
|
-
#
|
10
|
+
# Flag indicating if layout should expand
|
11
|
+
# to be able to contain childen (if false
|
12
|
+
# error will be throw if trying to add
|
13
|
+
# child to layout that cannot contain it)
|
14
|
+
attr_accessor :expand
|
15
|
+
|
16
|
+
def expandable?
|
17
|
+
(!defined?(@expand) || !!@expand) &&
|
18
|
+
(!parent? || parent.expandable?)
|
19
|
+
end
|
20
|
+
|
21
|
+
def children
|
22
|
+
window.children.collect { |w| w.component }
|
23
|
+
end
|
24
|
+
|
25
|
+
def empty?
|
26
|
+
return true if window.nil?
|
27
|
+
window.children.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def child_windows
|
31
|
+
window.children
|
32
|
+
end
|
33
|
+
|
34
|
+
def parent?
|
35
|
+
window.parent?
|
36
|
+
end
|
37
|
+
|
38
|
+
def parent
|
39
|
+
window.parent.component
|
40
|
+
end
|
11
41
|
|
12
|
-
|
42
|
+
# TODO padding / margin support
|
13
43
|
|
14
|
-
|
15
|
-
|
44
|
+
# Subclasses should override this method returning
|
45
|
+
# current rows in layout
|
46
|
+
def current_rows
|
47
|
+
raise "NotImplemented"
|
16
48
|
end
|
17
49
|
|
18
50
|
# Subclasses should override this method returning
|
19
|
-
#
|
20
|
-
def
|
51
|
+
# current cols in layout
|
52
|
+
def current_cols
|
21
53
|
raise "NotImplemented"
|
22
54
|
end
|
23
55
|
|
56
|
+
# Subclasses should overrid this method returning
|
57
|
+
# boolean indicating if boundries will be
|
58
|
+
# exceeded when child is added
|
59
|
+
def exceeds_bounds_with?(child)
|
60
|
+
raise "NotImplemented"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns boolean indicating if current layout
|
64
|
+
# exceeds bounds of window
|
65
|
+
def exceeds_bounds?
|
66
|
+
current_cols > window.cols ||
|
67
|
+
current_rows > window.rows
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
# Return layout containing component
|
72
|
+
# If coordinates are contained in a child in current layout
|
73
|
+
def layout_containing(component)
|
74
|
+
return self if children.include?(component)
|
75
|
+
|
76
|
+
found = nil
|
77
|
+
children.each { |c|
|
78
|
+
next if found
|
79
|
+
|
80
|
+
if c.kind_of?(Layout)
|
81
|
+
found = c unless c.layout_containing(component).nil?
|
82
|
+
end
|
83
|
+
}
|
84
|
+
|
85
|
+
found
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return boolean indicating if layout contains specified child
|
89
|
+
def contains?(child)
|
90
|
+
children.any? { |c|
|
91
|
+
(c.kind_of?(Layout) && c.contains?(child)) || c == child
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
24
95
|
# Return boolean indicating if any child component
|
25
96
|
# is activatable
|
26
97
|
def activatable?
|
27
|
-
|
98
|
+
return false if empty?
|
99
|
+
children.any? { |c| c.activatable? }
|
100
|
+
end
|
101
|
+
|
102
|
+
def highlight_focus?
|
103
|
+
false
|
28
104
|
end
|
29
105
|
|
30
106
|
# Create new child window and add it to layout
|
31
107
|
def add_child(h={})
|
108
|
+
c = nil
|
109
|
+
|
110
|
+
if h.key?(:component)
|
111
|
+
c = h[:component]
|
112
|
+
|
113
|
+
h = {:rows => c.requested_rows + c.extra_padding,
|
114
|
+
:cols => c.requested_cols + c.extra_padding}.merge(h)
|
115
|
+
end
|
116
|
+
|
117
|
+
raise ArgumentError, "must specify x/y" unless h.key?(:x) &&
|
118
|
+
h.key?(:y)
|
119
|
+
|
120
|
+
raise ArgumentError, "must specify rows/cols" unless h.key?(:rows) &&
|
121
|
+
h.key?(:cols)
|
122
|
+
|
123
|
+
h[:rows], h[:cols] = *Window.adjust_proportional(window, h[:rows], h[:cols])
|
124
|
+
h[:x], h[:y] = *Window.align(window, h[:x], h[:y], h[:rows], h[:cols])
|
125
|
+
h[:rows], h[:cols] = *Window.fill_parent(parent? ? parent.window : Terminal,
|
126
|
+
h[:x], h[:y],
|
127
|
+
h[:rows], h[:cols]) if h[:fill]
|
128
|
+
|
129
|
+
if exceeds_bounds_with?(h)
|
130
|
+
if expandable? # ... && can_expand_to?(h)
|
131
|
+
expand(h)
|
132
|
+
|
133
|
+
else
|
134
|
+
raise ArgumentError, "child exceeds bounds"
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
32
139
|
child = window.create_child(h)
|
33
140
|
|
141
|
+
# TODO need to reverse expansion if operation fails at any
|
142
|
+
# point on, or verify expandable before create_child but
|
143
|
+
# do not expand until after
|
144
|
+
|
34
145
|
if child.win.nil?
|
35
146
|
raise ArgumentError, "could not create child window"
|
36
147
|
end
|
37
148
|
|
38
|
-
if exceeds_bounds?
|
149
|
+
if exceeds_bounds?
|
39
150
|
window.del_child(child) unless child.win.nil?
|
40
151
|
raise ArgumentError, "child exceeds bounds"
|
41
152
|
end
|
42
153
|
|
43
|
-
|
154
|
+
child.component = c unless c.nil?
|
155
|
+
|
44
156
|
update_reterm
|
45
157
|
child
|
46
158
|
end
|
47
159
|
|
160
|
+
def expand(h)
|
161
|
+
nr = [current_rows, h[:y] + h[:rows]].max
|
162
|
+
nc = [current_cols, h[:x] + h[:cols]].max
|
163
|
+
|
164
|
+
# verify parent can contain expanded area and is expandable
|
165
|
+
if parent?
|
166
|
+
if parent.exceeds_bounds_with?(h)
|
167
|
+
raise ArgumentError, "cannot expand parent" unless parent.expandable?
|
168
|
+
|
169
|
+
# expand parent
|
170
|
+
parent.expand(h)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# perform actual window expansion
|
175
|
+
window.resize(nr+1, nc+1)
|
176
|
+
|
177
|
+
# FIXME need to expand children who are marked as 'fill'
|
178
|
+
end
|
179
|
+
|
48
180
|
# Draw all layout children
|
49
181
|
def draw!
|
50
182
|
children.each { |c| c.draw! }
|
183
|
+
draw_focus!
|
51
184
|
end
|
52
185
|
|
53
186
|
# Activates layout by dispatching to navigation
|
54
187
|
# input system.
|
55
188
|
#
|
56
189
|
# @see NavInput
|
57
|
-
def activate!
|
190
|
+
def activate!(*input)
|
191
|
+
reactivate!
|
192
|
+
|
58
193
|
draw!
|
59
194
|
update_reterm
|
60
|
-
handle_input
|
195
|
+
handle_input(*input)
|
196
|
+
deactivate!
|
61
197
|
end
|
62
198
|
end # class Layout
|
63
199
|
end # module RETerm
|
data/lib/reterm/layouts.rb
CHANGED
@@ -0,0 +1,69 @@
|
|
1
|
+
module RETerm
|
2
|
+
module Layouts
|
3
|
+
# Layout which permits the user to arrainge items anywhere on screen
|
4
|
+
class Grid < Layout
|
5
|
+
def current_rows
|
6
|
+
return 1 if empty?
|
7
|
+
child_windows.max { |c1, c2| c1.rows <=> c2.rows }.rows + 1
|
8
|
+
end
|
9
|
+
|
10
|
+
def current_cols
|
11
|
+
return 1 if empty?
|
12
|
+
child_windows.max { |c1, c2| c1.cols <=> c2.cols }.cols + 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def exceeds_bounds_with?(child)
|
16
|
+
x1 = child.is_a?(Hash) ? child[:x] : child.x
|
17
|
+
y1 = child.is_a?(Hash) ? child[:y] : child.y
|
18
|
+
|
19
|
+
x2 = child.is_a?(Hash) ?
|
20
|
+
[current_cols, x1 + child[:cols]].compact.max :
|
21
|
+
[current_cols, x1 + child.cols].max
|
22
|
+
|
23
|
+
y2 = child.is_a?(Hash) ?
|
24
|
+
[current_rows, y1 + child[:rows]].compact.max :
|
25
|
+
[current_rows, y1 + child.rows].max
|
26
|
+
|
27
|
+
x1 < window.x || y1 < window.y ||
|
28
|
+
x2 > window.cols || y2 > window.rows
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid_input?(ch, from_parent)
|
32
|
+
if UP_CONTROLS.include?(ch)
|
33
|
+
return focusable.any? { |f| f.window.y < focused.window.y }
|
34
|
+
|
35
|
+
elsif DOWN_CONTROLS.include?(ch)
|
36
|
+
return focusable.any? { |f| f.window.y > focused.window.y }
|
37
|
+
|
38
|
+
elsif LEFT_CONTROLS.include?(ch)
|
39
|
+
focusable.any? { |f| f.window.x < focused.window.x }
|
40
|
+
|
41
|
+
elsif RIGHT_CONTROLS.include?(ch)
|
42
|
+
focusable.any? { |f| f.window.x > focused.window.x }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Cycle through components child position on grid
|
47
|
+
def next_focus(ch)
|
48
|
+
f = nil
|
49
|
+
if UP_CONTROLS.include?(ch)
|
50
|
+
f = focusable.select { |f| f.window.y < focused.window.y }.first
|
51
|
+
|
52
|
+
elsif DOWN_CONTROLS.include?(ch)
|
53
|
+
f = focusable.select { |f| f.window.y > focused.window.y }.first
|
54
|
+
|
55
|
+
elsif LEFT_CONTROLS.include?(ch)
|
56
|
+
f = focusable.select { |f| f.window.x < focused.window.x }.first
|
57
|
+
|
58
|
+
elsif RIGHT_CONTROLS.include?(ch)
|
59
|
+
f = focusable.select { |f| f.window.x > focused.window.x }.first
|
60
|
+
|
61
|
+
else
|
62
|
+
return super
|
63
|
+
end
|
64
|
+
|
65
|
+
focusable.index(f)
|
66
|
+
end
|
67
|
+
end # class Grid
|
68
|
+
end # module Layouts
|
69
|
+
end # module RETerm
|