Unverified Commit 487bd690 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Support disabling animations in the CLI (#132239)

parent d8470f71
......@@ -124,6 +124,14 @@ Future<void> main(List<String> args) async {
windows: globals.platform.isWindows,
);
},
Terminal: () {
return AnsiTerminal(
stdio: globals.stdio,
platform: globals.platform,
now: DateTime.now(),
isCliAnimationEnabled: featureFlags.isCliAnimationEnabled,
);
},
PreRunValidator: () => PreRunValidator(fileSystem: globals.fs),
},
shutdownHooks: globals.shutdownHooks,
......
......@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:args/command_runner.dart';
......
......@@ -259,8 +259,12 @@ class CommandHelpOption {
message.write(''.padLeft(width - parentheticalText.length));
message.write(_terminal.color(parentheticalText, TerminalColor.grey));
// Terminals seem to require this because we have both bolded and colored
// a line. Otherwise the next line comes out bold until a reset bold.
// Some terminals seem to have a buggy implementation of the SGR ANSI escape
// codes and seem to require that we explicitly request "normal intensity"
// at the end of the line to prevent the next line comes out bold, despite
// the fact that the line already contains a "normal intensity" code.
// This doesn't make much sense but has been reproduced by multiple users.
// See: https://github.com/flutter/flutter/issues/52204
if (_terminal.supportsColor) {
message.write(AnsiTerminal.resetBold);
}
......
......@@ -36,7 +36,7 @@ abstract class Logger {
/// If true, silences the logger output.
bool quiet = false;
/// If true, this logger supports color output.
/// If true, this logger supports ANSI sequences and animations are enabled.
bool get supportsColor;
/// If true, this logger is connected to a terminal.
......@@ -443,7 +443,7 @@ class StdoutLogger extends Logger {
bool get isVerbose => false;
@override
bool get supportsColor => terminal.supportsColor;
bool get supportsColor => terminal.supportsColor && terminal.isCliAnimationEnabled;
@override
bool get hasTerminal => _stdio.stdinHasTerminal;
......@@ -772,7 +772,7 @@ class BufferLogger extends Logger {
bool get isVerbose => _verbose;
@override
bool get supportsColor => terminal.supportsColor;
bool get supportsColor => terminal.supportsColor && terminal.isCliAnimationEnabled;
final StringBuffer _error = StringBuffer();
final StringBuffer _warning = StringBuffer();
......
......@@ -76,8 +76,14 @@ abstract class Terminal {
factory Terminal.test({bool supportsColor, bool supportsEmoji}) = _TestTerminal;
/// Whether the current terminal supports color escape codes.
///
/// Check [isCliAnimationEnabled] as well before using `\r` or ANSI sequences
/// to perform animations.
bool get supportsColor;
/// Whether to show animations on this terminal.
bool get isCliAnimationEnabled;
/// Whether the current terminal can display emoji.
bool get supportsEmoji;
......@@ -152,6 +158,7 @@ class AnsiTerminal implements Terminal {
required io.Stdio stdio,
required Platform platform,
DateTime? now, // Time used to determine preferredStyle. Defaults to 0001-01-01 00:00.
this.isCliAnimationEnabled = true,
})
: _stdio = stdio,
_platform = platform,
......@@ -199,6 +206,9 @@ class AnsiTerminal implements Terminal {
@override
bool get supportsColor => _platform.stdoutSupportsAnsi;
@override
final bool isCliAnimationEnabled;
// Assume unicode emojis are supported when not on Windows.
// If we are on Windows, unicode emojis are supported in Windows Terminal,
// which sets the WT_SESSION environment variable. See:
......@@ -275,14 +285,14 @@ class AnsiTerminal implements Terminal {
}
@override
String clearScreen() => supportsColor ? clear : '\n\n';
String clearScreen() => supportsColor && isCliAnimationEnabled ? clear : '\n\n';
/// Returns ANSI codes to clear [numberOfLines] lines starting with the line
/// the cursor is on.
///
/// If the terminal does not support ANSI codes, returns an empty string.
String clearLines(int numberOfLines) {
if (!supportsColor) {
if (!supportsColor || !isCliAnimationEnabled) {
return '';
}
return cursorBeginningOfLineCode +
......@@ -402,6 +412,9 @@ class _TestTerminal implements Terminal {
@override
final bool supportsColor;
@override
bool get isCliAnimationEnabled => supportsColor;
@override
final bool supportsEmoji;
......
......@@ -47,6 +47,9 @@ abstract class FeatureFlags {
/// Whether WebAssembly compilation for Flutter Web is enabled.
bool get isFlutterWebWasmEnabled => false;
/// Whether animations are used in the command line interface.
bool get isCliAnimationEnabled => true;
/// Whether a particular feature is enabled for the current channel.
///
/// Prefer using one of the specific getters above instead of this API.
......@@ -64,6 +67,7 @@ const List<Feature> allFeatures = <Feature>[
flutterFuchsiaFeature,
flutterCustomDevicesFeature,
flutterWebWasm,
cliAnimation,
];
/// All current Flutter feature flags that can be configured.
......@@ -122,7 +126,7 @@ const Feature flutterFuchsiaFeature = Feature(
);
const Feature flutterCustomDevicesFeature = Feature(
name: 'Early support for custom device types',
name: 'early support for custom device types',
configSetting: 'enable-custom-devices',
environmentOverride: 'FLUTTER_CUSTOM_DEVICES',
master: FeatureChannelSetting(
......@@ -146,6 +150,14 @@ const Feature flutterWebWasm = Feature(
),
);
/// The [Feature] for CLI animations.
///
/// The TERM environment variable set to "dumb" turns this off.
const Feature cliAnimation = Feature.fullyEnabled(
name: 'animations in the command line interface',
configSetting: 'cli-animations',
);
/// A [Feature] is a process for conditionally enabling tool features.
///
/// All settings are optional, and if not provided will generally default to
......@@ -229,9 +241,9 @@ class Feature {
];
// Add channel info for settings only on some channels.
if (channels.length == 1) {
buffer.write('\nThis setting applies to only the ${channels.single} channel.');
buffer.write('\nThis setting applies only to the ${channels.single} channel.');
} else if (channels.length == 2) {
buffer.write('\nThis setting applies to only the ${channels.join(' and ')} channels.');
buffer.write('\nThis setting applies only to the ${channels.join(' and ')} channels.');
}
if (extraHelpText != null) {
buffer.write(' $extraHelpText');
......
......@@ -47,6 +47,14 @@ class FlutterFeatureFlags implements FeatureFlags {
@override
bool get isFlutterWebWasmEnabled => isEnabled(flutterWebWasm);
@override
bool get isCliAnimationEnabled {
if (_platform.environment['TERM'] == 'dumb') {
return false;
}
return isEnabled(cliAnimation);
}
@override
bool isEnabled(Feature feature) {
final String currentChannel = _flutterVersion.channel;
......
......@@ -25,8 +25,7 @@ const String _kFlutterFirstRunMessage = '''
║ Flutter tool. ║
║ ║
║ By downloading the Flutter SDK, you agree to the Google Terms of Service. ║
║ Note: The Google Privacy Policy describes how data is handled in this ║
║ service. ║
║ The Google Privacy Policy describes how data is handled in this service. ║
║ ║
║ Moreover, Flutter includes the Dart SDK, which may send usage metrics and ║
║ crash reports to Google. ║
......@@ -36,6 +35,8 @@ const String _kFlutterFirstRunMessage = '''
║ ║
║ See Google's privacy policy:
https://policies.google.com/privacy ║
To disable animations in this tool, use 'flutter config --no-animations'.
╚════════════════════════════════════════════════════════════════════════════╝
''';
......
......@@ -238,6 +238,9 @@ class FakeTerminal implements Terminal {
@override
bool get supportsColor => terminal.supportsColor;
@override
bool get isCliAnimationEnabled => terminal.isCliAnimationEnabled;
@override
bool get supportsEmoji => terminal.supportsEmoji;
......
......@@ -882,6 +882,9 @@ class FakeTerminal extends Fake implements AnsiTerminal {
@override
final bool supportsColor;
@override
bool get isCliAnimationEnabled => supportsColor;
@override
bool singleCharMode = false;
......
......@@ -1228,4 +1228,7 @@ class FakeDevice extends Fake implements Device {
class FakeTerminal extends Fake implements AnsiTerminal {
@override
final bool supportsColor = false;
@override
bool get isCliAnimationEnabled => supportsColor;
}
......@@ -79,6 +79,7 @@ void main() {
group('CommandHelp', () {
group('toString', () {
testWithoutContext('ends with a resetBold when it has parenthetical text', () {
// This is apparently required to work around bugs in some terminal clients.
final Platform platform = FakePlatform(stdoutSupportsAnsi: true);
final AnsiTerminal terminal = AnsiTerminal(stdio: FakeStdio(), platform: platform);
......@@ -131,19 +132,19 @@ void main() {
wrapColumn: maxLineWidth,
);
expect(commandHelp.I.toString(), endsWith('\x1B[90m(debugInvertOversizedImages)\x1B[39m\x1B[22m'));
expect(commandHelp.L.toString(), endsWith('\x1B[90m(debugDumpLayerTree)\x1B[39m\x1B[22m'));
expect(commandHelp.P.toString(), endsWith('\x1B[90m(WidgetsApp.showPerformanceOverlay)\x1B[39m\x1B[22m'));
expect(commandHelp.S.toString(), endsWith('\x1B[90m(debugDumpSemantics)\x1B[39m\x1B[22m'));
expect(commandHelp.U.toString(), endsWith('\x1B[90m(debugDumpSemantics)\x1B[39m\x1B[22m'));
expect(commandHelp.a.toString(), endsWith('\x1B[90m(debugProfileWidgetBuilds)\x1B[39m\x1B[22m'));
expect(commandHelp.b.toString(), endsWith('\x1B[90m(debugBrightnessOverride)\x1B[39m\x1B[22m'));
expect(commandHelp.f.toString(), endsWith('\x1B[90m(debugDumpFocusTree)\x1B[39m\x1B[22m'));
expect(commandHelp.i.toString(), endsWith('\x1B[90m(WidgetsApp.showWidgetInspectorOverride)\x1B[39m\x1B[22m'));
expect(commandHelp.o.toString(), endsWith('\x1B[90m(defaultTargetPlatform)\x1B[39m\x1B[22m'));
expect(commandHelp.p.toString(), endsWith('\x1B[90m(debugPaintSizeEnabled)\x1B[39m\x1B[22m'));
expect(commandHelp.t.toString(), endsWith('\x1B[90m(debugDumpRenderTree)\x1B[39m\x1B[22m'));
expect(commandHelp.w.toString(), endsWith('\x1B[90m(debugDumpApp)\x1B[39m\x1B[22m'));
expect(commandHelp.I.toString(), contains('\x1B[90m(debugInvertOversizedImages)\x1B[39m'));
expect(commandHelp.L.toString(), contains('\x1B[90m(debugDumpLayerTree)\x1B[39m'));
expect(commandHelp.P.toString(), contains('\x1B[90m(WidgetsApp.showPerformanceOverlay)\x1B[39m'));
expect(commandHelp.S.toString(), contains('\x1B[90m(debugDumpSemantics)\x1B[39m'));
expect(commandHelp.U.toString(), contains('\x1B[90m(debugDumpSemantics)\x1B[39m'));
expect(commandHelp.a.toString(), contains('\x1B[90m(debugProfileWidgetBuilds)\x1B[39m'));
expect(commandHelp.b.toString(), contains('\x1B[90m(debugBrightnessOverride)\x1B[39m'));
expect(commandHelp.f.toString(), contains('\x1B[90m(debugDumpFocusTree)\x1B[39m'));
expect(commandHelp.i.toString(), contains('\x1B[90m(WidgetsApp.showWidgetInspectorOverride)\x1B[39m'));
expect(commandHelp.o.toString(), contains('\x1B[90m(defaultTargetPlatform)\x1B[39m'));
expect(commandHelp.p.toString(), contains('\x1B[90m(debugPaintSizeEnabled)\x1B[39m'));
expect(commandHelp.t.toString(), contains('\x1B[90m(debugDumpRenderTree)\x1B[39m'));
expect(commandHelp.w.toString(), contains('\x1B[90m(debugDumpApp)\x1B[39m'));
});
testWithoutContext('should not create a help text longer than maxLineWidth without ansi support', () {
......@@ -184,6 +185,7 @@ void main() {
wrapColumn: maxLineWidth,
);
// The trailing \x1B[22m is to work around reported bugs in some terminal clients.
expect(commandHelp.I.toString(), equals('\x1B[1mI\x1B[22m Toggle oversized image inversion. \x1B[90m(debugInvertOversizedImages)\x1B[39m\x1B[22m'));
expect(commandHelp.L.toString(), equals('\x1B[1mL\x1B[22m Dump layer tree to the console. \x1B[90m(debugDumpLayerTree)\x1B[39m\x1B[22m'));
expect(commandHelp.M.toString(), equals('\x1B[1mM\x1B[22m Write SkSL shaders to a unique file in the project directory.'));
......
......@@ -1286,6 +1286,46 @@ void main() {
expect(mockLogger.traceText, 'Oooh, I do I do I do\n');
expect(mockLogger.errorText, 'Helpless!\n$stackTrace\n');
});
testWithoutContext('Animations are disabled when, uh, disabled.', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
isCliAnimationEnabled: false,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40),
);
logger.startProgress('po').stop();
expect(outputStderr(), <String>['']);
expect(outputStdout(), <String>[
'po 0ms',
'',
]);
logger.startProgress('ta')
..pause()
..resume()
..stop();
expect(outputStderr(), <String>['']);
expect(outputStdout(), <String>[
'po 0ms',
'ta ',
'ta 0ms',
'',
]);
logger.startSpinner()
..pause()
..resume()
..stop();
expect(outputStderr(), <String>['']);
expect(outputStdout(), <String>[
'po 0ms',
'ta ',
'ta 0ms',
'',
]);
});
});
}
......
......@@ -2884,6 +2884,9 @@ class FakeTerminal extends Fake implements AnsiTerminal {
@override
final bool supportsColor;
@override
bool get isCliAnimationEnabled => supportsColor;
@override
bool usesTerminalUi = true;
......
......@@ -448,6 +448,7 @@ class TestFeatureFlags implements FeatureFlags {
this.isFuchsiaEnabled = false,
this.areCustomDevicesEnabled = false,
this.isFlutterWebWasmEnabled = false,
this.isCliAnimationEnabled = true,
});
@override
......@@ -477,6 +478,9 @@ class TestFeatureFlags implements FeatureFlags {
@override
final bool isFlutterWebWasmEnabled;
@override
final bool isCliAnimationEnabled;
@override
bool isEnabled(Feature feature) {
switch (feature) {
......@@ -496,6 +500,8 @@ class TestFeatureFlags implements FeatureFlags {
return isFuchsiaEnabled;
case flutterCustomDevicesFeature:
return areCustomDevicesEnabled;
case cliAnimation:
return isCliAnimationEnabled;
}
return false;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment