@akiojin/gwt 9.7.0 → 9.8.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.
- package/README.ja.md +37 -6
- package/README.md +38 -8
- package/assets/icons/icon.icns +0 -0
- package/assets/icons/icon.ico +0 -0
- package/assets/icons/icon.png +0 -0
- package/assets/release-assets.json +18 -0
- package/docker-compose.yml +2 -2
- package/package.json +6 -3
- package/scripts/check-release-flow.sh +17 -0
- package/scripts/release-assets.cjs +11 -0
- package/scripts/test-all.sh +15 -1
- package/scripts/test_release_assets.cjs +67 -0
- package/vendor/vt100/LICENSE +21 -0
- package/vendor/vt100/src/attrs.rs +144 -0
- package/vendor/vt100/src/callbacks.rs +69 -0
- package/vendor/vt100/src/cell.rs +179 -0
- package/vendor/vt100/src/grid.rs +742 -0
- package/vendor/vt100/src/lib.rs +64 -0
- package/vendor/vt100/src/parser.rs +96 -0
- package/vendor/vt100/src/perform.rs +277 -0
- package/vendor/vt100/src/row.rs +488 -0
- package/vendor/vt100/src/screen.rs +1354 -0
- package/vendor/vt100/src/term.rs +551 -0
- package/wix/main.wxs +40 -5
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//! This crate parses a terminal byte stream and provides an in-memory
|
|
2
|
+
//! representation of the rendered contents.
|
|
3
|
+
//!
|
|
4
|
+
//! # Overview
|
|
5
|
+
//!
|
|
6
|
+
//! This is essentially the terminal parser component of a graphical terminal
|
|
7
|
+
//! emulator pulled out into a separate crate. Although you can use this crate
|
|
8
|
+
//! to build a graphical terminal emulator, it also contains functionality
|
|
9
|
+
//! necessary for implementing terminal applications that want to run other
|
|
10
|
+
//! terminal applications - programs like `screen` or `tmux` for example.
|
|
11
|
+
//!
|
|
12
|
+
//! # Synopsis
|
|
13
|
+
//!
|
|
14
|
+
//! ```
|
|
15
|
+
//! let mut parser = vt100::Parser::new(24, 80, 0);
|
|
16
|
+
//!
|
|
17
|
+
//! let screen = parser.screen().clone();
|
|
18
|
+
//! parser.process(b"this text is \x1b[31mRED\x1b[m");
|
|
19
|
+
//! assert_eq!(
|
|
20
|
+
//! parser.screen().cell(0, 13).unwrap().fgcolor(),
|
|
21
|
+
//! vt100::Color::Idx(1),
|
|
22
|
+
//! );
|
|
23
|
+
//!
|
|
24
|
+
//! let screen = parser.screen().clone();
|
|
25
|
+
//! parser.process(b"\x1b[3D\x1b[32mGREEN");
|
|
26
|
+
//! assert_eq!(
|
|
27
|
+
//! parser.screen().contents_formatted(),
|
|
28
|
+
//! &b"\x1b[?25h\x1b[m\x1b[H\x1b[Jthis text is \x1b[32mGREEN"[..],
|
|
29
|
+
//! );
|
|
30
|
+
//! assert_eq!(
|
|
31
|
+
//! parser.screen().contents_diff(&screen),
|
|
32
|
+
//! &b"\x1b[1;14H\x1b[32mGREEN"[..],
|
|
33
|
+
//! );
|
|
34
|
+
//! ```
|
|
35
|
+
|
|
36
|
+
#![warn(missing_docs)]
|
|
37
|
+
#![warn(clippy::cargo)]
|
|
38
|
+
#![warn(clippy::pedantic)]
|
|
39
|
+
#![warn(clippy::nursery)]
|
|
40
|
+
#![warn(clippy::as_conversions)]
|
|
41
|
+
#![warn(clippy::get_unwrap)]
|
|
42
|
+
#![allow(clippy::cognitive_complexity)]
|
|
43
|
+
#![allow(clippy::missing_const_for_fn)]
|
|
44
|
+
#![allow(clippy::similar_names)]
|
|
45
|
+
#![allow(clippy::struct_excessive_bools)]
|
|
46
|
+
#![allow(clippy::too_many_arguments)]
|
|
47
|
+
#![allow(clippy::too_many_lines)]
|
|
48
|
+
#![allow(clippy::type_complexity)]
|
|
49
|
+
|
|
50
|
+
mod attrs;
|
|
51
|
+
mod callbacks;
|
|
52
|
+
mod cell;
|
|
53
|
+
mod grid;
|
|
54
|
+
mod parser;
|
|
55
|
+
mod perform;
|
|
56
|
+
mod row;
|
|
57
|
+
mod screen;
|
|
58
|
+
mod term;
|
|
59
|
+
|
|
60
|
+
pub use attrs::Color;
|
|
61
|
+
pub use callbacks::Callbacks;
|
|
62
|
+
pub use cell::Cell;
|
|
63
|
+
pub use parser::Parser;
|
|
64
|
+
pub use screen::{MouseProtocolEncoding, MouseProtocolMode, Screen};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/// A parser for terminal output which produces an in-memory representation of
|
|
2
|
+
/// the terminal contents.
|
|
3
|
+
pub struct Parser<CB: crate::callbacks::Callbacks = ()> {
|
|
4
|
+
parser: vte::Parser,
|
|
5
|
+
screen: crate::perform::WrappedScreen<CB>,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
impl Parser {
|
|
9
|
+
/// Creates a new terminal parser of the given size and with the given
|
|
10
|
+
/// amount of scrollback.
|
|
11
|
+
#[must_use]
|
|
12
|
+
pub fn new(rows: u16, cols: u16, scrollback_len: usize) -> Self {
|
|
13
|
+
Self {
|
|
14
|
+
parser: vte::Parser::new(),
|
|
15
|
+
screen: crate::perform::WrappedScreen::new(
|
|
16
|
+
rows,
|
|
17
|
+
cols,
|
|
18
|
+
scrollback_len,
|
|
19
|
+
),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl<CB: crate::callbacks::Callbacks> Parser<CB> {
|
|
25
|
+
/// Creates a new terminal parser of the given size and with the given
|
|
26
|
+
/// amount of scrollback. Terminal events will be reported via method
|
|
27
|
+
/// calls on the provided [`Callbacks`](crate::callbacks::Callbacks)
|
|
28
|
+
/// implementation.
|
|
29
|
+
pub fn new_with_callbacks(
|
|
30
|
+
rows: u16,
|
|
31
|
+
cols: u16,
|
|
32
|
+
scrollback_len: usize,
|
|
33
|
+
callbacks: CB,
|
|
34
|
+
) -> Self {
|
|
35
|
+
Self {
|
|
36
|
+
parser: vte::Parser::new(),
|
|
37
|
+
screen: crate::perform::WrappedScreen::new_with_callbacks(
|
|
38
|
+
rows,
|
|
39
|
+
cols,
|
|
40
|
+
scrollback_len,
|
|
41
|
+
callbacks,
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Processes the contents of the given byte string, and updates the
|
|
47
|
+
/// in-memory terminal state.
|
|
48
|
+
pub fn process(&mut self, bytes: &[u8]) {
|
|
49
|
+
self.parser.advance(&mut self.screen, bytes);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Returns a reference to a [`Screen`](crate::Screen) object containing
|
|
53
|
+
/// the terminal state.
|
|
54
|
+
#[must_use]
|
|
55
|
+
pub fn screen(&self) -> &crate::Screen {
|
|
56
|
+
&self.screen.screen
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Returns a mutable reference to a [`Screen`](crate::Screen) object
|
|
60
|
+
/// containing the terminal state.
|
|
61
|
+
#[must_use]
|
|
62
|
+
pub fn screen_mut(&mut self) -> &mut crate::Screen {
|
|
63
|
+
&mut self.screen.screen
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Returns a reference to the [`Callbacks`](crate::callbacks::Callbacks)
|
|
67
|
+
/// state object passed into the constructor.
|
|
68
|
+
pub fn callbacks(&self) -> &CB {
|
|
69
|
+
&self.screen.callbacks
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Returns a mutable reference to the
|
|
73
|
+
/// [`Callbacks`](crate::callbacks::Callbacks) state object passed into
|
|
74
|
+
/// the constructor.
|
|
75
|
+
pub fn callbacks_mut(&mut self) -> &mut CB {
|
|
76
|
+
&mut self.screen.callbacks
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl Default for Parser {
|
|
81
|
+
/// Returns a parser with dimensions 80x24 and no scrollback.
|
|
82
|
+
fn default() -> Self {
|
|
83
|
+
Self::new(24, 80, 0)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
impl std::io::Write for Parser {
|
|
88
|
+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
|
89
|
+
self.process(buf);
|
|
90
|
+
Ok(buf.len())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn flush(&mut self) -> std::io::Result<()> {
|
|
94
|
+
Ok(())
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
const BASE64: &[u8] =
|
|
2
|
+
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
3
|
+
const CLIPBOARD_SELECTOR: &[u8] = b"cpqs01234567";
|
|
4
|
+
|
|
5
|
+
pub struct WrappedScreen<CB: crate::callbacks::Callbacks = ()> {
|
|
6
|
+
pub screen: crate::screen::Screen,
|
|
7
|
+
pub callbacks: CB,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
impl WrappedScreen<()> {
|
|
11
|
+
pub fn new(rows: u16, cols: u16, scrollback_len: usize) -> Self {
|
|
12
|
+
Self::new_with_callbacks(rows, cols, scrollback_len, ())
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl<CB: crate::callbacks::Callbacks> WrappedScreen<CB> {
|
|
17
|
+
pub fn new_with_callbacks(
|
|
18
|
+
rows: u16,
|
|
19
|
+
cols: u16,
|
|
20
|
+
scrollback_len: usize,
|
|
21
|
+
callbacks: CB,
|
|
22
|
+
) -> Self {
|
|
23
|
+
Self {
|
|
24
|
+
screen: crate::screen::Screen::new(
|
|
25
|
+
crate::grid::Size { rows, cols },
|
|
26
|
+
scrollback_len,
|
|
27
|
+
),
|
|
28
|
+
callbacks,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl<CB: crate::callbacks::Callbacks> vte::Perform for WrappedScreen<CB> {
|
|
34
|
+
fn print(&mut self, c: char) {
|
|
35
|
+
if c == '\u{fffd}' || ('\u{80}'..'\u{a0}').contains(&c) {
|
|
36
|
+
self.callbacks.unhandled_char(&mut self.screen, c);
|
|
37
|
+
} else {
|
|
38
|
+
self.screen.text(c);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn execute(&mut self, b: u8) {
|
|
43
|
+
match b {
|
|
44
|
+
7 => self.callbacks.audible_bell(&mut self.screen),
|
|
45
|
+
8 => self.screen.bs(),
|
|
46
|
+
9 => self.screen.tab(),
|
|
47
|
+
10 => self.screen.lf(),
|
|
48
|
+
11 => self.screen.vt(),
|
|
49
|
+
12 => self.screen.ff(),
|
|
50
|
+
13 => self.screen.cr(),
|
|
51
|
+
// we don't implement shift in/out alternate character sets, but
|
|
52
|
+
// it shouldn't count as an "error"
|
|
53
|
+
14 | 15 => {}
|
|
54
|
+
_ => self.callbacks.unhandled_control(&mut self.screen, b),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, b: u8) {
|
|
59
|
+
if let Some(i) = intermediates.first() {
|
|
60
|
+
self.callbacks.unhandled_escape(
|
|
61
|
+
&mut self.screen,
|
|
62
|
+
Some(*i),
|
|
63
|
+
intermediates.get(1).copied(),
|
|
64
|
+
b,
|
|
65
|
+
);
|
|
66
|
+
} else {
|
|
67
|
+
match b {
|
|
68
|
+
b'7' => self.screen.decsc(),
|
|
69
|
+
b'8' => self.screen.decrc(),
|
|
70
|
+
b'=' => self.screen.deckpam(),
|
|
71
|
+
b'>' => self.screen.deckpnm(),
|
|
72
|
+
b'M' => self.screen.ri(),
|
|
73
|
+
b'c' => self.screen.ris(),
|
|
74
|
+
b'g' => self.callbacks.visual_bell(&mut self.screen),
|
|
75
|
+
_ => {
|
|
76
|
+
self.callbacks.unhandled_escape(
|
|
77
|
+
&mut self.screen,
|
|
78
|
+
None,
|
|
79
|
+
None,
|
|
80
|
+
b,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn csi_dispatch(
|
|
88
|
+
&mut self,
|
|
89
|
+
params: &vte::Params,
|
|
90
|
+
intermediates: &[u8],
|
|
91
|
+
_ignore: bool,
|
|
92
|
+
c: char,
|
|
93
|
+
) {
|
|
94
|
+
let unhandled = |screen: &mut crate::screen::Screen| {
|
|
95
|
+
self.callbacks.unhandled_csi(
|
|
96
|
+
screen,
|
|
97
|
+
intermediates.first().copied(),
|
|
98
|
+
intermediates.get(1).copied(),
|
|
99
|
+
¶ms.iter().collect::<Vec<_>>(),
|
|
100
|
+
c,
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
match intermediates.first() {
|
|
104
|
+
None => match c {
|
|
105
|
+
'@' => self.screen.ich(canonicalize_params_1(params, 1)),
|
|
106
|
+
'A' => self.screen.cuu(canonicalize_params_1(params, 1)),
|
|
107
|
+
'B' => self.screen.cud(canonicalize_params_1(params, 1)),
|
|
108
|
+
'C' => self.screen.cuf(canonicalize_params_1(params, 1)),
|
|
109
|
+
'D' => self.screen.cub(canonicalize_params_1(params, 1)),
|
|
110
|
+
'E' => self.screen.cnl(canonicalize_params_1(params, 1)),
|
|
111
|
+
'F' => self.screen.cpl(canonicalize_params_1(params, 1)),
|
|
112
|
+
'G' => self.screen.cha(canonicalize_params_1(params, 1)),
|
|
113
|
+
'H' => self.screen.cup(canonicalize_params_2(params, 1, 1)),
|
|
114
|
+
'J' => self
|
|
115
|
+
.screen
|
|
116
|
+
.ed(canonicalize_params_1(params, 0), unhandled),
|
|
117
|
+
'K' => self
|
|
118
|
+
.screen
|
|
119
|
+
.el(canonicalize_params_1(params, 0), unhandled),
|
|
120
|
+
'L' => self.screen.il(canonicalize_params_1(params, 1)),
|
|
121
|
+
'M' => self.screen.dl(canonicalize_params_1(params, 1)),
|
|
122
|
+
'P' => self.screen.dch(canonicalize_params_1(params, 1)),
|
|
123
|
+
'S' => self.screen.su(canonicalize_params_1(params, 1)),
|
|
124
|
+
'T' => self.screen.sd(canonicalize_params_1(params, 1)),
|
|
125
|
+
'X' => self.screen.ech(canonicalize_params_1(params, 1)),
|
|
126
|
+
'd' => self.screen.vpa(canonicalize_params_1(params, 1)),
|
|
127
|
+
'm' => self.screen.sgr(params, unhandled),
|
|
128
|
+
'r' => self.screen.decstbm(canonicalize_params_decstbm(
|
|
129
|
+
params,
|
|
130
|
+
self.screen.grid().size(),
|
|
131
|
+
)),
|
|
132
|
+
't' => {
|
|
133
|
+
let mut params_iter = params.iter();
|
|
134
|
+
let op =
|
|
135
|
+
params_iter.next().and_then(|x| x.first().copied());
|
|
136
|
+
if op == Some(8) {
|
|
137
|
+
let (screen_rows, screen_cols) = self.screen.size();
|
|
138
|
+
let rows =
|
|
139
|
+
params_iter.next().map_or(screen_rows, |x| {
|
|
140
|
+
*x.first().unwrap_or(&screen_rows)
|
|
141
|
+
});
|
|
142
|
+
let cols =
|
|
143
|
+
params_iter.next().map_or(screen_cols, |x| {
|
|
144
|
+
*x.first().unwrap_or(&screen_cols)
|
|
145
|
+
});
|
|
146
|
+
self.callbacks.resize(&mut self.screen, (rows, cols));
|
|
147
|
+
} else {
|
|
148
|
+
self.callbacks.unhandled_csi(
|
|
149
|
+
&mut self.screen,
|
|
150
|
+
None,
|
|
151
|
+
None,
|
|
152
|
+
¶ms.iter().collect::<Vec<_>>(),
|
|
153
|
+
c,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
_ => {
|
|
158
|
+
self.callbacks.unhandled_csi(
|
|
159
|
+
&mut self.screen,
|
|
160
|
+
None,
|
|
161
|
+
None,
|
|
162
|
+
¶ms.iter().collect::<Vec<_>>(),
|
|
163
|
+
c,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
Some(b'?') => match c {
|
|
168
|
+
'J' => self
|
|
169
|
+
.screen
|
|
170
|
+
.decsed(canonicalize_params_1(params, 0), unhandled),
|
|
171
|
+
'K' => self
|
|
172
|
+
.screen
|
|
173
|
+
.decsel(canonicalize_params_1(params, 0), unhandled),
|
|
174
|
+
'h' => self.screen.decset(params, unhandled),
|
|
175
|
+
'l' => self.screen.decrst(params, unhandled),
|
|
176
|
+
_ => {
|
|
177
|
+
self.callbacks.unhandled_csi(
|
|
178
|
+
&mut self.screen,
|
|
179
|
+
Some(b'?'),
|
|
180
|
+
intermediates.get(1).copied(),
|
|
181
|
+
¶ms.iter().collect::<Vec<_>>(),
|
|
182
|
+
c,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
Some(i) => {
|
|
187
|
+
self.callbacks.unhandled_csi(
|
|
188
|
+
&mut self.screen,
|
|
189
|
+
Some(*i),
|
|
190
|
+
intermediates.get(1).copied(),
|
|
191
|
+
¶ms.iter().collect::<Vec<_>>(),
|
|
192
|
+
c,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn osc_dispatch(&mut self, params: &[&[u8]], _bel_terminated: bool) {
|
|
199
|
+
match params {
|
|
200
|
+
[b"0", s] => {
|
|
201
|
+
self.callbacks.set_window_icon_name(&mut self.screen, s);
|
|
202
|
+
self.callbacks.set_window_title(&mut self.screen, s);
|
|
203
|
+
}
|
|
204
|
+
[b"1", s] => {
|
|
205
|
+
self.callbacks.set_window_icon_name(&mut self.screen, s);
|
|
206
|
+
}
|
|
207
|
+
[b"2", s] => {
|
|
208
|
+
self.callbacks.set_window_title(&mut self.screen, s);
|
|
209
|
+
}
|
|
210
|
+
[b"52", ty, data] => {
|
|
211
|
+
match (
|
|
212
|
+
ty.iter().all(|c| CLIPBOARD_SELECTOR.contains(c)),
|
|
213
|
+
*data,
|
|
214
|
+
) {
|
|
215
|
+
(true, b"?") => {
|
|
216
|
+
self.callbacks
|
|
217
|
+
.paste_from_clipboard(&mut self.screen, ty);
|
|
218
|
+
}
|
|
219
|
+
(true, data)
|
|
220
|
+
if data.iter().all(|c| BASE64.contains(c)) =>
|
|
221
|
+
{
|
|
222
|
+
self.callbacks.copy_to_clipboard(
|
|
223
|
+
&mut self.screen,
|
|
224
|
+
ty,
|
|
225
|
+
data,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
_ => {
|
|
229
|
+
self.callbacks
|
|
230
|
+
.unhandled_osc(&mut self.screen, params);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
_ => {
|
|
235
|
+
self.callbacks.unhandled_osc(&mut self.screen, params);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn canonicalize_params_1(params: &vte::Params, default: u16) -> u16 {
|
|
242
|
+
let first = params.iter().next().map_or(0, |x| *x.first().unwrap_or(&0));
|
|
243
|
+
if first == 0 {
|
|
244
|
+
default
|
|
245
|
+
} else {
|
|
246
|
+
first
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn canonicalize_params_2(
|
|
251
|
+
params: &vte::Params,
|
|
252
|
+
default1: u16,
|
|
253
|
+
default2: u16,
|
|
254
|
+
) -> (u16, u16) {
|
|
255
|
+
let mut iter = params.iter();
|
|
256
|
+
let first = iter.next().map_or(0, |x| *x.first().unwrap_or(&0));
|
|
257
|
+
let first = if first == 0 { default1 } else { first };
|
|
258
|
+
|
|
259
|
+
let second = iter.next().map_or(0, |x| *x.first().unwrap_or(&0));
|
|
260
|
+
let second = if second == 0 { default2 } else { second };
|
|
261
|
+
|
|
262
|
+
(first, second)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn canonicalize_params_decstbm(
|
|
266
|
+
params: &vte::Params,
|
|
267
|
+
size: crate::grid::Size,
|
|
268
|
+
) -> (u16, u16) {
|
|
269
|
+
let mut iter = params.iter();
|
|
270
|
+
let top = iter.next().map_or(0, |x| *x.first().unwrap_or(&0));
|
|
271
|
+
let top = if top == 0 { 1 } else { top };
|
|
272
|
+
|
|
273
|
+
let bottom = iter.next().map_or(0, |x| *x.first().unwrap_or(&0));
|
|
274
|
+
let bottom = if bottom == 0 { size.rows } else { bottom };
|
|
275
|
+
|
|
276
|
+
(top, bottom)
|
|
277
|
+
}
|