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