Version 0.4.0: mask, eine Eingabemaske mit eigener Oberfläche

This commit is contained in:
wkh
2026-10-11 22:08:38 +02:00
parent 837a73366d
commit 82e408f3b0
12 changed files with 592 additions and 4 deletions
+3
View File
@@ -7,7 +7,10 @@ folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
## [Unveröffentlicht] ## [Unveröffentlicht]
## [0.4.0] - 2026-10-11
### Hinzugefügt ### Hinzugefügt
- `mask`: Eingabemaske mit eigener Oberfläche (Felder mit Breite, Schaltflächen, Rahmen `-b`, `-s` zeigt das Bild ohne Terminal). Schreibt die Eingabe als Tabelle auf die Standardausgabe. Die Vorlage kommt wie bei `screen` von der Standardeingabe (`mask < entry`). Dazu `man/mask.man`, `demo/maskform` und Tests (auch mit Pseudo-Terminal).
- `demo/entry`: beschriftete Eingabemaske für `screen` (README erklärt den Unterschied zu `form`). - `demo/entry`: beschriftete Eingabemaske für `screen` (README erklärt den Unterschied zu `form`).
## [0.3.0] - 2026-10-11 ## [0.3.0] - 2026-10-11
+17 -1
View File
@@ -86,6 +86,22 @@ ein stiller Kommentar. Neue Befehle brauchen hier ein Beispiel, sonst schlägt d
`CLEAR=$(clear.rdb) sh maske.sh > tabelle 2>&1`. Die Tabelle geht auf stderr (wie im Original), `CLEAR=$(clear.rdb) sh maske.sh > tabelle 2>&1`. Die Tabelle geht auf stderr (wie im Original),
daher das `2>&1`. Mit `form` würde die Maske leer und doppelt aussehen. daher das `2>&1`. Mit `form` würde die Maske leer und doppelt aussehen.
## Eingabemasken: `mask`
`mask` ist die Variante von `screen` mit eigener Oberfläche (nur Ruby, kein ncurses). Es zeichnet die
Vorlage (von der Standardeingabe, wie `screen`; ein Dateiname geht auch) selbst und schreibt die Eingabe als Tabelle auf die Standardausgabe, also pipefähig:
```
mask -b < demo/maskform Rahmen (-b), Felder, Schaltflächen
mask < demo/maskform > neu Tabelle in eine Datei
mask < demo/maskform | append -h kunden oder direkt in eine Tabelle
mask -s < demo/maskform nur das Bild und die Feldpositionen, ohne Terminal
```
In der Vorlage: `<Spalte>` oder `<Spalte:20>` ist ein Feld (mit Breite), `[ Speichern ]` eine Schaltfläche,
`[ Abbrechen ]` (oder Cancel, Abort, Quit) bricht ab, `<!date!>` fügt Befehlsausgabe ein. Kommt eine Spalte mehrfach
vor, ist es ein Feld. Tab/Pfeile wechseln, Enter bestätigt, Esc bricht ab (Status 1, keine Ausgabe).
## Befehle ## Befehle
| Gruppe | Befehle | | Gruppe | Befehle |
@@ -100,7 +116,7 @@ ein stiller Kommentar. Neue Befehle brauchen hier ein Beispiel, sonst schlägt d
| Text | `cap` `uppercase` `lowercase` `substitute` `trimblank` `padstring` `length` `filesize` `ascii` `chr` `word` `prompt` `not` `path` | | Text | `cap` `uppercase` `lowercase` `substitute` `trimblank` `padstring` `length` `filesize` `ascii` `chr` `word` `prompt` `not` `path` |
| Umwandeln | `tabletolist` `listtotable` `listtosh` `tabletofix` `fixtotable` `tabletostruct` `tabletotbl` `tabletofact` `tabletorule` `tabletom4` `tabletosed` `translate` `uniondict` | | Umwandeln | `tabletolist` `listtotable` `listtosh` `tabletofix` `fixtotable` `tabletostruct` `tabletotbl` `tabletofact` `tabletorule` `tabletom4` `tabletosed` `translate` `uniondict` |
| Datum, Namen | `julian` `gregorian` `computedate` `todaysdate` `soundex` `like` `hashkey` | | Datum, Namen | `julian` `gregorian` `computedate` `todaysdate` `soundex` `like` `hashkey` |
| Berichte | `report` `reportwriter` `screen` `label` `letter` | | Berichte | `report` `reportwriter` `screen` `mask` `label` `letter` |
| Terminal, Hilfe | `termput` `tset` `clear.rdb` `cursor` `menu` `rdb` `commands` `helpme` `whatis` `whatwill` `howmany` `man` | | Terminal, Hilfe | `termput` `tset` `clear.rdb` `cursor` `menu` `rdb` `commands` `helpme` `whatis` `whatwill` `howmany` `man` |
| Betrieb | `testall` `testsearch` `timesearch` `rmcore` `cpdir.rdb` | | Betrieb | `testall` `testsearch` `timesearch` `rmcore` `cpdir.rdb` |
| Buchhaltung (/act) | `act` `chartdup` `foot` `adjust` `balance` `close` `consolidate` `getjournal` `fillform` `calculate` `tax` `cashflow` `calcpay` `w2` `bom` `explode` `sale` `invoice` `cstate` `update.inv` | | Buchhaltung (/act) | `act` `chartdup` `foot` `adjust` `balance` `close` `consolidate` `getjournal` `fillform` `calculate` `tax` `cashflow` `calcpay` `w2` `bom` `explode` `sale` `invoice` `cstate` `update.inv` |
Symlink
+1
View File
@@ -0,0 +1 @@
/home/wkh/Development/Coherent/rdb/bin/rdb
+3
View File
@@ -4,3 +4,6 @@ Name : <Name> Company: <Company>
Street: <Street> Street: <Street>
City : <City> State : <State> ZIP: <ZIP> City : <City> State : <State> ZIP: <ZIP>
Phone : <Phone> Phone : <Phone>
[Speichern] [Abbrechen]
+24
View File
@@ -0,0 +1,24 @@
: paint crt screen
exec 1>/dev/tty
cat <<SCREEN
${CLEAR} Mail List Entry Form
Name : Company:
Street:
City : State : ZIP:
Phone :
SCREEN
: read user input
cursor 3 9 ; read Name;
cursor 3 37 ; read Company;
cursor 4 9 ; read Street;
cursor 5 9 ; read City;
cursor 5 37 ; read State;
cursor 5 56 ; read ZIP;
cursor 6 9 ; read Phone;
: output table head
exec 1>&2
echo "Name Company Street City State ZIP Phone"
echo "---- ------- ------ ---- ----- --- -----"
: append row
echo "$Name $Company $Street $City $State $ZIP $Phone"
+9
View File
@@ -0,0 +1,9 @@
Mail List Entry
Name : <Name:24>
Company: <Company:24>
Street : <Street:24>
City : <City:16> State: <State:10> ZIP: <ZIP:9>
Phone : <Phone:16>
[ Save ] [ Cancel ]
+5
View File
@@ -187,6 +187,11 @@ letter form < maillist | head -5
## Run the script with: CLEAR=$(clear.rdb) sh script > table 2>&1 (the table goes to stderr) ## Run the script with: CLEAR=$(clear.rdb) sh script > table 2>&1 (the table goes to stderr)
cat entry cat entry
screen < entry screen < entry
## mask draws the form itself (fields, buttons, frame) and writes the row as a table on stdout.
## It needs a terminal, so here only the picture of the form and its fields (-s) is shown.
## Try it: mask -b < maskform mask < maskform | append -h t
cat maskform
mask -s -b < maskform
## reportwriter: a sample report with page headings (the date is cut out) ## reportwriter: a sample report with page headings (the date is cut out)
reportwriter -3 Prices < inventory | grep -v 'Report Date' reportwriter -3 Prices < inventory | grep -v 'Report Date'
## tabletom4 / tabletosed / tabletostruct were shown above ## tabletom4 / tabletosed / tabletostruct were shown above
+43 -2
View File
@@ -745,6 +745,9 @@ Name : <Name> Company: <Company>
Street: <Street> Street: <Street>
City : <City> State : <State> ZIP: <ZIP> City : <City> State : <State> ZIP: <ZIP>
Phone : <Phone> Phone : <Phone>
[Speichern] [Abbrechen]
$ screen < entry $ screen < entry
: paint crt screen : paint crt screen
exec 1>/dev/tty exec 1>/dev/tty
@@ -755,6 +758,8 @@ Name : Company:
Street: Street:
City : State : ZIP: City : State : ZIP:
Phone : Phone :
[Speichern] [Abbrechen]
SCREEN SCREEN
: read user input : read user input
cursor 3 9 ; read Name; cursor 3 9 ; read Name;
@@ -770,6 +775,42 @@ echo "Name Company Street City State ZIP Phone"
echo "---- ------- ------ ---- ----- --- -----" echo "---- ------- ------ ---- ----- --- -----"
: append row : append row
echo "$Name $Company $Street $City $State $ZIP $Phone" echo "$Name $Company $Street $City $State $ZIP $Phone"
## mask draws the form itself (fields, buttons, frame) and writes the row as a table on stdout.
## It needs a terminal, so here only the picture of the form and its fields (-s) is shown.
## Try it: mask -b < maskform mask < maskform | append -h t
$ cat maskform
Mail List Entry
Name : <Name:24>
Company: <Company:24>
Street : <Street:24>
City : <City:16> State: <State:10> ZIP: <ZIP:9>
Phone : <Phone:16>
[ Save ] [ Cancel ]
$ mask -s -b < maskform
┌──────────────────────────────────────────────────────────────┐
│ Mail List Entry │
│ │
│ Name : ________________________ │
│ Company: ________________________ │
│ Street : ________________________ │
│ City : ________________ State: __________ ZIP: _________ │
│ Phone : ________________ │
│ │
│ [ Save ] [ Cancel ] │
└──────────────────────────────────────────────────────────────┘
Fields:
Name row 4 col 12 width 24
Company row 5 col 12 width 24
Street row 6 col 12 width 24
City row 7 col 12 width 16
State row 7 col 37 width 10
ZIP row 7 col 54 width 9
Phone row 8 col 12 width 16
[Save] row 10 col 6
[Cancel] row 10 col 17 (cancels)
## reportwriter: a sample report with page headings (the date is cut out) ## reportwriter: a sample report with page headings (the date is cut out)
$ reportwriter -3 Prices < inventory | grep -v 'Report Date' $ reportwriter -3 Prices < inventory | grep -v 'Report Date'
Prices Prices
@@ -804,9 +845,9 @@ Item Amount Cost Value Description
## tabletom4 / tabletosed / tabletostruct were shown above ## tabletom4 / tabletosed / tabletostruct were shown above
## ===== Help and the terminal ===== ## ===== Help and the terminal =====
$ rdb --version $ rdb --version
rdb 0.3.0 rdb 0.4.0
$ howmany $ howmany
134 135
$ whatis row $ whatis row
row - make a new table where rows match logical condition row - make a new table where rows match logical condition
row row 'column condition column-or-value ..' < table row row 'column condition column-or-value ..' < table
+275
View File
@@ -0,0 +1,275 @@
# frozen_string_literal: true
require 'io/console'
require 'open3'
# mask -- a full-screen input form. It reads the same kind of form as `screen`
# (text with <Column> fields) but draws it itself and writes the row typed in as
# a table on the standard output, so it works in a pipe: mask < entry | append t
#
# Besides <Column> the form may contain
# <Column:20> a field 20 characters wide (otherwise as wide as the <...>)
# [ Save ] a button; Cancel, Abort, Quit or Abbrechen cancel, any other accepts
# <!command!> replaced by the first line of the command's output
# Frames are plain text in the form, or drawn around everything with -b.
module Rdb
module Mask
Slot = Struct.new(:row, :col, :width, :name)
Button = Struct.new(:row, :col, :text, :label)
CANCEL = /\A(cancel|abort|quit|abbrechen)\z/i
TOKEN = /<!(.*?)!>|<([^<>!:\n]+?)(?::(\d+))?>|\[ *([^\[\]<>\n]*[^\[\]<>\s]) *\]/
class Form
attr_reader :lines, :slots, :buttons
def self.parse(text, frame: false)
lines = []
slots = []
buttons = []
text.each_line(chomp: true).with_index do |line, row|
out = +''
pos = 0
while (m = TOKEN.match(line, pos))
out << line[pos...m.begin(0)]
if m[1]
out << Open3.capture2('sh', '-c', m[1]).first.lines.first.to_s.chomp
elsif m[2]
width = m[3] ? m[3].to_i : m[0].size
slots << Slot.new(row, out.size, width, m[2].strip)
out << ' ' * width
else
buttons << Button.new(row, out.size, m[0], m[4].strip)
out << m[0]
end
pos = m.end(0)
end
lines << (out << line[pos..].to_s)
end
new(lines, slots, buttons).then { |f| frame ? f.framed : f }
end
def initialize(lines, slots, buttons)
@lines = lines
@slots = slots
@buttons = buttons
end
def names
@slots.map(&:name).uniq
end
# Fields (first slot of each column) and buttons in reading order.
def items
fields = @slots.uniq(&:name).map { |s| [:field, s] }
(fields + @buttons.map { |b| [:button, b] }).sort_by { |_, o| [o.row, o.col] }
end
def framed
w = @lines.map(&:size).max.to_i
lines = ["┌#{'─' * (w + 2)}┐"] + @lines.map { |l| "│ #{l.ljust(w)} │" } + ["└#{'─' * (w + 2)}┘"]
self.class.new(lines,
@slots.map { |s| Slot.new(s.row + 1, s.col + 2, s.width, s.name) },
@buttons.map { |b| Button.new(b.row + 1, b.col + 2, b.text, b.label) })
end
end
# The state of one input session: values, which item has the focus, and the keys.
class Session
attr_reader :values, :focus, :pos
def initialize(form)
@form = form
@items = form.items
@values = form.names.to_h { |n| [n, +''] }
@focus = 0
reset_pos
end
def kind = @items[@focus]&.first
def current = @items[@focus]&.last
def field? = kind == :field
# Feed one key; returns :accept or :cancel when the session ends.
def feed(key)
case key
when "\t", "\e[B", "\e[6~" then move(1)
when "\e[Z", "\e[A", "\e[5~" then move(-1)
when "\r", "\n" then return enter
when "\x03", "\e", "\x04" then return :cancel
when "\e[C" then @pos = [@pos + 1, text.size].min if field?
when "\e[D" then @pos = [@pos - 1, 0].max if field?
when "\e[H", "\e[1~", "\eOH", "\x01" then @pos = 0
when "\e[F", "\e[4~", "\eOF", "\x05" then @pos = text.size if field?
when "\x7f", "\b" then backspace
when "\e[3~" then delete
when "\x15" then clear_field
when ' '
return press unless field?
insert(key)
else insert(key) if key.size == 1 && key.ord >= 32
end
nil
end
# Plain-text picture of the form (no escapes), for `mask -s` and the tests.
def plain
grid = @form.lines.map(&:dup)
@form.slots.each { |s| grid[s.row][s.col, s.width] = '_' * s.width }
grid.join("\n") + "\n"
end
# Escape sequences that paint the whole screen.
def render
out = +"\e[?25l\e[H\e[2J"
@form.lines.each_with_index { |l, r| out << "\e[#{r + 1};1H#{l}" }
cursor = nil
@form.slots.each do |s|
focused = field? && current.name == s.name
start = focused ? view_start(s.width) : 0
shown = @values[s.name][start, s.width].to_s.ljust(s.width)
out << "\e[#{s.row + 1};#{s.col + 1}H\e[#{focused ? 7 : 4}m#{shown}\e[0m"
cursor = [s.row + 1, s.col + 1 + @pos - start] if focused && current.equal?(s)
end
@form.buttons.each do |b|
focused = kind == :button && current.equal?(b)
out << "\e[#{b.row + 1};#{b.col + 1}H\e[#{focused ? 7 : 1}m#{b.text}\e[0m"
cursor = [b.row + 1, b.col + 1] if focused
end
out << "\e[#{cursor[0]};#{cursor[1]}H" if cursor
out << "\e[?25h" if field?
out
end
private
def text = field? ? @values[current.name] : +''
def reset_pos
@pos = field? ? text.size : 0
end
def move(delta)
@focus = (@focus + delta) % [@items.size, 1].max
reset_pos
end
def enter
return press unless field?
last = @focus == @items.size - 1
return :accept if last && @form.buttons.empty?
move(1)
nil
end
def press
current.label.match?(CANCEL) ? :cancel : :accept
end
def insert(key)
return unless field?
text.insert(@pos, key)
@pos += 1
end
def backspace
return unless field? && @pos.positive?
text.slice!(@pos - 1)
@pos -= 1
end
def delete
text.slice!(@pos) if field?
end
def clear_field
return unless field?
text.clear
@pos = 0
end
def view_start(width)
[@pos - width + 1, 0].max
end
end
def self.read_key(io)
c = io.getch
return c unless c == "\e" && io.wait_readable(0.05)
seq = +"\e#{io.getch}"
if seq == "\e["
loop do
c = io.getch
seq << c
break if c.ord.between?(0x40, 0x7e)
end
elsif seq == "\eO"
seq << io.getch
end
seq
end
# Run the form on a terminal; returns the values, or nil when cancelled.
def self.run(form, tty)
session = Session.new(form)
tty.raw do
tty.write "\e[?1049h"
begin
loop do
tty.write session.render
key = begin
read_key(tty)
rescue EOFError
"\x03"
end
result = session.feed(key)
return (result == :accept ? session.values : nil) if result
end
ensure
tty.write "\e[?25h\e[?1049l"
end
end
end
end
end
Rdb.register('mask', 'Full-screen input form that writes a table row', 'mask [-b] [-s] [form] < form > table') do |args|
frame = !args.delete('-b').nil?
show = !args.delete('-s').nil?
die 'usage: mask [-b] [-s] [form] < form > table' if args.size > 1
file = args.first
file.nil? || file == '-' || File.exist?(file) or die("#{file}: no such form")
text = file && file != '-' ? File.read(file) : $stdin.read
form = Rdb::Mask::Form.parse(text, frame: frame)
die "the form has no <field>" if form.names.empty?
session = Rdb::Mask::Session.new(form)
if show
print session.plain
puts '', 'Fields:'
form.slots.each { |s| puts " #{s.name}\trow #{s.row + 1} col #{s.col + 1} width #{s.width}" }
form.buttons.each { |b| puts " [#{b.label}]\trow #{b.row + 1} col #{b.col + 1}#{b.label.match?(Rdb::Mask::CANCEL) ? ' (cancels)' : ''}" }
next
end
tty = begin
File.open('/dev/tty', 'r+')
rescue SystemCallError
die 'no terminal available (mask draws on /dev/tty)'
end
values = Rdb::Mask.run(form, tty)
tty.close
if values.nil?
warn 'mask: cancelled'
exit 1
end
Rdb::Table.new(form.names, [form.names.map { |n| values[n].tr("\t\r\n", ' ') }]).write
end
+1 -1
View File
@@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Rdb module Rdb
VERSION = '0.3.0' VERSION = '0.4.0'
end end
+116
View File
@@ -0,0 +1,116 @@
.TH mask "" "" "/rdb Command"
.PC "Full-screen input form that writes a table row"
\fBmask \fB[ -b ] [ -s ] [ \fIform\fB ] < \fIform \fB> \fItable\fR
.PP
.II "mask^input form"
.II "form^full-screen input"
.I "(Ruby re-creation only; the original /rdb has no such command.)"
.PP
The command
.B mask
draws
.I form
on the terminal, lets the user fill in its fields, and writes the result as
a one-row table on the standard output.
Unlike
.BR screen ,
it does not write a shell script: it runs the form itself.
The screen is drawn on
.BR /dev/tty ,
so the table can be redirected or piped:
.DM
mask < maskform > newrow
mask < maskform | append -h customer
.DE
.PP
.I form
is ordinary text.
Everything is shown as it stands, except:
.IP "\fB<Column>\fR" 1.2i
An input field for column
.IR Column .
It is as wide as the text between the angle brackets, including the brackets.
Blanks around the name are ignored, so \fB< City >\fR is column \fBCity\fR.
.IP "\fB<Column:20>\fR"
The same, but 20 characters wide.
Longer input scrolls inside the field.
.IP "\fB[ Save ]\fR"
A button.
A button whose label is
.BR Cancel ,
.BR Abort ,
.B Quit
or
.B Abbrechen
cancels; any other button accepts.
.IP "\fB<!command!>\fR"
Replaced by the first line the shell
.I command
writes.
.PP
If a column occurs more than once in the form, it is one field,
and every place shows what was typed.
The table has one column per field, in the order of first appearance.
Frames are made of characters in the form, for example \fB+---+\fR or
box-drawing characters.
.SH Keys
.nf
Tab, Down, Enter next field or button
Shift-Tab, Up previous field or button
Left, Right, Home, End move inside a field
Backspace, Delete erase; Ctrl-U clears the field
Enter, Space on a button: press it
Enter on the last field of a form without buttons: accept
Esc, Ctrl-C cancel
.fi
.PP
When the user cancels,
.B mask
writes nothing and exits with status 1.
.SH Options
.IP \fB-b\fR 0.5i
Draw a frame around the whole form.
.IP \fB-s\fR
Show the form as a plain picture (fields as underscores) with the position of
each field and button, and exit.
This needs no terminal and is useful to check a form.
.PP
Like
.BR screen ,
.B mask
reads the form from the standard input.
A file name argument is also accepted, and
.B -
means the standard input.
.SH Example
Form
.BR maskform :
.DM
Mail List Entry
Name : <Name:24>
City : <City:16> State: <State:10>
[ Save ] [ Cancel ]
.DE
.PP
.DM
mask -b < maskform > newrow
.DE
.PP
After typing ``Ann'' and ``Rome'', pressing Tab to the buttons and Enter on
.BR Save ,
.B newrow
contains:
.DM
Name City State
---- ---- -----
Ann Rome
.DE
.SH "See Also"
.B
cursor,
report,
screen
.R
.SO rdb
+95
View File
@@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'test_helper' require_relative 'test_helper'
require 'pty'
require_relative '../lib/rdb'
class SelectionTest < RdbCase class SelectionTest < RdbCase
def test_row_numeric_condition def test_row_numeric_condition
@@ -523,3 +525,96 @@ class ActTest < RdbCase
assert_equal [%w[b]], data(out) assert_equal [%w[b]], data(out)
end end
end end
class MaskTest < RdbCase
def form(text, frame: false) = Rdb::Mask::Form.parse(text, frame: frame)
def type(session, *keys)
keys.each { |k| k.start_with?("\e") || k.size == 1 ? session.feed(k) : k.each_char { |c| session.feed(c) } }
end
def test_parse_fields_buttons_and_width
f = form("Name: <Name:10> < City >\n[ Save ] [Cancel]\n")
assert_equal %w[Name City], f.names
assert_equal [0, 6, 10], f.slots[0].to_a.first(3)
assert_equal 9, f.slots[1].width # the width of "< City >"
assert_equal %w[Save Cancel], f.buttons.map(&:label)
assert_equal ["Name: #{' ' * 10} #{' ' * 9}", '[ Save ] [Cancel]'], f.lines
end
def test_command_in_form_and_frame
f = form("<!echo hi!> <A>\n", frame: true)
assert_equal '│ hi │', f.lines[1]
assert_equal [1, 5], f.slots.first.to_a.first(2)
end
def test_typing_and_navigation
s = Rdb::Mask::Session.new(form("<A:5> <B:5> [ OK ] [ Cancel ]\n"))
type(s, 'one', "\t", 'two', "\x7f", "\r")
assert_equal({ 'A' => 'one', 'B' => 'tw' }, s.values)
assert_equal :button, s.kind
assert_equal :accept, s.feed("\r")
end
def test_cancel_by_button_and_escape
s = Rdb::Mask::Session.new(form("<A:5> [ Save ] [ Abbrechen ]\n"))
s.feed("\e[Z")
assert_equal :cancel, s.feed(' ')
assert_equal :cancel, Rdb::Mask::Session.new(form("<A>\n")).feed("\e")
end
def test_enter_on_last_field_accepts_without_buttons
s = Rdb::Mask::Session.new(form("<A:3> <B:3>\n"))
assert_nil s.feed("\r")
assert_equal :accept, s.feed("\r")
end
def test_same_column_twice_is_one_field
s = Rdb::Mask::Session.new(form("<A:3> <A:3>\n"))
type(s, 'xy')
assert_equal({ 'A' => 'xy' }, s.values)
assert_equal %w[A], form("<A:3> <A:3>\n").names
end
def test_show_prints_picture_without_terminal
write('f', "Name: <Name:6>\n[ Save ] [ Cancel ]\n")
out = rdb('mask', '-s', file: 'f')
assert_includes out, "Name: ______\n[ Save ] [ Cancel ]"
assert_includes out, " Name\trow 1 col 7 width 6"
assert_includes out, " [Cancel]\trow 2 col 10 (cancels)"
assert_equal out, rdb('mask', '-s', 'f') # a file name works too
end
# Drive mask on a pseudo terminal; its table goes to a file, like in a pipe.
def run_on_pty(*keys)
out = File.join(@dir, 'mask.out')
result = nil
PTY.spawn({ 'PATH' => "#{BIN}:#{ENV['PATH']}" }, 'sh', '-c', "cd #{@dir} && mask < f > mask.out") do |r, w, pid|
sleep 0.5
keys.each do |k|
w.write k
sleep 0.1
r.read_nonblock(65_536) rescue nil
end
sleep 0.3
r.read_nonblock(65_536) rescue nil
_, st = Process.wait2(pid)
result = [File.exist?(out) ? File.read(out) : '', st.exitstatus]
end
result
end
def test_mask_on_a_terminal_writes_a_table
write('f', "Name: <Name:10>\nCity: <City:10>\n[ Save ] [ Cancel ]\n")
table, status = run_on_pty("Ann\r", "Rome\r", "\r")
assert_equal 0, status
assert_equal "Name\tCity\n----\t----\nAnn\tRome\n", table
end
def test_mask_cancel_writes_nothing
write('f', "Name: <Name:10>\n[ Save ] [ Cancel ]\n")
table, status = run_on_pty("Ann", "\e[Z", "\r")
assert_equal 1, status
assert_equal '', table
end
end