content: // Copyright 2016 Google Inc. Use of this source code is governed by an
  // MIT-style license that can be found in the LICENSE file or at
  // https://opensource.org/licenses/MIT.
  
  import 'dart:async';
  import 'dart:convert';
  import 'dart:io' as io;
  
  import 'package:async/async.dart';
  import 'package:path/path.dart' as p;
  import 'package:source_span/source_span.dart';
  import 'package:watcher/watcher.dart';
  
  import '../exception.dart';
  import '../utils.dart';
  
  export 'dart:io' show exitCode, FileSystemException;
  
  bool get isWindows => io.Platform.isWindows;
  
  bool get isMacOS => io.Platform.isMacOS;
  
  bool get hasTerminal => io.stdout.hasTerminal;
  
  bool get supportsAnsiEscapes {
    if (!hasTerminal) return false;
  
    // We don't trust [io.stdout.supportsAnsiEscapes] except on Windows because it
    // relies on the TERM environment variable which has many false negatives.
    if (!isWindows) return true;
    return io.stdout.supportsAnsiEscapes;
  }
  
  void safePrint(Object? message) {
    print(message);
  }
  
  void printError(Object? message) {
    io.stderr.writeln(message);
  }
  
  String readFile(String path) {
    var bytes = io.File(path).readAsBytesSync();
  
    try {
      return utf8.decode(bytes);
    } on FormatException catch (error, stackTrace) {
      var decodedUntilError = utf8.decode(
        bytes.sublist(0, error.offset),
        allowMalformed: true,
      );
      var stringOffset = decodedUntilError.length;
      if (decodedUntilError.endsWith("�")) stringOffset--;
  
      var decoded = utf8.decode(bytes, allowMalformed: true);
      var sourceFile = SourceFile.fromString(decoded, url: p.toUri(path));
      throwWithTrace(
        SassException(
          "Invalid UTF-8.",
          sourceFile.location(stringOffset).pointSpan(),
        ),
        error,
        stackTrace,
      );
    }
  }
  
  void writeFile(String path, String contents) =>
      io.File(path).writeAsStringSync(contents);
  
  void deleteFile(String path) => io.File(path).deleteSync();
  
  Future<String> readStdin() async {
    var completer = Completer<String>();
    completer.complete(await io.systemEncoding.decodeStream(io.stdin));
    return completer.future;
  }
  
  bool fileExists(String path) => io.File(path).existsSync();
  
  bool dirExists(String path) => io.Directory(path).existsSync();
  
  bool linkExists(String path) => io.Link(path).existsSync();
  
  void ensureDir(String path) => io.Directory(path).createSync(recursive: true);
  
  Iterable<String> listDir(String path, {bool recursive = false}) =>
      io.Directory(path)
          .listSync(recursive: recursive)
          .whereType<io.File>()
          .map((entity) => entity.path);
  
  // The `File()` method works with directories as well.
  String realpath(String path) => io.File(path).resolveSymbolicLinksSync();
  
  DateTime modificationTime(String path) {
    var stat = io.FileStat.statSync(path);
    if (stat.type == io.FileSystemEntityType.notFound) {
      throw io.FileSystemException("File not found.", path);
    }
    return stat.modified;
  }
  
  String? getEnvironmentVariable(String name) => io.Platform.environment[name];
  
  Future<Stream<WatchEvent>> watchDir(String path, {bool poll = false}) async {
    var watcher = poll ? PollingDirectoryWatcher(path) : DirectoryWatcher(path);
  
    // Wrap [stream] in a [SubscriptionStream] so that its `onListen` event
    // triggers but the caller can still listen at their leisure.
    var stream = SubscriptionStream<WatchEvent>(
      watcher.events
          .transform(
            const SingleSubscriptionTransformer<WatchEvent, WatchEvent>(),
          )
          .listen(null),
    );
    await watcher.ready;
  
    return stream;
  }
  