pwsh 1.0.1
pwsh: ^1.0.1 copied to clipboard
A wrapper for XTerm with PTY and Powershell styling
example/lib/main.dart
/// Example app for the `pwsh` plugin.
///
/// The window has a row of demo buttons, a command box and a [PwshTerminal].
/// Each button shows one part of the API:
///
/// * **Log levels**: every `write*` log method on [PwshController].
/// * **Line rules**: how log messages handle missing and trailing newlines,
/// and text that was left mid-line.
/// * **Progress**: in-place updates with `\r` log messages and
/// [PwshController.replaceLine].
/// * **Styled lines**: [PwshController.writeLine] with each [ConsoleTone].
/// * **Run**: runs the command in the box with PowerShell through a
/// [PwshSession], with a Kill button in the terminal toolbar while it runs.
///
/// Everything except Run works without the native PTY library. If the
/// library or PowerShell is missing, Run prints a `FAIL` message explaining
/// what went wrong instead of crashing.
library;
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:pwsh/pwsh.dart';
void main() => runApp(const ExampleApp());
/// The root widget: a dark Material app showing [ConsolePage].
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'pwsh example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
colorSchemeSeed: const Color(0xFF61AFEF),
),
home: const ConsolePage(),
);
}
}
/// A page with demo buttons, a command box and the terminal.
class ConsolePage extends StatefulWidget {
const ConsolePage({super.key});
@override
State<ConsolePage> createState() => _ConsolePageState();
}
class _ConsolePageState extends State<ConsolePage> {
/// The console model. It lives in the state, not the widget tree, so its
/// contents survive rebuilds. 5,000 lines of scrollback is plenty here.
final _console = PwshController(maxLines: 5000);
final _command = TextEditingController(
text: r'$PSVersionTable | Format-Table; Get-ChildItem | Select-Object -First 10',
);
/// The running PowerShell session, or `null` when nothing is running.
/// Used to enable the Kill button and to stop two commands overlapping.
PwshSession? _session;
/// Set while a demo is printing, so the demo buttons can't be pressed
/// again halfway through.
bool _busy = false;
/// PowerShell 7 is `pwsh.exe` on Windows and `pwsh` elsewhere. To use the
/// built-in Windows PowerShell 5.1 instead, change this to
/// `'powershell.exe'`.
static final _shell = Platform.isWindows ? 'pwsh.exe' : 'pwsh';
@override
void initState() {
super.initState();
_console.writeLog('Welcome to the pwsh example.');
_console.writeDebug('Pick a demo above, or type a command and press Run.');
}
@override
void dispose() {
_session?.kill();
_command.dispose();
super.dispose();
}
/// Runs [demo] with the buttons disabled, so demos don't interleave.
Future<void> _runDemo(Future<void> Function() demo) async {
setState(() => _busy = true);
try {
await demo();
} finally {
if (mounted) setState(() => _busy = false);
}
}
/// Prints one message at every log level.
Future<void> _logLevels() async {
_console.writeTrace('logTrace: step-by-step detail');
_console.writeDebug('writeDebug: useful while developing');
_console.writeLog('writeLog: normal information');
_console.writePass('writePass: a step succeeded');
_console.writeWarn('writeWarn: something looks off, carrying on');
_console.writeFail('writeFail: a step failed');
_console.writeDev('writeDev: temporary output while working on a feature');
}
/// Shows that log messages always get exactly one line each.
Future<void> _lineRules() async {
_console.writeLog('No trailing newline: one is added for you');
_console.writeLog('Trailing newline: no extra one is added\n');
_console.writeLog('Multi-line messages\nkeep their inner\nline breaks');
// Leave the cursor mid-line, the way a prompt or progress dots would.
_console.write('Some text without a newline...');
_console.writeWarn('...and this message still starts on its own line');
}
/// Animates two kinds of in-place progress.
Future<void> _progress() async {
const step = Duration(milliseconds: 60);
// 1. Log messages starting with \r redraw the current line, with the
// level label and timestamp redrawn too.
for (var percent = 0; percent <= 100; percent += 5) {
_console.writeLog('\rDownloading packages $percent%');
await Future<void>.delayed(step);
}
// Ending the last update with \n keeps it on screen.
_console.writePass('\rDownloaded packages\n');
// 2. replaceLine redraws a plain line in a tone of your choice, here as a
// text progress bar.
const width = 30;
for (var done = 0; done <= width; done++) {
final bar = '#' * done + '.' * (width - done);
final percent = (done * 100 / width).round();
_console.replaceLine('[$bar] $percent%', tone: ConsoleTone.info);
await Future<void>.delayed(step);
}
// replaceLine leaves the cursor at the end of the line; move on.
_console.ensureLineStart();
_console.writeLine('Build finished', tone: ConsoleTone.success);
}
/// Prints a line in every [ConsoleTone].
Future<void> _styledLines() async {
for (final tone in ConsoleTone.values) {
_console.writeLine('ConsoleTone.${tone.name}', tone: tone);
}
}
/// Runs the command in the text box with PowerShell.
Future<void> _run() async {
final command = _command.text.trim();
if (command.isEmpty || _session != null) return;
_console.writeLine('> $command', tone: ConsoleTone.command);
final PwshSession session;
try {
session = PwshSession.start(
_shell,
// -NoProfile skips the user's profile script, so the example starts
// quickly and behaves the same on every machine. -Command runs the
// text and then exits.
['-NoLogo', '-NoProfile', '-Command', command],
console: _console,
);
} on Object catch (error) {
// Thrown when the native PTY library isn't bundled with the app or
// the pseudo console can't be created. See the README.
_console.writeFail('Could not start $_shell: $error');
return;
}
setState(() => _session = session);
final code = await session.exitCode;
if (!mounted) return;
setState(() => _session = null);
// exitCode completes after the last output has been written, so this
// always appears below the command's output.
if (code == 0) {
_console.writePass('$_shell exited with code 0');
} else {
_console.writeFail('$_shell exited with code $code');
}
}
@override
Widget build(BuildContext context) {
final running = _session != null;
final demosEnabled = !_busy && !running;
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
_DemoButton(
'Log levels',
demosEnabled,
() => _runDemo(_logLevels),
),
_DemoButton(
'Line rules',
demosEnabled,
() => _runDemo(_lineRules),
),
_DemoButton(
'Progress',
demosEnabled,
() => _runDemo(_progress),
),
_DemoButton(
'Styled lines',
demosEnabled,
() => _runDemo(_styledLines),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
child: Row(
children: [
Expanded(
child: TextField(
controller: _command,
enabled: !running,
style: const TextStyle(fontFamily: 'Cascadia Mono'),
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'PowerShell command ($_shell)',
),
onSubmitted: (_) => _run(),
),
),
const SizedBox(width: 8),
FilledButton.icon(
onPressed: demosEnabled ? _run : null,
icon: const Icon(Icons.play_arrow),
label: const Text('Run'),
),
],
),
),
Expanded(
child: PwshTerminal(
_console,
title: running
? 'Running $_shell (pid ${_session!.pid})'
: 'Console',
// Only set while a command runs, so the Kill button only shows
// up when there's something to kill.
onKill: running ? () => _session?.kill() : null,
),
),
],
),
);
}
}
/// A tonal button for one demo.
class _DemoButton extends StatelessWidget {
const _DemoButton(this.label, this.enabled, this.onPressed);
final String label;
final bool enabled;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return FilledButton.tonal(
onPressed: enabled ? onPressed : null,
child: Text(label),
);
}
}