diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a14904..7cc6106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,32 @@ is not part of this repository. ## [Unreleased] +### Added + +- A plan run from the window can now be left undone, the way a second Ctrl+C leaves one in a + terminal. After Interrupt has been pressed and the run is still going three seconds later, a + "Leave the rest undone" button appears to its left: it stops watching the step in flight and puts + nothing back. It exists for a service that keeps reporting progress and never finishes, which + used to leave a window that could only be closed from the task manager. Pressing Interrupt twice + never reaches it. +- When a stop or restart would be refused because other services running on this machine depend on + the one you picked, the plan offers "Also stop the 2 in the way" under the sentence naming them. + Pressing it builds the same plan again with those services stopped first, as `--dependents` does + in a terminal. A force stop refused for the same reason offers the same, and it is the way forward + from that sheet. Nothing is carried out until you press the button at the foot of the plan, and a + restart as administrator keeps the choice. + ### Changed +- Interrupt goes grey once pressed, and the line at the top of the plan says the run was + interrupted and is finishing the step in flight. Until now pressing it left no trace on the screen. +- Closing the window while a plan is being carried out now shows the same thing as Interrupt and + says the window closes when the run has ended. +- A run that was interrupted says so when it ends, and says when nothing was put back, instead of + reading like any run that fell short. +- The line saying which step is running moved under the buttons at the foot of the plan, where it + has the whole width and no longer wraps beside them. + - A minimised window no longer reads the list of services every second. It reads it again the moment it is restored, so the list is current as soon as it is on screen. A window started minimised, from a shortcut set to "Run: Minimized", reads it once and then waits the same way. diff --git a/README.md b/README.md index 954e475..eed89c5 100644 --- a/README.md +++ b/README.md @@ -258,8 +258,9 @@ line that asks for the same thing, and the way back afterwards. *asked for*, or *would break otherwise*. Without `--dry-run` those same steps run in that same order. There is no second code path for the real thing. - **What comes down with it is in the plan.** The services that would break are steps of their own - (`--dependents` on the command line, always in the window), and a plan that would take down - something the machine needs says so in its warnings. + when you ask for them - `--dependents` on the command line, and in the window a button under the + plan's sentence naming them - and a plan that would take down something the machine needs says so + in its warnings. - **A refusal comes before the button.** A driver, a service the manager will not accept a stop for, a process the system protects - refused when the plan is built, with the reason, not after a wait. The window greys the button and says why beside it. diff --git a/src/Bws.Gui/Carrying.cs b/src/Bws.Gui/Carrying.cs index 28df8c0..b9a6222 100644 --- a/src/Bws.Gui/Carrying.cs +++ b/src/Bws.Gui/Carrying.cs @@ -48,11 +48,17 @@ internal static class Carrying /// /// /// Asks the run to stop going forward. The steps that give back what earlier steps took are - /// still carried out, which is the whole difference between this and closing the window. + /// still carried out. + /// + /// + /// Asks the run to stop altogether and put nothing back - the command line's second Ctrl+C. /// - /// Only the first of the command line's two levels is offered. The second - stop and put nothing - /// back - exists there because the alternative was somebody killing the process, and a killed - /// process reports nothing. A window has no such person: closing it asks for this one and waits. + /// This said "only the first of the two levels is offered" until 2026-09-30, on the + /// argument that a window has nobody reaching for the task manager: closing it asked for the + /// first level and waited. Package C of the stability report took that argument away - the step + /// limit counts time without progress, so an entry reporting progress forever is watched + /// forever, and the window then had nothing left to wait for (backlog 497). What guards the + /// second level against a press nobody meant is Underway's rule, not this method. /// /// /// Called before each step that is really attempted, with its place across the whole selection. @@ -62,7 +68,11 @@ internal static class Carrying /// often enough to pass a test and throws on a collection. /// internal static Task Out( - BulkPlan plan, TimeSpan ceiling, CancellationToken stopping, Action announce) => + BulkPlan plan, + TimeSpan ceiling, + CancellationToken stopping, + CancellationToken abandoning, + Action announce) => Task.Run( () => { @@ -73,7 +83,7 @@ internal static Task Out( // one. var runner = new BulkRunner(new PlanRunner(new WindowsScmControl(), new SystemClock())); - return runner.Run(plan, ceiling, stopping, abandonment: default, starting: announce); + return runner.Run(plan, ceiling, stopping, abandoning, starting: announce); }, CancellationToken.None); } diff --git a/src/Bws.Gui/MainWindow.Carrying.cs b/src/Bws.Gui/MainWindow.Carrying.cs index 9bfcb50..8c8946c 100644 --- a/src/Bws.Gui/MainWindow.Carrying.cs +++ b/src/Bws.Gui/MainWindow.Carrying.cs @@ -51,6 +51,12 @@ public partial class MainWindow /// Asks the run in progress to stop before its next step. Null when none is. private CancellationTokenSource? _stopping; + /// + /// Asks the run in progress to stop altogether and put nothing back - the second level, since + /// 2026-09-30 (backlog 497). Null when none is. Cancelled only when Underway agrees. + /// + private CancellationTokenSource? _abandoning; + /// /// How a plan is carried out: , unless somebody handed this window /// another way. @@ -72,7 +78,7 @@ public partial class MainWindow /// Init rather than settable, so a run cannot be swapped out from under a window that /// is already using one. /// - internal Func, Task> CarriedOutBy + internal Func, Task> CarriedOutBy { get; init; @@ -106,15 +112,18 @@ internal async Task CarryOut() } using var stopping = new CancellationTokenSource(); + using var abandoning = new CancellationTokenSource(); _stopping = stopping; + _abandoning = abandoning; var watching = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromSeconds(1) }; - watching.Tick += (_, _) => _model.Planned.Tick(); + // The same tick moves the clock on the line and offers the second level once it is due. + watching.Tick += (_, _) => _model.Planned.Underway.Tick(); try { @@ -139,7 +148,7 @@ internal async Task CarryOut() // thread reach a bound property here rather than there. Built per run rather than kept, // because building it anywhere else would capture whatever thread happened to be there. var announce = new Progress<(PlanStep Step, int Number)>( - what => _model.Planned.Announce(what.Step, what.Number)); + what => _model.Planned.Underway.Announce(what.Step, what.Number)); // THE CEILING COMES FROM THE PANEL RATHER THAN FROM A CONSTANT, SINCE 2026-09-09 - // backlog 330. Read here, at the press, rather than held anywhere: the box is on the @@ -148,6 +157,7 @@ internal async Task CarryOut() plan, TimeSpan.FromSeconds(_model.Planned.Waiting), stopping.Token, + abandoning.Token, (step, number) => ((IProgress<(PlanStep, int)>)announce).Report((step, number))); _running = running; @@ -171,6 +181,7 @@ internal async Task CarryOut() _running = null; _stopping = null; + _abandoning = null; // AND THE PANEL STOPS SAYING A RUN IS UNDER WAY, WHICH UNTIL 2026-09-03 ONLY THE TWO // ENDINGS ABOVE DID - backlog 298. Both of them go through Finished, so anything else @@ -239,8 +250,16 @@ internal void TakeThisAsARun(Task running, CancellationTokenSource stopping) /// /// So the close is refused ONCE, the run is asked to stop, and the close is asked for again /// when it has. The steps that give back what earlier steps took still run, because that is what - /// asking to stop means here - the same first level the command line offers, and deliberately - /// not its second. + /// asking to stop means here - the same first level the command line offers. + /// + /// AND THE SHEET SAYS SO, SINCE 2026-09-30 - backlog 497. Until then this asked the first + /// level and waited with nothing on the screen changing, and since the step limit counts time + /// without progress, an entry reporting progress forever made that wait endless - the window + /// could only be ended from the task manager. The close now goes through the same door as the + /// Interrupt button, so the sheet shows the first level asked, says the window will close when + /// the run has ended, and offers the second level on the sheet after the same wait. A second + /// reach for the corner is not the second level - that is the same double-press Underway guards + /// against on the button. /// protected override async void OnClosing(System.ComponentModel.CancelEventArgs e) { @@ -260,6 +279,7 @@ protected override async void OnClosing(System.ComponentModel.CancelEventArgs e) // Skipping it here is the ordinary shape of a cancelled close rather than a shortcut: the // event means "this window is closing", and after this line it is not. e.Cancel = true; + _model.Planned.Underway.Interrupt(closing: true); _stopping?.Cancel(); await running.ConfigureAwait(true); @@ -278,6 +298,28 @@ protected override async void OnClosing(System.ComponentModel.CancelEventArgs e) /// Reached from two files, that story would be told in one and relied on in the other. /// /// Not async: cancelling is instant, and the waiting belongs to whoever is awaiting the run. + /// + /// Both levels through one door since 2026-09-30 (backlog 497), and a flag rather than a + /// second method because the window stands on the ceiling of methods a type may have. The first + /// level is asked whatever the sheet says - the test seam hands this window runs with the sheet + /// idle, and stopping a run must never depend on a picture of it. The second is asked only when + /// Underway agrees, which is the whole of what keeps a press nobody meant from leaving a machine + /// with nothing put back. /// - internal void AskTheRunToStop() => _stopping?.Cancel(); + /// The second level: stop watching and put nothing back. + internal void AskTheRunToStop(bool leaveTheRestUndone = false) + { + if (leaveTheRestUndone) + { + if (_model.Planned.Underway.Abandon()) + { + _abandoning?.Cancel(); + } + + return; + } + + _model.Planned.Underway.Interrupt(closing: false); + _stopping?.Cancel(); + } } diff --git a/src/Bws.Gui/MainWindow.HandOver.cs b/src/Bws.Gui/MainWindow.HandOver.cs index 4baddc3..8a8ebeb 100644 --- a/src/Bws.Gui/MainWindow.HandOver.cs +++ b/src/Bws.Gui/MainWindow.HandOver.cs @@ -18,7 +18,7 @@ public partial class MainWindow /// The plan that is open, as it was asked for - remembered at the moment it opens, because the /// sheet itself keeps the answer and not the question. /// - private (ActionKind Kind, StartSetting? To, bool AlsoStop)? _asked; + private (ActionKind Kind, StartSetting? To, bool AlsoStop, bool Dependents)? _asked; /// /// The first reading, and everything that has to wait for it. Out of the constructor since @@ -64,7 +64,8 @@ private async Task FirstLook() Picked = [.. PickedRows().Select(row => row.ServiceName)], Asked = _model.Planned.Showing ? _asked?.Kind : null, To = _model.Planned.Showing ? _asked?.To : null, - AlsoStop = _model.Planned.Showing && _asked?.AlsoStop == true + AlsoStop = _model.Planned.Showing && _asked?.AlsoStop == true, + Dependents = _model.Planned.Showing && _asked?.Dependents == true }; /// @@ -114,7 +115,7 @@ internal async Task TakeOverAsync(HandOver? carried, bool refused) if (found > 0 && carried.Asked is { } kind) { - await Preview(kind, carried.To, carried.AlsoStop).ConfigureAwait(true); + await Preview(kind, carried.To, carried.AlsoStop, carried.Dependents).ConfigureAwait(true); } } diff --git a/src/Bws.Gui/MainWindow.Menu.cs b/src/Bws.Gui/MainWindow.Menu.cs index 5216504..53955ed 100644 --- a/src/Bws.Gui/MainWindow.Menu.cs +++ b/src/Bws.Gui/MainWindow.Menu.cs @@ -194,8 +194,9 @@ internal bool OpenDetailsOfPointed() => /// The cascade is NOT asked for, which matches the command line's own default and is a safety /// property rather than a preference. Asking to stop one service is not asking to stop seven. /// What the plan does instead is name the ones in the way, in a warning, so `C2`'s promise about - /// saying what would be dragged in is kept without quietly widening what was asked. There is no - /// control for turning it on yet and that is open rather than decided. + /// saying what would be dragged in is kept without quietly widening what was asked. Since + /// 2026-09-30 the sheet offers to ask again with them - the owner's decision on W-4 of the + /// stability report - and that offer arrives here as , never a menu. /// /// What was asked for, in the words a person used. /// @@ -208,7 +209,10 @@ internal bool OpenDetailsOfPointed() => /// Whether the setting carries a stop - the offer under "keeps running" was taken, which asks /// this same question again over the same rows with this one word changed. /// - internal async Task Preview(ActionKind kind, StartSetting? to = null, bool alsoStop = false) + /// + /// Whether the running dependants in the way are stopped first - the offer under them was taken. + /// + internal async Task Preview(ActionKind kind, StartSetting? to = null, bool alsoStop = false, bool dependents = 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 @@ -269,7 +273,8 @@ internal async Task Preview(ActionKind kind, StartSetting? to = null, bool // MainViewModel.PlanAsync. The whole listing selected and asked to stop was measured at // 224-240 ms, all of it round trips to the manager, and the owner's decision was that a // window not answering for that long is too much. - var plan = await _model.PlanAsync(new BulkAction(kind, names, To: to, AlsoStop: alsoStop)).ConfigureAwait(true); + var plan = await _model.PlanAsync( + new BulkAction(kind, names, IncludeDependents: dependents, To: to, AlsoStop: alsoStop)).ConfigureAwait(true); if (asked != _previews) { @@ -290,7 +295,7 @@ internal async Task Preview(ActionKind kind, StartSetting? to = null, bool if (shown) { // The question behind the open sheet, for a restart as administrator to ask again. - _asked = (kind, to, alsoStop); + _asked = (kind, to, alsoStop, dependents); PlanPanel.TakeTheKeyboard(); } @@ -299,25 +304,78 @@ internal async Task Preview(ActionKind kind, StartSetting? to = null, bool } /// - /// Takes up the offer under "keeps running": the same ask over the same rows, with the stop - /// riding on it - so the sheet on the screen becomes the same sheet with a second step. + /// Takes up an offer under a sentence of the plan: the same ask over the same rows with one word + /// changed - so the sheet on the screen becomes the same sheet with more steps. /// /// THE SAME SHEET HERE, WHERE THE OFFER UNDER A FAILURE OPENS A NEW ONE, and the difference /// is the one names. That sheet is a record of a run and may not grow a /// second question. This one has not run - it is still the question, and taking the offer - /// changes the question before anybody answers it. PlanWarningLine offers nothing on a record. + /// changes the question before anybody answers it. PlanSentence offers nothing on a record. /// - /// Only from the shape the offer stands under: a start type plan setting Disabled that - /// does not already stop. Anything else answers false and opens nothing. + /// Only from the shape each offer stands under. The stop rides on a start type plan + /// setting Disabled that does not already stop. The dependants join any other plan that did not + /// already ask for them - PlanSentence decides which plans offer it, and a startup setting is + /// not one of them. Anything else answers false and opens nothing. /// /// And only while the sheet still offers it - asked of the same lines that draw the /// button, so the door and the button cannot disagree. Until review of PR #17 the shape was /// the only check, and a press during a run rebuilt the plan under the run still going. + /// + /// One door for both since 2026-09-30, and that is the ceiling of methods on this window + /// asking rather than tidiness: a second offer would otherwise have been a second method. + /// + /// THE DEPENDANTS ARE ASKED OVER THE NAMES OF THE PLAN ON THE SHEET, NOT OVER THE PICKED ROWS + /// - found by the second analysis of its design. A forcing sheet opened from a failure + /// () is about the entry that failed, which can be a dependant of the row that + /// was picked. Asked again over the picked rows, the offer would have built a plan for a different + /// entry than the one on the screen - the preview and the question parting company, which is the + /// worst fault this product can have. The first offer keeps going through Preview: a startup + /// setting is only ever opened from the picked rows. /// - internal async Task AlsoStop() => - _asked is { Kind: ActionKind.SetStartType, To: StartSetting.Disabled, AlsoStop: false } - && _model.Planned.Warnings.Any(line => line.HasOffer) - && await Preview(ActionKind.SetStartType, StartSetting.Disabled, alsoStop: true).ConfigureAwait(true); + internal async Task TakeTheOffer(PlanOffer offer) + { + if (_asked is not { } asked + || _model.Planned.Plan is not { } shown + || !_model.Planned.Warnings.Concat(_model.Planned.Problems).Any(line => line.HasOffer && line.Offer == offer)) + { + return false; + } + + if (offer == PlanOffer.AlsoStop) + { + return asked is { Kind: ActionKind.SetStartType, To: StartSetting.Disabled, AlsoStop: false } + && await Preview(ActionKind.SetStartType, StartSetting.Disabled, alsoStop: true).ConfigureAwait(true); + } + + if (offer != PlanOffer.InTheWay || asked.Dependents || shown.Action.IncludeDependents) + { + return false; + } + + var again = shown.Action with { IncludeDependents = true }; + var ticket = ++_previews; + var plan = await _model.PlanAsync(again).ConfigureAwait(true); + + // The same counter every preview goes through, for the reason Force gives. + if (ticket != _previews) + { + return false; + } + + var shownAs = again.ServiceNames.Count != 1 ? null : _model.Rows + .FirstOrDefault(row => string.Equals(row.ServiceName, again.ServiceNames[0], StringComparison.OrdinalIgnoreCase)) + ?.DisplayName; + + if (!_model.Planned.Show(plan, shownAs)) + { + return false; + } + + _asked = asked with { Dependents = true }; + PlanPanel.TakeTheKeyboard(); + + return true; + } /// /// Takes up the offer under a failure: closes the sheet reporting it and opens a new one @@ -379,7 +437,7 @@ internal async Task Force(PlanFailure failure) if (shown) { - _asked = (offer.Kind, null, false); + _asked = (offer.Kind, null, false, false); // WHERE THE KEYBOARD LANDS IS PART OF THIS SLICE RATHER THAN A COURTESY. This is the // one sheet in the window whose main button ends a process, so Enter arriving on it diff --git a/src/Bws.Gui/MainWindow.xaml.cs b/src/Bws.Gui/MainWindow.xaml.cs index 30b59cd..0489385 100644 --- a/src/Bws.Gui/MainWindow.xaml.cs +++ b/src/Bws.Gui/MainWindow.xaml.cs @@ -294,6 +294,9 @@ private void ListenToThePlanSheet() // with two stories. The wiring stays here, where every other event of this panel is wired. PlanPanel.InterruptRequest += (_, _) => AskTheRunToStop(); + // The second level through the same door - which asks Underway before it cancels anything. + PlanPanel.AbandonRequest += (_, _) => AskTheRunToStop(leaveTheRestUndone: true); + // The clipboard rather than the panel, for the reason beside CopyRequest over there: only // the window can say a copy was refused, because only the window owns the status line. PlanPanel.CopyRequest += (_, asked) => Put(asked.Command); @@ -303,10 +306,11 @@ private void ListenToThePlanSheet() // asks the manager and that moved off this thread on 2026-09-03. PlanPanel.ForceRequest += async (_, asked) => await Force(asked.Failure).ConfigureAwait(true); - // THE OFFER UNDER "KEEPS RUNNING" - spec C4, the owner's variant O1 of 2026-09-24. The same - // question asked again with the stop riding on it, so the sheet that answers is this one - // with a second step rather than a second sheet somebody has to remember to open. - PlanPanel.AlsoStopRequest += async (_, _) => await AlsoStop().ConfigureAwait(true); + // THE OFFERS UNDER A SENTENCE OF THE PLAN - "keeps running" (spec C4, the owner's variant O1 + // of 2026-09-24) and dependants in the way (W-4, 2026-09-30). The same question asked again + // with one word changed, so the sheet that answers is this one rather than a second sheet + // somebody has to remember to open. + PlanPanel.OfferRequest += async (_, asked) => await TakeTheOffer(asked.Offer).ConfigureAwait(true); } /// diff --git a/src/Bws.Gui/PlanFooter.xaml b/src/Bws.Gui/PlanFooter.xaml index 3df4145..925d21c 100644 --- a/src/Bws.Gui/PlanFooter.xaml +++ b/src/Bws.Gui/PlanFooter.xaml @@ -37,6 +37,10 @@ safety: a verb that changes under a finger turns a double click into an instruction nobody gave. It appears to the LEFT, so the rectangle somebody aimed at never becomes something else - it becomes disabled. + + THE SECOND WAY OF STOPPING FOLLOWS THE SAME RULE ONE STEP FURTHER, since 2026-09-30 (backlog + 497): it appears to the left of Interrupt, only after Interrupt was pressed and has gone grey, + and only once the run has kept going for three seconds more. Underway says why each half. --> + + @@ -189,7 +199,7 @@ narrow paragraph beside two buttons. --> + internal event EventHandler? InterruptRequest; + /// + /// Somebody asked a run already interrupted to stop watching and put nothing back - the second + /// level, since 2026-09-30 (backlog 497). Whether it may is Underway's answer, not this control's. + /// + internal event EventHandler? AbandonRequest; + public PlanFooter() => InitializeComponent(); /// @@ -82,4 +88,7 @@ private void CarryOutRequested(object sender, RoutedEventArgs e) => private void InterruptRequested(object sender, RoutedEventArgs e) => InterruptRequest?.Invoke(this, EventArgs.Empty); + + private void AbandonRequested(object sender, RoutedEventArgs e) => + AbandonRequest?.Invoke(this, EventArgs.Empty); } diff --git a/src/Bws.Gui/PlanView.xaml b/src/Bws.Gui/PlanView.xaml index b117462..35cd052 100644 --- a/src/Bws.Gui/PlanView.xaml +++ b/src/Bws.Gui/PlanView.xaml @@ -259,7 +259,7 @@ + ItemTemplate="{StaticResource PlanSentenceTemplate}" /> ? ForceRequest; /// - /// Somebody took the offer under "keeps running": the same ask again, with the stop as a second - /// step. An event for the reason the one above gives - building a plan means asking the manager + /// Somebody asked a run already interrupted to stop watching and put nothing back. Passed on + /// unchanged, like the two presses above it. + /// + internal event EventHandler? AbandonRequest; + + /// + /// Somebody took an offer under a sentence of the plan, and it carries which: the stop riding on + /// a startup setting, or the dependants in the way stopped first. The same ask again with one word + /// changed. An event for the reason the ones above give - building a plan means asking the manager /// off this thread, and that lives where every other preview is opened. /// - internal event EventHandler? AlsoStopRequest; + internal event EventHandler? OfferRequest; /// /// Somebody asked for one of the terminal commands, and it carries which one. @@ -74,22 +81,33 @@ public PlanView() // outside these two lines had to learn that the ask lives somewhere else now. Footer.CarryOutRequest += (_, _) => CarryOutRequest?.Invoke(this, EventArgs.Empty); Footer.InterruptRequest += (_, _) => InterruptRequest?.Invoke(this, EventArgs.Empty); + Footer.AbandonRequest += (_, _) => AbandonRequest?.Invoke(this, EventArgs.Empty); - // CAUGHT ON THE LIST RATHER THAN NAMED IN THE MARKUP, which is the other way this sheet + // CAUGHT ON THE LISTS RATHER THAN NAMED IN THE MARKUP, which is the other way this sheet // catches a press from a template - the failures name theirs in PlanView.xaml. This file's // markup stands at the size ceiling, so the handler is wired here and the markup carries - // only a name. Only a line that carries the offer has a button, so any press here is it. - WarningList.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(AlsoStopRequested)); + // only a name. Both lists draw PlanSentence with one template since 2026-09-30, and the + // line under the finger says which offer it is. + WarningList.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(OfferRequested)); + ProblemList.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(OfferRequested)); } - private void AlsoStopRequested(object sender, RoutedEventArgs e) + private void OfferRequested(object sender, RoutedEventArgs e) { - if (e.OriginalSource is not Button { Tag: PlanWarningLine { HasOffer: true } }) + if (e.OriginalSource is not Button { Tag: PlanSentence { HasOffer: true } line }) { return; } - AlsoStopRequest?.Invoke(this, EventArgs.Empty); + OfferRequest?.Invoke(this, new OfferAsked(line.Offer)); + } + + /// + /// Which offer was taken. A type for one value for the reason gives. + /// + internal sealed class OfferAsked(PlanOffer offer) : EventArgs + { + internal PlanOffer Offer { get; } = offer; } private void CloseRequested(object sender, RoutedEventArgs e) => Dismiss(); @@ -356,7 +374,7 @@ internal void TakeTheKeyboard() => [.. StepList.Items.OfType().Select(line => line.Text)]; /// The entries that got no plan, as they reach the screen. - internal IReadOnlyList ProblemLines => [.. ProblemList.Items.OfType()]; + internal IReadOnlyList ProblemLines => [.. ProblemList.Items.OfType().Select(line => line.Text)]; /// /// The command lines, as they reach the screen - one box each up to twenty, one field past that @@ -395,14 +413,16 @@ private static IReadOnlyList Shown(ItemsControl list, TextBox field) => /// What is worth knowing, as it reaches the screen. internal IReadOnlyList WarningLines => - [.. WarningList.Items.OfType().Select(line => line.Text)]; + [.. WarningList.Items.OfType().Select(line => line.Text)]; /// - /// The offer under those sentences, as it reaches the screen - read off the items for the reason - /// gives about the failures. + /// The offers under the sentences of both lists - what is worth knowing and what was refused - as + /// they reach the screen, read off the items for the reason gives about + /// the failures. Named for the first offer until 2026-09-30, when the second arrived. /// - internal IReadOnlyList AlsoStopLines => - [.. WarningList.Items.OfType().Where(line => line.HasOffer).Select(line => line.Label)]; + internal IReadOnlyList SentenceOffers => + [.. WarningList.Items.OfType().Concat(ProblemList.Items.OfType()) + .Where(line => line.HasOffer).Select(line => line.Label)]; /// /// Whether the refusals section is on the screen at all, heading included. diff --git a/src/Bws.Gui/Resources/gui.en.json b/src/Bws.Gui/Resources/gui.en.json index 8f78441..b06ca53 100644 --- a/src/Bws.Gui/Resources/gui.en.json +++ b/src/Bws.Gui/Resources/gui.en.json @@ -236,6 +236,13 @@ "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.notice.interrupting": "Interrupted. Finishing the step in flight, then putting back what this run took down.", + "gui.plan.notice.abandoning": "Leaving the rest undone. Nothing is put back, and the run ends as soon as Windows answers what it is being asked now.", + "gui.plan.notice.closing": "The window closes when the run has ended.", + "gui.plan.notice.interrupted.one": "Interrupted, so the entry is not where you asked. The steps after that point were not tried.", + "gui.plan.notice.interrupted.many": "Interrupted, so {0} of {1} entries are where you asked. The steps after that point were not tried.", + "gui.plan.notice.leftAsItWas.one": "Interrupted and left as it was, so the entry is not where you asked. What this run took down was not put back.", + "gui.plan.notice.leftAsItWas.many": "Interrupted and left as it was, so {0} of {1} entries are where you asked. What this run took down was not put back.", "gui.plan.steps": "In this order", "gui.plan.step": "{0}. {1} {2} {3}", @@ -337,6 +344,9 @@ "gui.plan.offer.alsoStop.one": "Also stop it", "gui.plan.offer.alsoStop.many": "Also stop the {0} that are running", "gui.plan.offer.alsoStop.hint": "Add a step to this plan that stops each running entry after its startup type is set. Nothing happens until you press the button at the foot.", + "gui.plan.offer.inTheWay.one": "Also stop the one in the way", + "gui.plan.offer.inTheWay.many": "Also stop the {0} in the way", + "gui.plan.offer.inTheWay.hint": "Build this plan again with the running entries that depend on it stopped first, as --dependents does from a terminal. Nothing happens until you press the button at the foot.", "gui.plan.confirm.type": "Type {0} to confirm", "gui.plan.confirm.hint": "The line above says what this would do. Typing the name out is how you say you have read it.", "gui.plan.confirm.selection": "A selection cannot do this to the entries named above. Deselect them, or do it to each one on its own.", @@ -344,6 +354,9 @@ "gui.plan.blocked.notSeconds": "The box above is not a number of seconds. Type a whole number, at least 1, to turn this on.", "gui.plan.interrupt": "Interrupt", "gui.plan.interrupt.hint": "Stop before the next step. Anything already taken down is still put back.", + "gui.plan.interrupt.asked": "Asked. The step in flight is watched to its end, then what this run took down is put back.", + "gui.plan.abandon": "Leave the rest undone", + "gui.plan.abandon.hint": "Stop watching the step in flight and put nothing back. What this run took down stays down, and the sheet says how to put it back.", "gui.plan.progress": "Step {0} of {1}: {2} {3}", "gui.plan.progress.waiting": "{0} - {1} s so far, given up on after {2} s with no progress", "gui.plan.waiting.label": "Wait up to", diff --git a/src/Bws.Gui/Themes/PlanLines.xaml b/src/Bws.Gui/Themes/PlanLines.xaml index 16a8256..f69524c 100644 --- a/src/Bws.Gui/Themes/PlanLines.xaml +++ b/src/Bws.Gui/Themes/PlanLines.xaml @@ -74,27 +74,11 @@ - - -