Skip to content

The History tab for 0.9.6: a row's menu, Export take, Restored, Original, UTF-8 - #162

Merged
AliceLafox merged 16 commits into
mainfrom
feat/history-row-menu
Oct 25, 2026
Merged

AliceLafox merged 16 commits into
mainfrom
feat/history-row-menu

Conversation

@AliceLafox

@AliceLafox AliceLafox commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

The History tab for 0.9.6: a menu on each row instead of three buttons, Export take, a Restored row that says what came back, the Original, the app's own words for Rhythm and Start & Stop, and UTF-8 that stays UTF-8 on its way into JUCE. One commit per item, so any one of them can be dropped without the rest.

  • A row's menu (app/HistoryRowMenu.h, app/HistoryPane.*): a click on a row with either button (Ctrl-click on a Mac, or Return on the selected row) selects it and offers Restore this state, Export take..., a separator, Clear history of this slot.... What can be chosen is historymenu::forRow, a function without a window: the same four lines on every row, grey when the row cannot do it, all grey while the pedal is busy. The pane asks the rule again when a line is chosen, so a menu left open across a re-read acts on nothing stale.
  • No Play on the tab: it loaded the take into the Audio tab's player without starting it or showing it. The History window keeps its own Play.
  • Export take... (app/history/ExportTakeJob.h): the save dialog proposes the take's own name (.wav added only when missing; a nameless take would be slot N take.wav); the write is history::exportTake, the window's - whole or absent, and a take no longer kept is a sentence. A worker job that needs no card and records no operation. The chosen file crosses into the core as UTF-8 (fsPath), so a folder outside the Windows code page is found.
  • Restored (store version 11): a restore records the operation it goes back to in ops.restores (nullable, a foreign key to ops), named before the card is touched, from the tab and the window. The row reads like Pushed - take, length, tempo - and adds "back to 17:52", the time of that row; on another day than the restore, with its date ("back to 11 Oct 17:52"). An older restore, or one whose row was forgotten (forgetting clears the reference), says what came back and claims no time. The tab's time column and that "back to" now come from one function.
  • Original: what the app found in a slot the first time it met the card, named by its take. A slot empty then has no Original row; a first sighting that failed for a slot keeps its row. The status line, the storage panel's label and the cut-off toast say "the original" too.
  • Rhythm / Start & Stop: rows and the undo/restore words no longer show the raw kinds rhythm and playstop.
  • UTF-8: JUCE reads a bare const char* as ASCII, and String + const char* reads the right side as UTF-8, so a literal holding a dash or a curly quote on the left of +, or alone, became Latin-1 mojibake - every connect line in operations.log, and the "Rename the pedal to ..." job on screen. Those go through fromUTF8; e.what() into a juce::String goes through utf8() on both storage-mode backends. tools/check_utf8_literals.py, in ctest, holds the tree to one rule: a narrow literal with a byte above 0x7f is the direct argument of fromUTF8(...)/utf8(...), or carries // utf8-data: <why> when it is data (a FAT name, bytes a parser must refuse). Text made in the core and in JUCE-free headers is ASCII now: " - " for a dash, "->" for an arrow, "..." for an ellipsis.

After an adversarial review (its attacking tests are the commit "Attacking tests for the History row menu review"; each fix after it turns some of them green):

  • Restore is offered only for a state the restore will put back. The rule read "a body and any kept take", and the restore handed that take to commands::restore - for a Cleared row, or an undo that emptied the slot, the take it had archived, which the primitive refuses beside an empty body. This was there before the menu; the menu made it a line you can click. A state is now the body and the take left beside it, offered when the body's WavStat agrees with that take (the primitive's own gate); both restores hand over only that take.
  • A row's menu remembers its slot: a swap is a row of both its slots, so a menu left open while another slot was selected could act on the new one.
  • An Original row names its take even when the first sighting was cut off at another slot (the card's row still says "interrupted"); a body that claims a loop beside an empty folder is not "empty at first sight".
  • The clock names the month itself (strftime's %b follows the process locale), rounds down before the epoch, and a restore whose target the store cannot resolve claims no time.
  • An export refusal names the chosen file, not take.wav.part, and the partial path keeps its characters on Windows.
  • The lint reads a source the way the compiler does: lines spliced and CRLF folded before tokens, a lone quote in #error/#if 0 text is not a literal, .cxx/.hh/.hxx/.m read too.
  • No byte above 0x7f is typed raw in any literal (the first CI run, app-windows): MSVC reads a source in the system's code page, so names typed into u8"..." in a test compiled to other bytes on Windows. The lint now requires an escape in every literal - narrow or wide, u8, a character, inside fromUTF8 or marked as data.

Tests: history_row_menu_tests (new: the menu over the rows a real store makes, free and busy), history_pane_tests (rewritten for the menu), clear_confirmation_tests (export and restore through the menu, the Restored row's words), card_timeline_tests (the proposed name), slot_story_tests (Restored, Original, the clock, Rhythm), slot_rows_tests (Original kept or dropped), history_store_tests (setRestores, a restore of a restore, forgetting the target), history_schema_tests (v7 and v10 files to v11: rows unchanged, the column empty, a second migrate a no-op), undo_run_tests, first_seen_tests, operations_log_tests (new: a log line with a non-ASCII reason, byte for byte), pedal_link_tests (a non-ASCII endpoint name through the refusal), card_rows_tests, history_window_tests, export_take_job_tests (new, from the review), utf8_literals_lint, utf8_literals_lint_tests and utf8_literals_lint_attack_tests (new).

Locally on macOS: a clean build has the 16 warnings main has (same files and lines), 0 errors; ctest 80/80, the six GUI harnesses included. The v11 step was run by this build on copies of three real stores (two at v10, one at v7): every table's row count unchanged, integrity_check ok, no foreign-key violation, restores empty on every old row.

…r the list

Either mouse button (or Ctrl-click, or Return on the selected row) selects
the row and offers Restore this state, Export take..., and, after a
separator, Clear history of this slot... The items and when each can be
chosen are historymenu::forRow, a function without a window: the same four
lines on every row, grey when the row cannot do it, and all grey while the
pedal is busy. The pane draws what it returns and asks the rule again when a
line is chosen, so a menu left open across a re-read acts on nothing stale.

Export take is offered here; its wiring follows.
Play loaded the take into the Audio tab's player without starting it or
showing it, and read as nothing happening. A click on a row now opens its
menu, so a double-click never reaches the row either. The tab's only caller
of playFromHistory is gone with it; the History window keeps its own Play
through playArchivedTake.
The save dialog proposes the take's own name, the one the row shows, with
.wav added only when the name does not already end so (the pedal's own
recordings are upper case); a take without a name would be proposed as
"slot N take.wav". The write is history::exportTake, the History window's:
the store's bytes, whole or absent, and a take the history no longer keeps
is its sentence rather than silence. It runs as a worker job that needs no
card and records no operation: nothing is read from the pedal or sent to it.

The chosen file crosses into the core as UTF-8 (fsPath), so a folder named
outside the Windows code page is still found.
The row read only the restore's note, and the job writes none, so it was
empty. A restore now records the operation it goes back to in a column of
its own (store version 11, ops.restores, nullable, a foreign key to ops),
named before the card is touched, from the tab and the window alike. The
row then reads like Pushed - the take, its length, its tempo - and adds
"back to 17:52", the time of the row it went back to; on another day than
the restore, with the date too ("back to 11 Oct 17:52"). A state that held
no take says "empty slot". A restore older than version 11, or whose row was
forgotten (forgetting clears the reference, as it does for reverts), says
what came back and claims no time.

The tab's time column and that "back to" now come from one function, so a
row names another row's time the way that row shows it - and a date a year
apart is no longer read as today.
What the app found in a slot the first time it met the card is the
Original, named by its take. A slot that was empty then has no Original row
at all: there was nothing, and a row saying so read as something that
happened. A first sighting that failed for a slot keeps its row - not seen
is not empty. The words around it move too: the status line while it runs
("Saving the original: N of 99 slots"), the storage panel's label
("Original - <take>"), and the toast when it is cut off ("Saving the
original stopped; it will finish next time you connect").
…d under

story::tell had no words for the rhythm and playstop operations and fell
back to the raw kind, so a row read "rhythm" or "playstop". It now says
"Rhythm" and "Start & Stop", and kindWords - which also names an operation
in the restore and undo words - says "Rhythm change" and "Start & Stop
change". The tabs that set them are hidden; their rows stay.
JUCE reads a bare const char* as ASCII, and `String + const char*` reads
the right side as UTF-8: a literal holding a dash, an arrow or a curly quote
on the LEFT of `+`, or alone, became Latin-1 mojibake. That was every
connect line in operations.log ("refused, nothing sent â..."), and the
job description "Rename the pedal to ..." on screen. Those literals now go
through fromUTF8. The storage-mode request returned e.what() into a
juce::String the same way, so a MIDI device named outside ASCII came back
garbled in the refusal; it goes through utf8() now, on both backends.

The comment that blamed std::string for the mojibake had it backwards:
juce::String(std::string) decodes UTF-8; const char* is what reads ASCII.
…CE are ASCII

tools/check_utf8_literals.py, registered in ctest: a narrow string literal
holding a byte above 0x7f - raw, or as a \x, octal or \u escape - must be
the direct argument of fromUTF8(...) or utf8(...), wherever it sits, the
right side of `+` included; one rule a reader can check by eye. Bytes that
are data rather than words (a FAT name with an accent, a sequence a parser
must refuse) carry a `// utf8-data: <why>` comment on their line. The
lint's own suite plants each way of writing such a byte and checks it is
caught, and that comments, wide and character literals are not.

To meet it, the words the core and the JUCE-free app headers make - error
sentences, the banner and status lines, the pedal-presence hints - are
ASCII now: a dash is " - ", an arrow "->", an ellipsis "...". JUCE-side text
keeps its typography through fromUTF8.
Tests written from the spec of feat/history-row-menu that try to break
it, not to confirm it: the rows layer against the restore primitive's
own gate, the Original row of an interrupted first sighting and of a
slot whose body claims a loop, a stale menu after the slot changed,
"Restore this state" of a Cleared row end to end, a restore reference
the store cannot resolve, chained restores forgotten in either order,
the v11 migration step failing last from v7 and from v10, the clock at
midnight, at the year's edge, on DST days and before the epoch, the
month's name under another locale, the export job without a card, and
the UTF-8 lint against spliced escapes, CRLF, preprocessor apostrophes
and other source suffixes.

A failing check is a finding; nothing in app/, core/ or tools/ is
changed except the new attack script and its ctest entry.
A row was restorable when it had a body and any kept take, and the restore
then handed that take to commands::restore beside the body. For a Cleared
row, or an undo that emptied the slot, the take is the one the operation
ARCHIVED, so the enabled "Restore this state" always ended in the
primitive's refusal ("a body and a take that disagree") and a failed
restore in the history. A trim cut off before its take landed, and a stray
file beside a body that says the slot is empty, were offered the same way.

A state is the body an operation left and the take it left beside it (the
'after' side). It is offered when the body's WavStat agrees with that take
- a loop needs its take kept in the store, an empty slot needs none - which
is the primitive's own gate. Both restores, the tab's and the window's,
hand over only that take. Test fixtures now write bodies the way the card
does: a slot with a loop says so in WavStat.
The first sighting commits slot by slot, so a slot it photographed is whole
while the sighting runs and after it was cut off at another slot. Its row
read as the interruption instead - the sighting's note in place of the
take's name, "interrupted" in the warning colour - until the next connect.
That slot's row is now its Original, finished; the card's row still says
the sighting was interrupted, and only a slot the sighting failed to read
says so on its own row.

A slot counts as empty at first sight only when its body says it holds no
loop too: a body that claims one beside an empty folder was not seen
empty, and keeps its Original row.
The menu kept the row's operation, and a swap's operation is a row of both
its slots: with the menu open on slot 4, selecting slot 9 turned "Clear
history of this slot..." onto slot 9 and "Restore this state" onto slot 9's
row. The menu now carries its slot. A choice made once the pane shows
another slot does nothing, and the restore and the export say which slot
they were about, so the app acts only while that slot is still selected.
clockWords used strftime's %b, which answers in the process locale: under
another one the History tab's column and a Restored row's "back to" would
change alphabet, out of a JUCE-free header whose words are ASCII. It names
the month itself now. A moment before the epoch is rounded down to its
second, not toward zero. And a restore whose target the store cannot
resolve - a reference planted past the foreign key - no longer reads the
missing time as 0 ("back to 01:00"): no row, no time.
…aracters

The partial file was built from path::string() - the narrow code page on
Windows, where a folder named outside it gets another path or a throw - and
a write that failed came back as "cannot write take.wav.part", the name the
player never chose. The partial is the chosen path with ".part" appended
in its own characters, a refusal names the chosen file in UTF-8, and the
partial is removed whatever the write threw.
Lines are joined before any token is read - CRLF and CR end a line like
LF, and a backslash at the end of a line joins it to the next - so an
escape split by a continuation (\x\<newline>e2) is caught, and a // comment
continued under CRLF hides the next line as it does under LF. A quote with
no partner on its line is not a literal the compiler reads (an apostrophe
in #error text, prose under #if 0), and no longer makes a valid file
uncheckable. .cxx, .hh, .hxx and .m files are read too.
…page

The Windows lane failed export_take_job_tests: the test's folder and file
names were typed into u8"..." literals, and MSVC, which reads a source in
the system's code page unless told otherwise, compiled other bytes than
clang did - the file landed under that other name and the job's words
named it. The same holds for any literal: inside fromUTF8 a typed dash
survives only because cp1252 happens to map its three bytes back.

The lint now requires an escape for every byte above 0x7f in every
literal - narrow or wide, u8, a character, inside fromUTF8 or marked as
data - and the three typed ones in the tree are escapes.
@AliceLafox
AliceLafox merged commit e043dc0 into main Oct 25, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant