Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ is not part of this repository.
button stayed off. Such a selection cannot be carried out, and the sheet now says so, with the
entries named above it and a sentence saying to deselect them or deal with each one on its own.
A plan for one such entry still asks for its name, as before.
- While a plan is being carried out, Escape and the close mark on the sheet no longer put the
sheet away. Putting it away used to let a second plan be carried out beside the first, after
which Interrupt and the guard against closing the window followed only the second one. The close
mark is greyed while the run goes on and says why. A preview or the details of a row asked for
during a run are refused, with the reason in the line at the bottom of the window.
- A plan that could not be carried out at all no longer reports "Done. All 0 entries are where you
asked." The sheet says nothing was carried out.
- The list is read again after a plan has run even when the window was in the middle of another
reading at that moment. The Startup type column used to keep showing the old value until F5.
F5 pressed while signatures are being verified is carried out once they are done, instead of
doing nothing.
- After a reading of the list fails once and the next one works, the window says so straight away.
It used to go on saying it could not read the list until something on the machine changed.

## [0.3.0] - 2026-09-25

Expand Down
29 changes: 21 additions & 8 deletions src/Bws.Gui/MainWindow.Carrying.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,16 @@ internal Func<BulkPlan, TimeSpan, CancellationToken, Action<PlanStep, int>, Task
/// a statement about pixels, and the press that matters is the one arriving while a run is
/// already going - from a second click, or from the keyboard, at the moment the first one has
/// not yet reached the screen.
///
/// <b>And the run itself is asked about, not only the panel, since 2026-09-29</b> - G-1 of the
/// external stability report. Until then the panel could be put away mid-run, which lowered the
/// flag this reads, and a second run started beside the first. The panel now refuses that
/// (Planned.CanClose), and this refuses independently, because the fields below belong to ONE
/// run and a second one would overwrite them.
/// </summary>
internal async Task<bool> CarryOut()
{
if (!_model.Planned.CanCarryOut || _model.Planned.Plan is not { } plan)
if (_running is { IsCompleted: false } || !_model.Planned.CanCarryOut || _model.Planned.Plan is not { } plan)
{
return false;
}
Expand Down Expand Up @@ -147,13 +153,6 @@ internal async Task<bool> CarryOut()
_running = running;

_model.Planned.Finished(await running.ConfigureAwait(true));

// Whatever moved, moved. Asking now rather than waiting up to a second means the list
// agrees with the panel by the time somebody looks up from it. KEEPING what is known
// about files since 2026-09-29 - the plan wrote none, `ADR-13`.
await _model.LoadKeepingAsync().ConfigureAwait(true);

return true;
}
catch (InvalidOperationException refusal)
{
Expand Down Expand Up @@ -184,6 +183,20 @@ internal async Task<bool> CarryOut()
// A no-op on every ordinary path, because Finished has already lowered the flag.
_model.Planned.NoLongerRunning();
}

// Whatever moved, moved. Asking now rather than waiting up to a second means the list
// agrees with the panel by the time somebody looks up from it. KEEPING what is known about
// files since 2026-09-29 - the plan wrote none, `ADR-13`.
//
// OUTSIDE THE TRY SINCE 2026-09-29 - G-2 of the external stability report. Inside it, an
// InvalidOperationException from refreshing the LIST landed in the catch meant for a refused
// PLAN, which recorded an empty run over the real one - so a run that had changed the
// machine was reported as "Done" over nothing, and its failures and way back were gone. A
// refresh that throws now goes to the window's own net and the report stays. It also runs
// after the fields above are cleared, so the run is over before the list is asked again.
await _model.LoadKeepingAsync().ConfigureAwait(true);

return true;
}

/// <summary>
Expand Down
10 changes: 10 additions & 0 deletions src/Bws.Gui/MainWindow.Details.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ internal bool OpenDetailsOf(EntryRow entry)
{
ArgumentNullException.ThrowIfNull(entry);

// THE SHEET KEEPS THE COLUMN WHILE ITS RUN IS GOING, since 2026-09-29 - G-1, argued at
// Planned.CanClose. It refuses to be put away, so opening the details beside it would put
// two panels in one column.
if (_model.Planned.Busy)
{
_model.Says.CouldNotDo(Texts.Of("gui.plan.blocked.running"));

return false;
}

PointAt(entry);

_model.Chosen.Row = entry;
Expand Down
11 changes: 10 additions & 1 deletion src/Bws.Gui/MainWindow.Keyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,16 @@ internal bool Act(Shortcut shortcut, out Task work)
// innermost thing there is to back out of, and it is open only while the keyboard
// is in the box - so the press that closes it is the press somebody makes with a
// query they are still typing, which is the one Escape must not take away.
return _model.Suggesting.Close() || _model.Planned.Hide() || _model.Chosen.Hide() || _model.ClearQuery();
//
// A RUN THAT IS GOING TAKES THE PRESS AND DOES NOTHING WITH IT, since 2026-09-29 -
// G-1, argued at Planned.CanClose. The sheet refuses to go away, and without this the
// press would fall through that refusal to the query and empty it under a running
// plan. The sheet's own line already says a run is going, and Interrupt is on it.
return _model.Suggesting.Close()
|| _model.Planned.Busy
|| _model.Planned.Hide()
|| _model.Chosen.Hide()
|| _model.ClearQuery();

default:
return false;
Expand Down
10 changes: 10 additions & 0 deletions src/Bws.Gui/MainWindow.Menu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,16 @@ internal bool OpenDetailsOfPointed() =>
/// </param>
internal async Task<bool> Preview(ActionKind kind, StartSetting? to = null, bool alsoStop = false)
{
// NOT WHILE A PLAN IS BEING CARRIED OUT, since 2026-09-29 - G-1, argued at
// Planned.CanClose. The sheet would refuse the new plan anyway, and this says so where a
// menu item can say anything at all: the status line, rule 8.
if (_model.Planned.Busy)
{
_model.Says.CouldNotDo(Texts.Of("gui.plan.blocked.running"));

return false;
}

var picked = PickedRows();
var names = Everything(picked);

Expand Down
6 changes: 5 additions & 1 deletion src/Bws.Gui/PlanView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,17 @@
<!--
NAMED SINCE 2026-09-07, because it is where the keyboard goes when a sheet that
can end a process opens without a box to type into. See PlanView.WayIn.

GREYED WHILE A RUN IS GOING, SINCE 2026-09-29 - Planned.CanClose says why. Its
two tooltips live in the style, because the one for the grey state comes from a
trigger there, and a tooltip set here would outrank it.
-->
<Button Grid.Column="1"
x:Name="PlanCloseButton"
Style="{StaticResource PlanClose}"
AutomationProperties.AutomationId="planClose"
AutomationProperties.Name="{DynamicResource gui.plan.close}"
ToolTip="{DynamicResource gui.plan.close.hint}"
IsEnabled="{Binding CanClose}"
VerticalAlignment="Top"
Click="CloseRequested" />
</Grid>
Expand Down
1 change: 1 addition & 0 deletions src/Bws.Gui/Resources/gui.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
"gui.plan.notice.done.many": "Done. All {0} entries are where you asked.",
"gui.plan.notice.partly.one": "The entry is not where you asked. What did not is below.",
"gui.plan.notice.partly.many": "{0} of {1} entries are where you asked. What did not is below.",
"gui.plan.notice.nothingRun": "Nothing was carried out. The line at the bottom of the window says why.",

"gui.plan.steps": "In this order",
"gui.plan.step": "{0}. {1} {2} {3}",
Expand Down
20 changes: 20 additions & 0 deletions src/Bws.Gui/Themes/Plan.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@
<Setter Property="Foreground" Value="{StaticResource TextSubdued}" />
<Setter Property="Padding" Value="{StaticResource PadCell}" />

<!--
THE TOOLTIP IS HERE RATHER THAN ON THE BUTTON SINCE 2026-09-29, and so is the promise
that it shows while greyed. A disabled control serves no tooltip unless it is told to
(`docs/10`), and the grey state is the one that has something to explain.
-->
<Setter Property="ToolTip" Value="{DynamicResource gui.plan.close.hint}" />
<Setter Property="ToolTipService.ShowOnDisabled" Value="True" />

<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
Expand Down Expand Up @@ -95,6 +103,18 @@
<Setter TargetName="Face" Property="Background"
Value="{StaticResource SurfaceSelected}" />
</Trigger>

<!--
GREY WHILE A RUN IS GOING (Planned.CanClose). The mark is already drawn
in the subdued text colour, which is what every other button here turns
to when greyed, so this one goes one step fainter - the edge the primary
action wears when it is greyed. Inactive controls are outside the WCAG
contrast rule, and the tooltip carries the reason.
-->
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource SurfacePanelEdge}" />
<Setter Property="ToolTip" Value="{DynamicResource gui.plan.blocked.running}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
Expand Down
27 changes: 24 additions & 3 deletions src/Bws.Gui/ViewModels/Planned.Running.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,27 @@ public sealed partial class Planned
public bool Busy
{
get => _busy;
private set => Set(ref _busy, value);
private set
{
if (Set(ref _busy, value))
{
Raise(nameof(CanClose));
}
}
}

/// <summary>
/// Whether the sheet may be put away - never while a run it carries is going on.
///
/// <b>THE CLOSE MARK USED TO BE LIVE THROUGH A RUN, AND SO DID ESCAPE - G-1 OF THE EXTERNAL
/// STABILITY REPORT, 2026-09-29.</b> Putting the sheet away dropped the plan and lowered
/// <see cref="Busy"/> while the run went on underneath, so the button to carry out came back live
/// and a second run could start beside the first. The window's close guard and Interrupt then
/// followed whichever run had started last, and the first one's report landed under the second
/// one's plan. Interrupt is the way out of a run, and it is on the sheet for as long as one lasts.
/// </summary>
public bool CanClose => !Busy;

/// <summary>
/// Which step is happening, while it happens.
///
Expand Down Expand Up @@ -186,9 +204,12 @@ internal void Finished(BulkRun run)
/// Three sentences on screen, all false, and the failure itself in the status line under them.
///
/// <b>How bad that was is smaller than it looks, and saying so is the point of measuring
/// rather than reasoning.</b> <see cref="Show"/> and <see cref="Hide"/> both lower the flag,
/// rather than reasoning.</b> <see cref="Show"/> and <see cref="Hide"/> both lowered the flag,
/// so Escape or a second plan already brought the panel back - what nothing did was tell the
/// person that, or stop the panel lying in the meantime.
/// person that, or stop the panel lying in the meantime. <b>Since 2026-09-29 neither of them
/// may touch a sheet whose run is going</b> (G-1, at <see cref="CanClose"/>), so this method is
/// now the ONLY way back out of a run that ended without a result, and the window calls it from
/// a finally for exactly that reason.
///
/// <b>NOT Finished with an empty run, which was the other candidate and is the worse one.</b>
/// That is what the one named refusal does, and there it is honest: a plan with problems never
Expand Down
17 changes: 14 additions & 3 deletions src/Bws.Gui/ViewModels/Planned.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,14 @@ public IReadOnlyList<PlanWarningLine> Warnings
/// <b>The count that decides is the number of entries, never the number that arrived.</b> A run
/// of one that did not arrive reports zero arrived out of one, so keying on the first number
/// would put "0 of 1 entries" back on the screen by a different route.
///
/// <b>A run of nothing says so, since 2026-09-29</b> - G-2 of the external stability report. The
/// one refusal the window records as a run carries no runs at all, and 0 arrived of 0 read as
/// "Done. All 0 entries are where you asked."
/// </summary>
private static string Reported(BulkRun run) => Arrived(run) == run.Runs.Count
private static string Reported(BulkRun run) =>
run.Runs.Count == 0 ? Texts.Of("gui.plan.notice.nothingRun")
: Arrived(run) == run.Runs.Count
? run.Runs.Count == 1
? Texts.Of("gui.plan.notice.done.one")
: Texts.Of("gui.plan.notice.done.many", run.Runs.Count)
Expand Down Expand Up @@ -414,7 +420,10 @@ internal bool Show(BulkPlan plan, string? shownAs = null, string? because = null
{
ArgumentNullException.ThrowIfNull(plan);

if (!plan.IsRunnable && plan.Problems.Count == 0)
// NOT OVER A RUN THAT IS STILL GOING, since 2026-09-29 - G-1, argued at CanClose. The run's
// report is bound to the plan it was started from, and a new plan put here mid-run would
// take the button, the report and the way back away from it.
if (Busy || (!plan.IsRunnable && plan.Problems.Count == 0))
{
return false;
}
Expand Down Expand Up @@ -464,10 +473,12 @@ internal bool Show(BulkPlan plan, string? shownAs = null, string? because = null
/// as it was at one moment, so one held after the panel closed would be an answer about a machine
/// that has moved on - and the next thing to open the panel would have to remember not to trust
/// it. Escape asks this, and so does the button.
///
/// <b>And it says no while a run is going</b> - G-1, argued at <see cref="CanClose"/>.
/// </summary>
internal bool Hide()
{
if (!Showing)
if (!Showing || Busy)
{
return false;
}
Expand Down
Loading
Loading