Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7a7577900 | ||
|
|
837a73366d |
@@ -7,6 +7,12 @@ folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
|
||||
|
||||
## [Unveröffentlicht]
|
||||
|
||||
## [0.4.0] - 2026-10-11
|
||||
|
||||
### 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`).
|
||||
|
||||
## [0.3.0] - 2026-10-11
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
@@ -77,6 +77,31 @@ Eine Zeile mit `##` erscheint als Überschrift in der Ausgabe, eine mit `#` ist
|
||||
ein stiller Kommentar. Neue Befehle brauchen hier ein Beispiel, sonst schlägt der Test
|
||||
`test_testall_mentions_every_command` fehl.
|
||||
|
||||
## Vorlagen: `form` und `entry`
|
||||
|
||||
- `demo/form` ist eine Vorlage für `report` und `letter`: sie setzt Spaltenwerte in einen Text ein.
|
||||
Sie hat keine Beschriftungen und nennt `Name` zweimal.
|
||||
- `demo/entry` ist eine Eingabemaske für `screen`: jedes Feld kommt einmal vor und hat eine
|
||||
Beschriftung. `screen < entry > maske.sh` erzeugt das Skript. Starten:
|
||||
`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.
|
||||
|
||||
## 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
|
||||
|
||||
| Gruppe | Befehle |
|
||||
@@ -91,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` |
|
||||
| Umwandeln | `tabletolist` `listtotable` `listtosh` `tabletofix` `fixtotable` `tabletostruct` `tabletotbl` `tabletofact` `tabletorule` `tabletom4` `tabletosed` `translate` `uniondict` |
|
||||
| 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` |
|
||||
| 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` |
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Mail List Entry Form
|
||||
|
||||
Name : <Name> Company: <Company>
|
||||
Street: <Street>
|
||||
City : <City> State : <State> ZIP: <ZIP>
|
||||
Phone : <Phone>
|
||||
|
||||
[Speichern] [Abbrechen]
|
||||
|
||||
@@ -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"
|
||||
@@ -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 ]
|
||||
+10
-2
@@ -182,8 +182,16 @@ report form < maillist
|
||||
## label prints mailing labels, letter prints form letters
|
||||
label < maillist
|
||||
letter form < maillist | head -5
|
||||
## screen turns a form into a shell script that asks for the fields
|
||||
screen < form | head -6
|
||||
## screen turns a form into a shell script that asks for the fields. Use a form with labels
|
||||
## and every column once (entry); the report template `form` would give a blank, doubled screen.
|
||||
## Run the script with: CLEAR=$(clear.rdb) sh script > table 2>&1 (the table goes to stderr)
|
||||
cat 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 -3 Prices < inventory | grep -v 'Report Date'
|
||||
## tabletom4 / tabletosed / tabletostruct were shown above
|
||||
|
||||
+73
-5
@@ -735,14 +735,82 @@ McDonalds
|
||||
123 Mac Attack
|
||||
Memphis, TENN 30000
|
||||
|
||||
## screen turns a form into a shell script that asks for the fields
|
||||
$ screen < form | head -6
|
||||
## screen turns a form into a shell script that asks for the fields. Use a form with labels
|
||||
## and every column once (entry); the report template `form` would give a blank, doubled screen.
|
||||
## Run the script with: CLEAR=$(clear.rdb) sh script > table 2>&1 (the table goes to stderr)
|
||||
$ cat entry
|
||||
Mail List Entry Form
|
||||
|
||||
Name : <Name> Company: <Company>
|
||||
Street: <Street>
|
||||
City : <City> State : <State> ZIP: <ZIP>
|
||||
Phone : <Phone>
|
||||
|
||||
[Speichern] [Abbrechen]
|
||||
|
||||
$ screen < entry
|
||||
: paint crt screen
|
||||
exec 1>/dev/tty
|
||||
cat <<SCREEN
|
||||
${CLEAR}
|
||||
${CLEAR} Mail List Entry Form
|
||||
|
||||
Name : Company:
|
||||
Street:
|
||||
City : State : ZIP:
|
||||
Phone :
|
||||
|
||||
[Speichern] [Abbrechen]
|
||||
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"
|
||||
## 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 -3 Prices < inventory | grep -v 'Report Date'
|
||||
Prices
|
||||
@@ -777,9 +845,9 @@ Item Amount Cost Value Description
|
||||
## tabletom4 / tabletosed / tabletostruct were shown above
|
||||
## ===== Help and the terminal =====
|
||||
$ rdb --version
|
||||
rdb 0.2.0
|
||||
rdb 0.4.0
|
||||
$ howmany
|
||||
134
|
||||
135
|
||||
$ whatis row
|
||||
row - make a new table where rows match logical condition
|
||||
row row 'column condition column-or-value ..' < table
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rdb
|
||||
VERSION = '0.3.0'
|
||||
VERSION = '0.4.0'
|
||||
end
|
||||
|
||||
+116
@@ -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
|
||||
@@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'test_helper'
|
||||
require 'pty'
|
||||
require_relative '../lib/rdb'
|
||||
|
||||
class SelectionTest < RdbCase
|
||||
def test_row_numeric_condition
|
||||
@@ -523,3 +525,96 @@ class ActTest < RdbCase
|
||||
assert_equal [%w[b]], data(out)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user