Version 0.5.0: pick, Zeilen auf dem Terminal wählen
This commit is contained in:
@@ -7,6 +7,11 @@ folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
|
|||||||
|
|
||||||
## [Unveröffentlicht]
|
## [Unveröffentlicht]
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-10-11
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
- `pick`: Tabelle auf dem Terminal durchblättern, filtern und eine oder mehrere Zeilen (`-m`) auswählen; die Auswahl geht als Tabelle auf die Standardausgabe. `-k` nimmt die Tasten aus einem String. Dazu `man/pick.man`, Beispiele in `testall.cmds` und Tests (auch mit Pseudo-Terminal).
|
||||||
|
|
||||||
## [0.4.0] - 2026-10-11
|
## [0.4.0] - 2026-10-11
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|||||||
@@ -102,6 +102,21 @@ In der Vorlage: `<Spalte>` oder `<Spalte:20>` ist ein Feld (mit Breite), `[ Spei
|
|||||||
`[ Abbrechen ]` (oder Cancel, Abort, Quit) bricht ab, `<!date!>` fügt Befehlsausgabe ein. Kommt eine Spalte mehrfach
|
`[ 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).
|
vor, ist es ein Feld. Tab/Pfeile wechseln, Enter bestätigt, Esc bricht ab (Status 1, keine Ausgabe).
|
||||||
|
|
||||||
|
## Zeilen auswählen: `pick`
|
||||||
|
|
||||||
|
`pick` zeigt eine Tabelle auf dem Terminal, man scrollt, filtert und wählt eine oder mehrere Zeilen. Die Tabelle
|
||||||
|
kommt von der Standardeingabe, die gewählten Zeilen gehen als Tabelle auf die Standardausgabe, also mitten in der Pipe:
|
||||||
|
|
||||||
|
```
|
||||||
|
pick < inventory eine Zeile wählen
|
||||||
|
pick -m < inventory | total Amount Value mehrere (Leertaste markiert, a alle)
|
||||||
|
column Item Description < inventory | pick -m | jointable - owners
|
||||||
|
pick -m -k 'down space down enter' < inventory Tasten aus einem String (Skripte, Tests)
|
||||||
|
```
|
||||||
|
|
||||||
|
Pfeile, Bild auf/ab, Pos1/Ende scrollen; `/` filtert (Enter behält den Filter, Esc löscht ihn); Enter wählt;
|
||||||
|
Esc bricht ab (Status 1, keine Ausgabe). Ohne Markierung gilt die Zeile unter dem Cursor.
|
||||||
|
|
||||||
## Befehle
|
## Befehle
|
||||||
|
|
||||||
| Gruppe | Befehle |
|
| Gruppe | Befehle |
|
||||||
@@ -116,7 +131,7 @@ vor, ist es ein Feld. Tab/Pfeile wechseln, Enter bestätigt, Esc bricht ab (Stat
|
|||||||
| 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` `mask` `label` `letter` |
|
| Berichte | `report` `reportwriter` `screen` `mask` `pick` `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` |
|
||||||
|
|||||||
@@ -265,3 +265,16 @@ bom ; cat bom
|
|||||||
## update.inv: change the stock of one item, with record locking (INV names the table)
|
## update.inv: change the stock of one item, with record locking (INV names the table)
|
||||||
cp inventory inv ; printf '7\n9\n\n' | INV=inv update.inv | tail -n +2
|
cp inventory inv ; printf '7\n9\n\n' | INV=inv update.inv | tail -n +2
|
||||||
grep '^7' inv
|
grep '^7' inv
|
||||||
|
|
||||||
|
## ===== Choosing rows on the terminal =====
|
||||||
|
## pick shows a table, lets you scroll, filter (/) and mark rows (Space, -m), and writes the
|
||||||
|
## chosen rows as a table, so it can sit in a pipe. It needs a terminal; -k feeds the keys instead.
|
||||||
|
## Try it: pick -m < inventory | total Amount Value
|
||||||
|
## mark the 2nd and 5th row (down, space, down, down, space) and finish with enter
|
||||||
|
pick -m -k 'down space down down space enter' < inventory
|
||||||
|
## the same chosen rows go on into total
|
||||||
|
pick -m -k 'down space down down space enter' < inventory | total Amount Value
|
||||||
|
## filter with / (type, enter), then choose the row that is left
|
||||||
|
pick -k '/clamps enter enter' < inventory
|
||||||
|
## Esc cancels: nothing on stdout, exit status 1
|
||||||
|
pick -k esc < inventory ; echo "status $?"
|
||||||
|
|||||||
+26
-2
@@ -845,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.4.0
|
rdb 0.5.0
|
||||||
$ howmany
|
$ howmany
|
||||||
135
|
136
|
||||||
$ 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
|
||||||
@@ -1107,3 +1107,27 @@ Item Number: (or hit Return key to exit.) Item=7 Amount=5 Cost=175.00 Value=8
|
|||||||
Amount Amount: Item Number: (or hit Return key to exit.)
|
Amount Amount: Item Number: (or hit Return key to exit.)
|
||||||
$ grep '^7' inv
|
$ grep '^7' inv
|
||||||
7 9 175.00 1575 scales
|
7 9 175.00 1575 scales
|
||||||
|
## ===== Choosing rows on the terminal =====
|
||||||
|
## pick shows a table, lets you scroll, filter (/) and mark rows (Space, -m), and writes the
|
||||||
|
## chosen rows as a table, so it can sit in a pipe. It needs a terminal; -k feeds the keys instead.
|
||||||
|
## Try it: pick -m < inventory | total Amount Value
|
||||||
|
## mark the 2nd and 5th row (down, space, down, down, space) and finish with enter
|
||||||
|
$ pick -m -k 'down space down down space enter' < inventory
|
||||||
|
Item Amount Cost Value Description
|
||||||
|
---- ------ ---- ----- -----------
|
||||||
|
2 100 0.50 50.00 test tubes
|
||||||
|
5 99 2.45 242.55 cleaning cloth
|
||||||
|
## the same chosen rows go on into total
|
||||||
|
$ pick -m -k 'down space down down space enter' < inventory | total Amount Value
|
||||||
|
Item Amount Cost Value Description
|
||||||
|
---- ------ ---- ----- -----------
|
||||||
|
199 292.55
|
||||||
|
## filter with / (type, enter), then choose the row that is left
|
||||||
|
$ pick -k '/clamps enter enter' < inventory
|
||||||
|
Item Amount Cost Value Description
|
||||||
|
---- ------ ---- ----- -----------
|
||||||
|
3 6 8.00 48.00 clamps
|
||||||
|
## Esc cancels: nothing on stdout, exit status 1
|
||||||
|
$ pick -k esc < inventory ; echo "status $?"
|
||||||
|
pick: nothing chosen
|
||||||
|
status 1
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'io/console'
|
||||||
|
|
||||||
|
# pick -- look at a table on the terminal, scroll through it and choose one or
|
||||||
|
# several rows. The table comes in on the standard input, the chosen rows go
|
||||||
|
# out on the standard output as a table, so it sits in the middle of a pipe:
|
||||||
|
# column Item Description < inventory | pick -m | total Item
|
||||||
|
# The keys are read from /dev/tty (or, for scripts and tests, from -k).
|
||||||
|
module Rdb
|
||||||
|
module Pick
|
||||||
|
KEYS = {
|
||||||
|
'up' => "\e[A", 'down' => "\e[B", 'pgup' => "\e[5~", 'pgdn' => "\e[6~",
|
||||||
|
'home' => "\e[H", 'end' => "\e[F", 'enter' => "\r", 'space' => ' ',
|
||||||
|
'esc' => "\e", 'backspace' => "\x7f"
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# The state of one session: which rows are shown, where the cursor is,
|
||||||
|
# which rows are marked. Knows nothing about the terminal.
|
||||||
|
class Session
|
||||||
|
attr_reader :head, :rows, :cursor, :top, :filter, :marked
|
||||||
|
|
||||||
|
def initialize(head, rows, multi: false)
|
||||||
|
@head = head
|
||||||
|
@rows = rows
|
||||||
|
@multi = multi
|
||||||
|
@marked = []
|
||||||
|
@filter = +''
|
||||||
|
@typing = false
|
||||||
|
@cursor = 0
|
||||||
|
@top = 0
|
||||||
|
@height = 20
|
||||||
|
@width = 80
|
||||||
|
refilter
|
||||||
|
end
|
||||||
|
|
||||||
|
def resize(height, width)
|
||||||
|
@height = [height, 1].max
|
||||||
|
@width = [width, 20].max
|
||||||
|
scroll
|
||||||
|
end
|
||||||
|
|
||||||
|
def typing? = @typing
|
||||||
|
|
||||||
|
# Indices (into rows) of the rows that pass the filter.
|
||||||
|
def visible = @visible
|
||||||
|
|
||||||
|
def current = @visible[@cursor]
|
||||||
|
|
||||||
|
# Rows to hand on: the marked ones, else the row under the cursor.
|
||||||
|
def selected
|
||||||
|
idx = @marked.empty? ? [current].compact : @marked.sort
|
||||||
|
idx.map { |i| @rows[i] }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns :accept or :cancel when the session is over.
|
||||||
|
def feed(key)
|
||||||
|
@typing ? type(key) : browse(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render
|
||||||
|
widths = @head.each_index.map { |i| ([@head[i]] + @rows.map { |r| r[i].to_s }).map(&:size).max }
|
||||||
|
line = ->(cells) { cells.each_with_index.map { |c, i| c.to_s.ljust(widths[i]) }.join(' ').rstrip }
|
||||||
|
out = +"\e[?25l\e[H"
|
||||||
|
out << "\e[K \e[1m#{fit(line.(@head))}\e[0m\r\n"
|
||||||
|
out << "\e[K #{fit(widths.map { |w| '-' * w }.join(' '))}\r\n"
|
||||||
|
@height.times do |n|
|
||||||
|
pos = @top + n
|
||||||
|
i = @visible[pos]
|
||||||
|
out << "\e[K"
|
||||||
|
if i
|
||||||
|
text = "#{@marked.include?(i) ? '*' : ' '} #{fit(line.(@rows[i]))}"
|
||||||
|
out << (pos == @cursor ? "\e[7m#{text}\e[0m" : text)
|
||||||
|
end
|
||||||
|
out << "\r\n"
|
||||||
|
end
|
||||||
|
out << "\e[K#{status}"
|
||||||
|
out
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fit(text) = text[0, @width - 2]
|
||||||
|
|
||||||
|
def status
|
||||||
|
keys = @typing ? 'type to filter Enter keep Esc clear' : "Enter ok#{' Space mark a all' if @multi} / filter Esc cancel"
|
||||||
|
filter = @filter.empty? && !@typing ? '' : " /#{@filter}#{'_' if @typing}"
|
||||||
|
"\e[7m #{@visible.empty? ? 0 : @cursor + 1}/#{@visible.size} of #{@rows.size}" \
|
||||||
|
"#{" marked #{@marked.size}" if @multi}#{filter} #{keys} \e[0m"
|
||||||
|
end
|
||||||
|
|
||||||
|
def browse(key)
|
||||||
|
case key
|
||||||
|
when "\e[A" then move(-1)
|
||||||
|
when "\e[B" then move(1)
|
||||||
|
when "\e[5~" then move(-@height)
|
||||||
|
when "\e[6~" then move(@height)
|
||||||
|
when "\e[H", "\e[1~", "\eOH" then move(-@visible.size)
|
||||||
|
when "\e[F", "\e[4~", "\eOF" then move(@visible.size)
|
||||||
|
when "\r", "\n" then return selected.empty? ? :cancel : :accept
|
||||||
|
when ' ' then toggle
|
||||||
|
when 'a' then mark_all if @multi
|
||||||
|
when '/' then @typing = true
|
||||||
|
when "\x03", "\e", 'q' then return :cancel
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def type(key)
|
||||||
|
case key
|
||||||
|
when "\r", "\n" then @typing = false
|
||||||
|
when "\e" then @filter.clear; @typing = false; refilter
|
||||||
|
when "\x03" then return :cancel
|
||||||
|
when "\x7f", "\b" then @filter.chop!; refilter
|
||||||
|
else
|
||||||
|
if key.size == 1 && key.ord >= 32
|
||||||
|
@filter << key
|
||||||
|
refilter
|
||||||
|
end
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def refilter
|
||||||
|
needle = @filter.downcase
|
||||||
|
@visible = @rows.each_index.select { |i| needle.empty? || @rows[i].join("\t").downcase.include?(needle) }
|
||||||
|
@cursor = [@cursor, [@visible.size - 1, 0].max].min
|
||||||
|
scroll
|
||||||
|
end
|
||||||
|
|
||||||
|
def move(delta)
|
||||||
|
@cursor = (@cursor + delta).clamp(0, [@visible.size - 1, 0].max)
|
||||||
|
scroll
|
||||||
|
end
|
||||||
|
|
||||||
|
def scroll
|
||||||
|
@top = @cursor if @cursor < @top
|
||||||
|
@top = @cursor - @height + 1 if @cursor >= @top + @height
|
||||||
|
@top = 0 if @top.negative?
|
||||||
|
end
|
||||||
|
|
||||||
|
def toggle
|
||||||
|
return unless @multi && current
|
||||||
|
|
||||||
|
@marked.include?(current) ? @marked.delete(current) : @marked << current
|
||||||
|
move(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_all
|
||||||
|
all = @visible.all? { |i| @marked.include?(i) }
|
||||||
|
all ? @marked -= @visible : @marked |= @visible
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.run(session, tty)
|
||||||
|
tty.raw do
|
||||||
|
tty.write "\e[?1049h"
|
||||||
|
begin
|
||||||
|
loop do
|
||||||
|
rows, cols = tty.winsize
|
||||||
|
session.resize((rows.zero? ? 24 : rows) - 3, cols.zero? ? 80 : cols)
|
||||||
|
tty.write session.render
|
||||||
|
key = begin
|
||||||
|
Rdb::Mask.read_key(tty)
|
||||||
|
rescue EOFError
|
||||||
|
"\x03"
|
||||||
|
end
|
||||||
|
result = session.feed(key)
|
||||||
|
return result if result
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
tty.write "\e[?25h\e[?1049l"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Words of a -k script: a name from KEYS, or else every character is one key.
|
||||||
|
def self.script(words)
|
||||||
|
words.split.flat_map { |w| KEYS.key?(w) ? [KEYS[w]] : w.chars }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Rdb.register('pick', 'Scroll through a table and choose one or more rows', 'pick [-m] [-k keys] < table > chosenrows') do |args|
|
||||||
|
multi = !args.delete('-m').nil?
|
||||||
|
keys = nil
|
||||||
|
if (i = args.index('-k'))
|
||||||
|
keys = args[i + 1] or die 'usage: pick [-m] [-k keys] < table'
|
||||||
|
args.slice!(i, 2)
|
||||||
|
end
|
||||||
|
die 'usage: pick [-m] [-k "down space enter"] < table > chosenrows' unless args.empty?
|
||||||
|
|
||||||
|
t = table_in
|
||||||
|
die 'pick: the table is empty' if t.rows.empty?
|
||||||
|
session = Rdb::Pick::Session.new(t.head, t.rows, multi: multi)
|
||||||
|
|
||||||
|
result = nil
|
||||||
|
if keys
|
||||||
|
session.resize(21, 80)
|
||||||
|
Rdb::Pick.script(keys).each do |k|
|
||||||
|
result = session.feed(k)
|
||||||
|
break if result
|
||||||
|
end
|
||||||
|
else
|
||||||
|
tty = begin
|
||||||
|
File.open('/dev/tty', 'r+')
|
||||||
|
rescue SystemCallError
|
||||||
|
die 'no terminal available (pick takes its keys from /dev/tty; see -k)'
|
||||||
|
end
|
||||||
|
result = Rdb::Pick.run(session, tty)
|
||||||
|
tty.close
|
||||||
|
end
|
||||||
|
unless result == :accept
|
||||||
|
warn 'pick: nothing chosen'
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
Rdb::Table.new(t.head, session.selected).write
|
||||||
|
end
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Rdb
|
module Rdb
|
||||||
VERSION = '0.4.0'
|
VERSION = '0.5.0'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
.TH pick "" "" "/rdb Command"
|
||||||
|
.PC "Scroll through a table and choose one or more rows"
|
||||||
|
\fBpick \fB[ -m ] [ -k \fIkeys\fB ] < \fItable \fB> \fIchosenrows\fR
|
||||||
|
.PP
|
||||||
|
.II "pick^choose rows"
|
||||||
|
.II "table^scroll"
|
||||||
|
.I "(Ruby re-creation only; the original /rdb has no such command.)"
|
||||||
|
.PP
|
||||||
|
The command
|
||||||
|
.B pick
|
||||||
|
shows
|
||||||
|
.I table
|
||||||
|
on the terminal, lets the user scroll through it, and writes the rows
|
||||||
|
that were chosen as a table on the standard output.
|
||||||
|
It therefore fits into the middle of a pipe:
|
||||||
|
.DM
|
||||||
|
column Item Description < inventory | pick -m | jointable - owners
|
||||||
|
pick -m < inventory | total Amount Value
|
||||||
|
.DE
|
||||||
|
.PP
|
||||||
|
The table is read from the standard input;
|
||||||
|
the keys are read from the terminal (\fB/dev/tty\fR).
|
||||||
|
A list is read like a table, and the output is always a table.
|
||||||
|
The head and the dash line stay at the top while the rows scroll.
|
||||||
|
.SH Keys
|
||||||
|
.nf
|
||||||
|
Up, Down move the cursor one row
|
||||||
|
PgUp, PgDn move a page
|
||||||
|
Home, End first or last row
|
||||||
|
Enter choose and finish
|
||||||
|
Space mark the row, move down (with -m)
|
||||||
|
a mark all shown rows; again, unmark them (with -m)
|
||||||
|
/ filter: type text, shown rows must contain it
|
||||||
|
(Enter keeps the filter, Esc clears it)
|
||||||
|
Esc, q, Ctrl-C cancel
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
.B pick
|
||||||
|
writes the marked rows in table order.
|
||||||
|
If no row is marked, it writes the row under the cursor.
|
||||||
|
Marks stay when a filter hides the row.
|
||||||
|
If the user cancels, or the filter leaves no row to choose,
|
||||||
|
.B pick
|
||||||
|
writes nothing and exits with status 1.
|
||||||
|
.SH Options
|
||||||
|
.IP \fB-m\fR 0.5i
|
||||||
|
Allow several rows: Space marks, \fBa\fR marks all.
|
||||||
|
Without it, Enter chooses the row under the cursor.
|
||||||
|
.IP "\fB-k\fR \fIkeys\fR"
|
||||||
|
Take the keys from this string instead of the terminal.
|
||||||
|
Words are key names:
|
||||||
|
.BR up ,
|
||||||
|
.BR down ,
|
||||||
|
.BR pgup ,
|
||||||
|
.BR pgdn ,
|
||||||
|
.BR home ,
|
||||||
|
.BR end ,
|
||||||
|
.BR enter ,
|
||||||
|
.BR space ,
|
||||||
|
.BR esc ,
|
||||||
|
.BR backspace ;
|
||||||
|
any other word is typed character by character.
|
||||||
|
This is for scripts, demonstrations and tests.
|
||||||
|
.SH Example
|
||||||
|
.DM
|
||||||
|
pick -m -k 'down space down down space enter' < inventory
|
||||||
|
.DE
|
||||||
|
.PP
|
||||||
|
marks the second and the fifth row and prints:
|
||||||
|
.DM
|
||||||
|
Item Amount Cost Value Description
|
||||||
|
---- ------ ---- ------ --------------
|
||||||
|
2 100 0.50 50.00 test tubes
|
||||||
|
5 99 2.45 242.55 cleaning cloth
|
||||||
|
.DE
|
||||||
|
.SH "See Also"
|
||||||
|
.B
|
||||||
|
display,
|
||||||
|
mask,
|
||||||
|
row,
|
||||||
|
search
|
||||||
|
.R
|
||||||
|
.SO rdb
|
||||||
@@ -618,3 +618,108 @@ class MaskTest < RdbCase
|
|||||||
assert_equal '', table
|
assert_equal '', table
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class PickTest < RdbCase
|
||||||
|
def session(multi: true, height: 3)
|
||||||
|
t = Rdb::Table.read(File.open(File.join(DEMO, 'inventory')))
|
||||||
|
Rdb::Pick::Session.new(t.head, t.rows, multi: multi).tap { |x| x.resize(height, 80) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def keys(sess, words) = Rdb::Pick.script(words).map { |k| sess.feed(k) }.compact.last
|
||||||
|
|
||||||
|
def test_mark_and_select
|
||||||
|
s = session
|
||||||
|
assert_equal :accept, keys(s, 'down space down down space enter')
|
||||||
|
assert_equal %w[2 5], s.selected.map(&:first)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_single_mode_takes_the_cursor_row_and_ignores_space
|
||||||
|
s = session(multi: false)
|
||||||
|
assert_equal :accept, keys(s, 'down down space enter')
|
||||||
|
assert_equal %w[3], s.selected.map(&:first)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_scrolling_keeps_the_cursor_in_view
|
||||||
|
s = session(height: 3)
|
||||||
|
keys(s, 'down down down down')
|
||||||
|
assert_equal [4, 2], [s.cursor, s.top]
|
||||||
|
keys(s, 'end')
|
||||||
|
assert_equal [6, 4], [s.cursor, s.top]
|
||||||
|
keys(s, 'pgup')
|
||||||
|
assert_equal 3, s.cursor
|
||||||
|
keys(s, 'home')
|
||||||
|
assert_equal [0, 0], [s.cursor, s.top]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_filter_and_clear
|
||||||
|
s = session
|
||||||
|
keys(s, '/clam')
|
||||||
|
assert_predicate s, :typing?
|
||||||
|
keys(s, 'enter')
|
||||||
|
assert_equal [2], s.visible
|
||||||
|
keys(s, '/ esc')
|
||||||
|
assert_equal 7, s.visible.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_marks_survive_a_filter_and_a_all_toggles
|
||||||
|
s = session
|
||||||
|
keys(s, 'space /scales enter')
|
||||||
|
assert_equal [0], s.marked
|
||||||
|
keys(s, 'a')
|
||||||
|
assert_equal [0, 6], s.marked.sort
|
||||||
|
keys(s, 'a')
|
||||||
|
assert_equal [0], s.marked
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cancel_and_empty_choice
|
||||||
|
assert_equal :cancel, keys(session, 'esc')
|
||||||
|
assert_equal :cancel, keys(session, '/zzz enter enter')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_command_with_keys_script_is_a_filter
|
||||||
|
out = rdb('pick', '-m', '-k', 'down space down down space enter', file: 'inventory')
|
||||||
|
assert_equal %w[2 5], data(out).map(&:first)
|
||||||
|
assert_equal '292.55', data(rdb('total', 'Value', stdin: out)).first[3]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_command_cancelled_exits_1_with_no_output
|
||||||
|
out, _err, st = run3('pick', '-k', 'esc', stdin: File.read(File.join(@dir, 'inventory')))
|
||||||
|
refute st.success?
|
||||||
|
assert_equal '', out
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_command_reads_a_list
|
||||||
|
out = rdb('pick', '-k', 'down enter', stdin: rdb('tabletolist', file: 'owners'))
|
||||||
|
assert_equal [%w[3 bob]], data(out)
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_on_pty(*key_strokes)
|
||||||
|
out = File.join(@dir, 'pick.out')
|
||||||
|
result = nil
|
||||||
|
PTY.spawn({ 'PATH' => "#{BIN}:#{ENV['PATH']}" }, 'sh', '-c', "cd #{@dir} && pick -m < inventory > pick.out") do |r, w, pid|
|
||||||
|
sleep 0.5
|
||||||
|
key_strokes.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_pick_on_a_terminal
|
||||||
|
table, status = run_on_pty("\e[B", ' ', "\e[B", ' ', "\r")
|
||||||
|
assert_equal 0, status
|
||||||
|
assert_equal %w[2 4], data(table).map(&:first)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pick_on_a_terminal_cancel
|
||||||
|
table, status = run_on_pty("\e[B", "\e")
|
||||||
|
assert_equal 1, status
|
||||||
|
assert_equal '', table
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user