flutter_colab 0.1.0
flutter_colab: ^0.1.0 copied to clipboard
A Dart/Flutter client for Google Colab by Chinmay Nagar. Execute code, manage files, and control Colab runtimes from desktop apps. No native config required.
import 'package:flutter_colab/flutter_colab.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const ColabExampleApp());
}
// ---------------------------------------------------------------------------
// Snippet model
// ---------------------------------------------------------------------------
class CodeSnippet {
const CodeSnippet({
required this.label,
required this.icon,
required this.code,
this.description = '',
});
final String label;
final IconData icon;
final String code;
final String description;
}
const _snippets = [
CodeSnippet(
label: 'Hello World',
icon: Icons.waving_hand,
description: 'Basic print statement',
code: "print('Hello from Colab! 🚀')",
),
CodeSnippet(
label: 'System Info',
icon: Icons.computer,
description: 'Python & OS details',
code: '''import platform, os
print(f"Python : {platform.python_version()}")
print(f"OS : {platform.system()} {platform.release()}")
print(f"CPU : {os.cpu_count()} cores")
print(f"Arch : {platform.machine()}")''',
),
CodeSnippet(
label: 'Check GPU',
icon: Icons.memory,
description: 'NVIDIA GPU info',
code: '!nvidia-smi',
),
CodeSnippet(
label: 'Install Package',
icon: Icons.download,
description: 'pip install example',
code: '!pip install requests -q\nprint("requests installed ✓")',
),
CodeSnippet(
label: 'Uninstall Package',
icon: Icons.delete_outline,
description: 'pip uninstall example',
code: '!pip uninstall requests -y\nprint("requests removed ✓")',
),
CodeSnippet(
label: 'List Packages',
icon: Icons.list,
description: 'Show installed packages',
code: '!pip list',
),
CodeSnippet(
label: 'Disk Space',
icon: Icons.storage,
description: 'Available disk space',
code: '!df -h',
),
CodeSnippet(
label: 'Memory Info',
icon: Icons.speed,
description: 'RAM usage',
code: '''import psutil
mem = psutil.virtual_memory()
print(f"Total : {mem.total / 1024**3:.1f} GB")
print(f"Available: {mem.available / 1024**3:.1f} GB")
print(f"Used : {mem.percent}%")''',
),
CodeSnippet(
label: 'Math Example',
icon: Icons.calculate,
description: 'NumPy computation',
code: '''import numpy as np
a = np.array([1, 2, 3, 4, 5])
print(f"Array : {a}")
print(f"Mean : {np.mean(a)}")
print(f"Sum : {np.sum(a)}")
print(f"Std : {np.std(a):.4f}")''',
),
CodeSnippet(
label: 'Custom Code',
icon: Icons.edit,
description: 'Write your own code',
code: '# Write your Python code here\n',
),
];
// ---------------------------------------------------------------------------
// App
// ---------------------------------------------------------------------------
class ColabExampleApp extends StatelessWidget {
const ColabExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Colab Client',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: const Color(0xFFF9AB00), // Colab orange-yellow
brightness: Brightness.dark,
useMaterial3: true,
fontFamily: 'monospace',
),
home: const ColabHomePage(),
);
}
}
// ---------------------------------------------------------------------------
// Home page
// ---------------------------------------------------------------------------
enum _ConnState { disconnected, connecting, connected }
class ColabHomePage extends StatefulWidget {
const ColabHomePage({super.key});
@override
State<ColabHomePage> createState() => _ColabHomePageState();
}
class _ColabHomePageState extends State<ColabHomePage> {
final _client = ColabClient();
final _codeCtrl = TextEditingController(
text: "print('Hello from Colab! 🚀')",
);
final _outputScrollCtrl = ScrollController();
_ConnState _connState = _ConnState.disconnected;
bool _executing = false;
String _statusMsg = 'Not connected';
final _outputItems = <_OutputItem>[];
// ---------------------------------------------------------------------------
// Actions
// ---------------------------------------------------------------------------
Future<void> _loginAndConnect() async {
setState(() {
_connState = _ConnState.connecting;
_statusMsg = 'Logging in…';
});
try {
await _client.login();
setState(() => _statusMsg = 'Assigning runtime…');
await _client.connect();
setState(() => _statusMsg = 'Starting kernel…');
await _client.openSession();
_client.startKeepAlive();
setState(() {
_connState = _ConnState.connected;
_statusMsg = 'Connected · ${_client.assignment?.endpoint ?? ''}';
});
} catch (e) {
setState(() {
_connState = _ConnState.disconnected;
_statusMsg = 'Error: $e';
});
}
}
Future<void> _disconnect() async {
_client.stopKeepAlive();
await _client.disconnect();
setState(() {
_connState = _ConnState.disconnected;
_statusMsg = 'Disconnected';
});
}
Future<void> _execute() async {
if (_connState != _ConnState.connected) {
_addOutput(_OutputItem.stderr('Not connected. Click "Connect" first.'));
return;
}
final code = _codeCtrl.text.trim();
if (code.isEmpty) return;
setState(() {
_executing = true;
_outputItems.clear();
});
try {
await for (final out in _client.executeStream(code)) {
_OutputItem? item;
switch (out) {
case StdoutOutput(:final text):
item = _OutputItem.stdout(text);
case StderrOutput(:final text):
item = _OutputItem.stderr(text);
case ErrorOutput(:final error):
item = _OutputItem.error('${error.name}: ${error.value}');
case ResultOutput(:final data):
item = _OutputItem.result(data['text/plain']?.toString() ?? '');
case DisplayDataOutput():
item = _OutputItem.result('[display data]');
}
setState(() => _outputItems.add(item!));
_scrollToBottom();
}
} catch (e) {
setState(() => _outputItems.add(_OutputItem.error(e.toString())));
} finally {
setState(() => _executing = false);
}
}
void _loadSnippet(CodeSnippet s) {
_codeCtrl.text = s.code;
_outputItems.clear();
setState(() {});
}
void _addOutput(_OutputItem item) => setState(() => _outputItems.add(item));
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_outputScrollCtrl.hasClients) {
_outputScrollCtrl.animateTo(
_outputScrollCtrl.position.maxScrollExtent,
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
);
}
});
}
// ---------------------------------------------------------------------------
// Build
// ---------------------------------------------------------------------------
@override
void dispose() {
_client.close();
_codeCtrl.dispose();
_outputScrollCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFF1E1E1E),
appBar: _buildAppBar(cs),
body: Row(
children: [
// Left: snippet sidebar
_SnippetSidebar(snippets: _snippets, onSelect: _loadSnippet),
// Right: main area
Expanded(
child: Column(
children: [
// Status bar
_StatusBar(state: _connState, message: _statusMsg),
// Code block
Expanded(flex: 4, child: _CodeBlock(controller: _codeCtrl)),
// Divider
const Divider(
height: 1,
thickness: 1,
color: Color(0xFF3C3C3C),
),
// Output block
Expanded(
flex: 3,
child: _OutputBlock(
items: _outputItems,
scrollController: _outputScrollCtrl,
executing: _executing,
),
),
],
),
),
],
),
floatingActionButton: _buildFab(cs),
);
}
AppBar _buildAppBar(ColorScheme cs) {
return AppBar(
backgroundColor: const Color(0xFF2D2D2D),
elevation: 0,
title: Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: const Color(0xFFF9AB00),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(Icons.science, size: 16, color: Colors.black),
),
const SizedBox(width: 10),
const Text(
'Colab Client',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.3,
),
),
],
),
actions: [
// Connect / Disconnect button
if (_connState == _ConnState.disconnected)
Padding(
padding: const EdgeInsets.only(right: 8),
child: FilledButton.icon(
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFF9AB00),
foregroundColor: Colors.black,
),
onPressed: _loginAndConnect,
icon: const Icon(Icons.cloud_upload_outlined, size: 16),
label: const Text('Connect'),
),
)
else if (_connState == _ConnState.connecting)
const Padding(
padding: EdgeInsets.only(right: 16),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
else
Padding(
padding: const EdgeInsets.only(right: 8),
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(color: Colors.redAccent),
),
onPressed: _disconnect,
icon: const Icon(Icons.cloud_off_outlined, size: 16),
label: const Text('Disconnect'),
),
),
],
);
}
Widget _buildFab(ColorScheme cs) {
return FloatingActionButton.extended(
backgroundColor: _connState == _ConnState.connected
? const Color(0xFFF9AB00)
: Colors.grey.shade700,
foregroundColor: Colors.black,
onPressed: _executing ? null : _execute,
icon: _executing
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.black,
),
)
: const Icon(Icons.play_arrow_rounded),
label: Text(_executing ? 'Running…' : 'Run Cell'),
);
}
}
// ---------------------------------------------------------------------------
// Snippet Sidebar
// ---------------------------------------------------------------------------
class _SnippetSidebar extends StatefulWidget {
const _SnippetSidebar({required this.snippets, required this.onSelect});
final List<CodeSnippet> snippets;
final void Function(CodeSnippet) onSelect;
@override
State<_SnippetSidebar> createState() => _SnippetSidebarState();
}
class _SnippetSidebarState extends State<_SnippetSidebar> {
int _selected = 0;
@override
Widget build(BuildContext context) {
return Container(
width: 200,
color: const Color(0xFF252526),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.fromLTRB(12, 14, 12, 6),
child: Text(
'SNIPPETS',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
color: Color(0xFF858585),
letterSpacing: 1.2,
),
),
),
Expanded(
child: ListView.builder(
itemCount: widget.snippets.length,
itemBuilder: (_, i) {
final s = widget.snippets[i];
final active = _selected == i;
return InkWell(
onTap: () {
setState(() => _selected = i);
widget.onSelect(s);
},
child: Container(
color: active
? const Color(0xFF37373D)
: Colors.transparent,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
children: [
Icon(
s.icon,
size: 15,
color: active
? const Color(0xFFF9AB00)
: const Color(0xFF858585),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.label,
style: TextStyle(
fontSize: 12,
color: active
? Colors.white
: const Color(0xFFCCCCCC),
fontWeight: active
? FontWeight.w600
: FontWeight.normal,
),
),
Text(
s.description,
style: const TextStyle(
fontSize: 10,
color: Color(0xFF6A6A6A),
),
),
],
),
),
],
),
),
);
},
),
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// Status Bar
// ---------------------------------------------------------------------------
class _StatusBar extends StatelessWidget {
const _StatusBar({required this.state, required this.message});
final _ConnState state;
final String message;
@override
Widget build(BuildContext context) {
final color = switch (state) {
_ConnState.connected => const Color(0xFF4EC994),
_ConnState.connecting => const Color(0xFFF9AB00),
_ConnState.disconnected => const Color(0xFF858585),
};
final dot = switch (state) {
_ConnState.connected => '●',
_ConnState.connecting => '◌',
_ConnState.disconnected => '○',
};
return Container(
width: double.infinity,
color: const Color(
0xFF007ACC,
).withValues(alpha: state == _ConnState.connected ? 1 : 0.4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Row(
children: [
Text(dot, style: TextStyle(color: color, fontSize: 12)),
const SizedBox(width: 6),
Expanded(
child: Text(
message,
style: const TextStyle(
color: Colors.white70,
fontSize: 11,
fontFamily: 'monospace',
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// Code Block
// ---------------------------------------------------------------------------
class _CodeBlock extends StatelessWidget {
const _CodeBlock({required this.controller});
final TextEditingController controller;
@override
Widget build(BuildContext context) {
return Container(
color: const Color(0xFF1E1E1E),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
color: const Color(0xFF2D2D2D),
child: Row(
children: [
const Icon(Icons.code, size: 14, color: Color(0xFFF9AB00)),
const SizedBox(width: 6),
const Text(
'Code Cell',
style: TextStyle(
fontSize: 11,
color: Color(0xFFCCCCCC),
fontWeight: FontWeight.w600,
),
),
const Spacer(),
// Copy button
_IconAction(
icon: Icons.copy_outlined,
tooltip: 'Copy code',
onTap: () =>
Clipboard.setData(ClipboardData(text: controller.text)),
),
const SizedBox(width: 4),
// Clear button
_IconAction(
icon: Icons.clear,
tooltip: 'Clear',
onTap: () => controller.clear(),
),
],
),
),
// Editor
Expanded(
child: TextField(
controller: controller,
maxLines: null,
expands: true,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 13,
color: Color(0xFFD4D4D4),
height: 1.5,
),
decoration: const InputDecoration(
contentPadding: EdgeInsets.all(14),
border: InputBorder.none,
hintText: '# Write Python code here…',
hintStyle: TextStyle(color: Color(0xFF4A4A4A)),
),
keyboardType: TextInputType.multiline,
),
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// Output Block
// ---------------------------------------------------------------------------
class _OutputBlock extends StatelessWidget {
const _OutputBlock({
required this.items,
required this.scrollController,
required this.executing,
});
final List<_OutputItem> items;
final ScrollController scrollController;
final bool executing;
@override
Widget build(BuildContext context) {
return Container(
color: const Color(0xFF1A1A1A),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
color: const Color(0xFF252526),
child: Row(
children: [
Icon(
executing ? Icons.hourglass_top_rounded : Icons.terminal,
size: 14,
color: executing
? const Color(0xFFF9AB00)
: const Color(0xFF4EC994),
),
const SizedBox(width: 6),
Text(
executing ? 'Executing…' : 'Output',
style: const TextStyle(
fontSize: 11,
color: Color(0xFFCCCCCC),
fontWeight: FontWeight.w600,
),
),
const Spacer(),
if (items.isNotEmpty)
_IconAction(
icon: Icons.delete_sweep_outlined,
tooltip: 'Clear output',
onTap: () {},
),
],
),
),
// Output content
Expanded(
child: items.isEmpty && !executing
? const Center(
child: Text(
'Run a cell to see output here',
style: TextStyle(color: Color(0xFF4A4A4A), fontSize: 12),
),
)
: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.all(12),
itemCount: items.length + (executing ? 1 : 0),
itemBuilder: (_, i) {
if (i == items.length) {
return const Padding(
padding: EdgeInsets.only(top: 4),
child: Row(
children: [
SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: Color(0xFFF9AB00),
),
),
SizedBox(width: 8),
Text(
'running…',
style: TextStyle(
color: Color(0xFF858585),
fontSize: 11,
),
),
],
),
);
}
return _OutputLine(item: items[i]);
},
),
),
],
),
);
}
}
class _OutputLine extends StatelessWidget {
const _OutputLine({required this.item});
final _OutputItem item;
@override
Widget build(BuildContext context) {
final color = switch (item.type) {
_OutputType.stdout => const Color(0xFFD4D4D4),
_OutputType.stderr => const Color(0xFFCE9178),
_OutputType.error => const Color(0xFFF44747),
_OutputType.result => const Color(0xFF4EC994),
};
final prefix = switch (item.type) {
_OutputType.stdout => '',
_OutputType.stderr => '⚠ ',
_OutputType.error => '✗ ',
_OutputType.result => '→ ',
};
return Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
'$prefix${item.text}',
style: TextStyle(
color: color,
fontFamily: 'monospace',
fontSize: 12,
height: 1.5,
),
),
);
}
}
// ---------------------------------------------------------------------------
// Small icon action button
// ---------------------------------------------------------------------------
class _IconAction extends StatelessWidget {
const _IconAction({
required this.icon,
required this.tooltip,
required this.onTap,
});
final IconData icon;
final String tooltip;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Tooltip(
message: tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(4),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(icon, size: 14, color: const Color(0xFF858585)),
),
),
);
}
}
// ---------------------------------------------------------------------------
// Output item model
// ---------------------------------------------------------------------------
enum _OutputType { stdout, stderr, error, result }
class _OutputItem {
const _OutputItem._(this.type, this.text);
factory _OutputItem.stdout(String t) => _OutputItem._(_OutputType.stdout, t);
factory _OutputItem.stderr(String t) => _OutputItem._(_OutputType.stderr, t);
factory _OutputItem.error(String t) => _OutputItem._(_OutputType.error, t);
factory _OutputItem.result(String t) => _OutputItem._(_OutputType.result, t);
final _OutputType type;
final String text;
}