Skip to content

Commit d85dfdf

Browse files
committed
Bump version 6.5.40 Traffic light in the app bar showing what R is doing
1 parent 0663784 commit d85dfdf

9 files changed

Lines changed: 324 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Visit [togaware](https://rattle.togaware.com) for details.
1717

1818
## 6.6 Review and Consolidate
1919

20+
+ Traffic light in the app bar showing what R is doing [6.5.40 20260816 gjw]
2021
+ EVALUATE: Interactive prediction of an observation [6.5.39 20260816 gjw]
2122
+ Centre the plot and restore its footer [6.5.38 20260815 gjw]
2223
+ Fix blurry plots and enlarge the plot display [6.5.37 20260815 gjw]

lib/home.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import 'package:rattle/utils/reset.dart';
6767
import 'package:rattle/utils/show_dataset_alert_dialog.dart';
6868
import 'package:rattle/utils/show_ok.dart';
6969
import 'package:rattle/utils/show_settings_dialog.dart';
70+
import 'package:rattle/widgets/r_status_light.dart';
7071
import 'package:rattle/widgets/status_bar.dart';
7172

7273
// Define the [NavigationRail] tabs for the home page.
@@ -454,6 +455,12 @@ class RattleHomeState extends ConsumerState<RattleHome>
454455

455456
// Deploy the buttons aligned to the top right for actions.
456457
actions: [
458+
// 20260816 gjw Issue #1173. Show what R is doing, since the app
459+
// itself cannot show it: the panels simply have nothing new until R
460+
// has finished, which looks the same as an app that has stopped.
461+
462+
const RStatusLight(),
463+
457464
if (_isVersionLoaded)
458465
VersionWidget(
459466
version: _appVersion,

lib/providers/pty.dart

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
/// Authors: Graham Williams
2525
library;
2626

27+
import 'dart:async';
2728
import 'dart:convert';
2829

2930
import 'package:flutter/material.dart';
@@ -34,6 +35,7 @@ import 'package:universal_io/io.dart' show Platform;
3435
import 'package:xterm/xterm.dart';
3536

3637
import 'package:rattle/app.dart';
38+
import 'package:rattle/providers/r_status.dart';
3739
import 'package:rattle/providers/stdout.dart';
3840
import 'package:rattle/providers/terminal.dart';
3941
import 'package:rattle/utils/clean_string.dart';
@@ -61,12 +63,48 @@ final ptyProvider = StateProvider<Pty>((ref) {
6163

6264
bool missingPackageNotified = false;
6365

66+
// Drive the traffic light of the app bar from what R actually reports, which
67+
// is the only honest account of what R is doing: `rSource()` hands the code
68+
// to the pty and returns, so the app itself does not know when R is
69+
// finished. (gjw 20260816)
70+
//
71+
// R prints its `> ` prompt before echoing each statement it reads, so the
72+
// console ending with a prompt does not on its own mean R is finished, and
73+
// watching for it alone would flicker green all the way through a script.
74+
// Instead we wait for R to go quiet: each chunk of output restarts the timer
75+
// below, and only when nothing more has arrived for [rIdleDelay], with the
76+
// console sitting at a prompt, is R actually waiting for us again.
77+
//
78+
// The alternative, submitting a marker command after each script and
79+
// watching for it to be echoed back, would be exact but would write Rattle's
80+
// own bookkeeping into the user's CONSOLE.
81+
82+
Timer? idleTimer;
83+
6484
pty.output.cast<List<int>>().transform(const Utf8Decoder()).listen((data) {
6585
terminal.write(data);
6686
// debugPrint('update stdoutProvider');
67-
final String accumulated = ref.read(stdoutProvider) + cleanString(data);
87+
final String cleaned = cleanString(data);
88+
final String accumulated = ref.read(stdoutProvider) + cleaned;
6889
ref.read(stdoutProvider.notifier).state = accumulated;
6990

91+
// An R error leaves the light red until the next script is run. R carries
92+
// on with the rest of the submitted code after an error at the top level,
93+
// so the prompt returning does not mean all was well.
94+
95+
if (rErrorReported.hasMatch(cleaned)) {
96+
ref.read(rStatusProvider.notifier).state = RStatus.failed;
97+
}
98+
99+
idleTimer?.cancel();
100+
idleTimer = Timer(rIdleDelay, () {
101+
final bool atPrompt = ref.read(stdoutProvider).endsWith('> ');
102+
103+
if (atPrompt && ref.read(rStatusProvider) == RStatus.running) {
104+
ref.read(rStatusProvider.notifier).state = RStatus.ready;
105+
}
106+
});
107+
70108
// If R reports a package that is not installed (or fails to load), the
71109
// downstream objects are never created and the display panel renders blank
72110
// (the "grey screen"), or throws in debug mode. Catch that here, where all
@@ -112,6 +150,11 @@ final ptyProvider = StateProvider<Pty>((ref) {
112150

113151
pty.exitCode.then((code) {
114152
terminal.write('the process exited with exit code $code');
153+
154+
// R is gone, so nothing can run until the app is restarted or reset.
155+
156+
idleTimer?.cancel();
157+
ref.read(rStatusProvider.notifier).state = RStatus.failed;
115158
});
116159

117160
terminal.onOutput = (data) {

lib/providers/r_status.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/// A provider of the status of the R process, for the traffic light display.
2+
///
3+
/// Time-stamp: "Sunday 2026-08-16 09:20:00 +1000 Graham Williams"
4+
///
5+
/// Copyright (C) 2026, Togaware Pty Ltd.
6+
///
7+
/// Licensed under the GNU General Public License, Version 3 (the "License");
8+
///
9+
/// License: https://opensource.org/license/gpl-3-0
10+
///
11+
// This program is free software: you can redistribute it and/or modify it under
12+
// the terms of the GNU General Public License as published by the Free Software
13+
// Foundation, either version 3 of the License, or (at your option) any later
14+
// version.
15+
//
16+
// This program is distributed in the hope that it will be useful, but WITHOUT
17+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18+
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
19+
// details.
20+
//
21+
// You should have received a copy of the GNU General Public License along with
22+
// this program. If not, see <https://opensource.org/license/gpl-3-0>.
23+
///
24+
/// Authors: Graham Williams
25+
26+
library;
27+
28+
import 'package:flutter_riverpod/legacy.dart';
29+
30+
/// What the R process is doing, as shown by the traffic light in the app bar.
31+
32+
enum RStatus {
33+
/// R is at its prompt, having completed what was asked of it. Green.
34+
35+
ready,
36+
37+
/// R is running our code. Yellow.
38+
39+
running,
40+
41+
/// R reported an error, or the R process is gone. Red.
42+
43+
failed,
44+
}
45+
46+
/// The R process starts out [RStatus.running] because it is: the R session is
47+
/// started when the CONSOLE is first built, and the light turns green when R
48+
/// settles at its prompt, having loaded. (gjw 20260816)
49+
50+
final rStatusProvider = StateProvider<RStatus>((ref) => RStatus.running);
51+
52+
/// How long R has to be quiet before we call it finished.
53+
///
54+
/// Long enough that the gap between R printing a prompt and echoing the next
55+
/// statement of a script does not read as R having finished, and short enough
56+
/// that the light turns green as soon as it has. (gjw 20260816)
57+
58+
const Duration rIdleDelay = Duration(milliseconds: 400);
59+
60+
/// How R reports an error, being `Error: ...` or `Error in <call> : ...` at the
61+
/// start of a line.
62+
///
63+
/// Anchored so that output which merely mentions an error, such as the
64+
/// `Overall Error = 16.67%` of the error matrix, is not mistaken for one.
65+
66+
final RegExp rErrorReported = RegExp(r'^Error(:| in )', multiLine: true);

lib/r/source.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import 'package:rattle/providers/normalise.dart';
4848
import 'package:rattle/providers/number.dart';
4949
import 'package:rattle/providers/partition.dart';
5050
import 'package:rattle/providers/pty.dart';
51+
import 'package:rattle/providers/r_status.dart';
5152
import 'package:rattle/providers/selected.dart';
5253
import 'package:rattle/providers/selected2.dart';
5354
import 'package:rattle/providers/settings.dart';
@@ -621,6 +622,13 @@ Future<void> rSource(
621622
// was 286 lines of code. It may be char length rather than line count that is
622623
// important though. (gjw 20250513)
623624

625+
// 20260816 gjw The traffic light of the app bar goes yellow from here, being
626+
// the moment the code is handed to R, and `providers/pty.dart` turns it green
627+
// again when R settles back at its prompt, or red if R reports an error. This
628+
// also clears a red left by a previous script.
629+
630+
ref.read(rStatusProvider.notifier).state = RStatus.running;
631+
624632
if (lines.length > 200) {
625633
String code1 = '${lines.take(200).join('\n')}\n';
626634
String code2 = lines.skip(200).join('\n');

lib/widgets/r_status_light.dart

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/// A traffic light in the app bar showing what the R process is doing.
2+
///
3+
/// Time-stamp: "Sunday 2026-08-16 09:20:00 +1000 Graham Williams"
4+
///
5+
/// Copyright (C) 2026, Togaware Pty Ltd.
6+
///
7+
/// Licensed under the GNU General Public License, Version 3 (the "License");
8+
///
9+
/// License: https://opensource.org/license/gpl-3-0
10+
///
11+
// This program is free software: you can redistribute it and/or modify it under
12+
// the terms of the GNU General Public License as published by the Free Software
13+
// Foundation, either version 3 of the License, or (at your option) any later
14+
// version.
15+
//
16+
// This program is distributed in the hope that it will be useful, but WITHOUT
17+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18+
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
19+
// details.
20+
//
21+
// You should have received a copy of the GNU General Public License along with
22+
// this program. If not, see <https://opensource.org/license/gpl-3-0>.
23+
///
24+
/// Authors: Graham Williams
25+
26+
library;
27+
28+
import 'package:flutter/material.dart';
29+
30+
import 'package:flutter_riverpod/flutter_riverpod.dart';
31+
import 'package:markdown_tooltip/markdown_tooltip.dart';
32+
33+
import 'package:rattle/providers/r_status.dart';
34+
35+
/// Show whether R is idle, busy, or has stopped.
36+
///
37+
/// Rattle hands our R code to an R session running alongside the app, and until
38+
/// R has finished with it the panels have nothing new to show. Without some
39+
/// indication of that, a slow model build is indistinguishable from an app that
40+
/// has stopped responding. The light reports what R is actually doing.
41+
42+
class RStatusLight extends ConsumerWidget {
43+
const RStatusLight({super.key});
44+
45+
@override
46+
Widget build(BuildContext context, WidgetRef ref) {
47+
final RStatus status = ref.watch(rStatusProvider);
48+
49+
final (Color colour, String label, String advice) = switch (status) {
50+
RStatus.ready => (
51+
Colors.green,
52+
'Ready',
53+
'R has finished and is waiting for you. Any output is on the panel '
54+
'behind, with the detail in the **Console** tab.',
55+
),
56+
RStatus.running => (
57+
Colors.amber,
58+
'Running',
59+
'R is running your code. A model over a large dataset can take a '
60+
'while, so the panels will not update until this turns green. '
61+
'Watch the **Console** tab to see how it is going.',
62+
),
63+
RStatus.failed => (
64+
Colors.red,
65+
'Stopped',
66+
'R reported an error, or the R session has stopped. Whatever you '
67+
'last asked for may be missing or incomplete. See the '
68+
'**Console** tab for what R reported. The light turns green '
69+
'again once the next request succeeds.',
70+
),
71+
};
72+
73+
return MarkdownTooltip(
74+
message: '''
75+
76+
**R is $label**
77+
78+
$advice
79+
80+
''',
81+
child: Padding(
82+
padding: const EdgeInsets.symmetric(horizontal: 12),
83+
child: Row(
84+
mainAxisSize: MainAxisSize.min,
85+
children: [
86+
// A ring around the light so that it reads as a light rather than
87+
// as a coloured dot, and so it stays visible on any app bar colour.
88+
89+
Container(
90+
width: 16,
91+
height: 16,
92+
decoration: BoxDecoration(
93+
color: colour,
94+
shape: BoxShape.circle,
95+
border: Border.all(color: Colors.black26),
96+
),
97+
),
98+
const SizedBox(width: 8),
99+
Text(
100+
label,
101+
style: const TextStyle(fontSize: 14),
102+
),
103+
],
104+
),
105+
),
106+
);
107+
}
108+
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: rattle
22
description: Rattle Data Science Next Generation
33
publish_to: 'none'
4-
version: 6.5.39+216
4+
version: 6.5.40+216
55

66
environment:
77
sdk: '>=3.0.5 <4.0.0'

snap/snapcraft.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
name: rattle
1010
base: core22
11-
version: 6.5.39
11+
version: 6.5.40
1212
summary: Rattle Next Generation Data Science
1313
description: |
1414
Rattle is the data scientists toolkit.

0 commit comments

Comments
 (0)