feat(kanban): add dew kanban tui interactive board TUI

Add an interactive Trello-style kanban TUI powered by dart_console.

Features:
- Side-by-side column layout, adapts to terminal width
- Keyboard navigation: j/k (up/down), h/l or ←/→ (switch column)
- Move selected ticket between columns with < and >
- Ticket cards show ID, type badge (colour-coded), title, labels/milestone
- Scroll indicators (↑ N above / ↓ N below) when a column overflows
- Column headers highlighted in their configured colour; active column
  uses filled background + double-line border
- Live filter with / (fuzzy search across id, title, type, labels, body)
- Detail view (Enter): full ticket info, body and comments with word-wrap,
  scrollable with j/k
- r to reload tickets from disk without leaving the TUI
- Graceful terminal restoration on exit (Esc / q)
- Requires an interactive terminal; prints a clear error otherwise

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Chris Hendrickson 2026-04-25 03:02:39 -04:00
parent dff35598f5
commit c25f32dc12
4 changed files with 821 additions and 0 deletions

View file

@ -0,0 +1,770 @@
import 'dart:math';
import 'package:dart_console/dart_console.dart';
import 'package:dew_core/dew_core.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import '../kanban_config.dart';
import '../ticket.dart';
import '../ticket_store.dart';
enum _Mode { board, detail }
/// A single rendered cell: text + optional styling.
class _Cell {
final String text;
final ConsoleColor? fg;
final ConsoleColor? bg;
final bool bold;
const _Cell(this.text, {this.fg, this.bg, this.bold = false});
}
/// Interactive Trello-style kanban TUI.
class TuiCommand extends DewCommand {
final FileSystem _fs;
TuiCommand({FileSystem fs = const LocalFileSystem()}) : _fs = fs;
@override
final String name = 'tui';
@override
final String description = 'Launch the interactive kanban board TUI.';
// Layout constants
static const _colSep = 1; // gap between columns (chars)
static const _minColW = 26; // minimum column width
static const _colHeaderH = 3; // rows used by column header box
static const _ticketH = 3; // rows per ticket card
static const _indicatorRows = 2; // rows reserved for scroll indicators
@override
Future<void> run() async {
final context = await ProjectContext.find(fs: _fs);
final config = context.config.kanban;
final store = TicketStore(
kanbanDir: context.dirs.kanban,
prefix: config.prefix,
fs: context.fs,
);
final console = Console();
if (!console.hasTerminal) {
print('dew kanban tui requires an interactive terminal.');
return;
}
var tickets = await store.list();
var byColumn = _groupByColumn(tickets, config);
var colIdx = 0;
var ticketIdx = 0;
var mode = _Mode.board;
var detailScroll = 0;
var statusMsg = '';
var searchQuery = '';
var searchMode = false;
console.hideCursor();
console.rawMode = true;
void redraw() {
final w = max(console.windowWidth, 40);
final h = max(console.windowHeight, 12);
console.clearScreen();
console.resetCursorPosition();
if (mode == _Mode.board) {
_renderBoard(
console: console,
config: config,
byColumn: byColumn,
colIdx: colIdx,
ticketIdx: ticketIdx,
statusMsg: statusMsg,
searchQuery: searchQuery,
searchMode: searchMode,
w: w,
h: h,
);
} else {
final col = config.columns[colIdx];
final colTickets = _filtered(byColumn[col.id] ?? [], searchQuery);
if (colTickets.isEmpty) {
mode = _Mode.board;
} else {
_renderDetail(
console: console,
ticket: colTickets[ticketIdx.clamp(0, colTickets.length - 1)],
config: config,
scroll: detailScroll,
w: w,
h: h,
);
}
}
}
try {
redraw();
loop:
while (true) {
final key = console.readKey();
// Search mode
if (searchMode) {
if (key.isControl) {
switch (key.controlChar) {
case ControlCharacter.enter:
searchMode = false;
ticketIdx = 0;
case ControlCharacter.escape:
searchMode = false;
searchQuery = '';
ticketIdx = 0;
case ControlCharacter.backspace:
if (searchQuery.isNotEmpty) {
searchQuery = searchQuery.substring(0, searchQuery.length - 1);
}
default:
{}
}
} else {
searchQuery += key.char;
}
redraw();
continue;
}
// Board mode
if (mode == _Mode.board) {
final col = config.columns[colIdx];
final colTickets = _filtered(byColumn[col.id] ?? [], searchQuery);
if (!key.isControl) {
switch (key.char) {
case 'q':
break loop;
case 'j':
if (ticketIdx < colTickets.length - 1) ticketIdx++;
case 'k':
if (ticketIdx > 0) ticketIdx--;
case 'h':
if (colIdx > 0) {
colIdx--;
ticketIdx = 0;
}
case 'l':
if (colIdx < config.columns.length - 1) {
colIdx++;
ticketIdx = 0;
}
case '<':
if (colIdx > 0 && colTickets.isNotEmpty) {
final t = colTickets[ticketIdx];
final newColId = config.columns[colIdx - 1].id;
try {
await store.update(t.id, column: newColId);
tickets = await store.list();
byColumn = _groupByColumn(tickets, config);
colIdx--;
final nt = _filtered(byColumn[newColId] ?? [], searchQuery);
ticketIdx = max(0, nt.length - 1);
statusMsg = 'Moved ${t.id}${config.columns[colIdx].name}';
} on ArgumentError catch (e) {
statusMsg = 'Error: ${e.message ?? e}';
}
}
case '>':
if (colIdx < config.columns.length - 1 && colTickets.isNotEmpty) {
final t = colTickets[ticketIdx];
final newColId = config.columns[colIdx + 1].id;
try {
await store.update(t.id, column: newColId);
tickets = await store.list();
byColumn = _groupByColumn(tickets, config);
colIdx++;
final nt = _filtered(byColumn[newColId] ?? [], searchQuery);
ticketIdx = max(0, nt.length - 1);
statusMsg = 'Moved ${t.id}${config.columns[colIdx].name}';
} on ArgumentError catch (e) {
statusMsg = 'Error: ${e.message ?? e}';
}
}
case 'r':
tickets = await store.list();
byColumn = _groupByColumn(tickets, config);
statusMsg = 'Reloaded.';
case '/':
searchMode = true;
searchQuery = '';
ticketIdx = 0;
default:
continue loop; // skip redraw
}
} else {
switch (key.controlChar) {
case ControlCharacter.arrowUp:
if (ticketIdx > 0) ticketIdx--;
case ControlCharacter.arrowDown:
if (ticketIdx < colTickets.length - 1) ticketIdx++;
case ControlCharacter.arrowLeft:
if (colIdx > 0) {
colIdx--;
ticketIdx = 0;
}
case ControlCharacter.arrowRight:
if (colIdx < config.columns.length - 1) {
colIdx++;
ticketIdx = 0;
}
case ControlCharacter.enter:
if (colTickets.isNotEmpty) {
mode = _Mode.detail;
detailScroll = 0;
}
case ControlCharacter.escape:
searchQuery = '';
statusMsg = '';
default:
continue loop; // skip redraw
}
}
}
// Detail mode
else {
if (!key.isControl) {
switch (key.char) {
case 'q':
break loop;
case 'b':
mode = _Mode.board;
detailScroll = 0;
case 'j':
detailScroll++;
case 'k':
if (detailScroll > 0) detailScroll--;
default:
continue loop;
}
} else {
switch (key.controlChar) {
case ControlCharacter.escape:
mode = _Mode.board;
detailScroll = 0;
case ControlCharacter.arrowUp:
if (detailScroll > 0) detailScroll--;
case ControlCharacter.arrowDown:
detailScroll++;
default:
continue loop;
}
}
}
redraw();
}
} finally {
console.rawMode = false;
console.showCursor();
console.clearScreen();
console.resetCursorPosition();
}
}
//
// Board rendering
//
static void _renderBoard({
required Console console,
required KanbanConfig config,
required Map<String, List<Ticket>> byColumn,
required int colIdx,
required int ticketIdx,
required String statusMsg,
required String searchQuery,
required bool searchMode,
required int w,
required int h,
}) {
_writeHeaderBar(console, config, searchQuery, searchMode, w);
console.writeLine(); // blank line after header
final numCols = config.columns.length;
// How many columns fit side-by-side?
final numVisible = max(1, min(numCols, (w + _colSep) ~/ (_minColW + _colSep)));
final colW = (w - (numVisible - 1) * _colSep) ~/ numVisible;
// Keep selected column in viewport
var viewStart = colIdx - numVisible ~/ 2;
viewStart = viewStart.clamp(0, max(0, numCols - numVisible));
if (colIdx < viewStart) viewStart = colIdx;
if (colIdx >= viewStart + numVisible) viewStart = colIdx - numVisible + 1;
viewStart = viewStart.clamp(0, max(0, numCols - numVisible));
final viewCols = config.columns.sublist(viewStart, min(viewStart + numVisible, numCols));
// h - 2 (header+blank) - 2 (footer separator+help) = content height
final colAreaH = h - 4;
// Build column cell buffers (one list per column, each list has colAreaH cells)
final colCells = <List<_Cell>>[];
for (int i = 0; i < viewCols.length; i++) {
final col = viewCols[i];
final isSelected = (viewStart + i) == colIdx;
final tickets = _filtered(byColumn[col.id] ?? [], searchQuery);
colCells.add(
_buildColumnCells(
col: col,
tickets: tickets,
colW: colW,
areaH: colAreaH,
isSelected: isSelected,
selectedIdx: isSelected ? ticketIdx : -1,
),
);
}
// Render side-by-side, row by row
for (int row = 0; row < colAreaH; row++) {
for (int ci = 0; ci < colCells.length; ci++) {
if (ci > 0) console.write(' ' * _colSep);
final cell = colCells[ci][row];
_applyCell(console, cell);
console.resetColorAttributes();
}
if (row < colAreaH - 1) {
console.writeLine();
}
}
_writeFooterBar(
console: console,
statusMsg: statusMsg,
searchMode: searchMode,
searchQuery: searchQuery,
w: w,
colIdx: colIdx,
numCols: numCols,
numVisible: numVisible,
columns: config.columns,
);
}
static List<_Cell> _buildColumnCells({
required ColumnConfig col,
required List<Ticket> tickets,
required int colW,
required int areaH,
required bool isSelected,
required int selectedIdx,
}) {
final cells = <_Cell>[];
final color = _colColor(col.color);
final innerW = colW - 2;
// Column header (3 rows)
// Top border
cells.add(_Cell(
'${'' * innerW}',
fg: isSelected ? color : ConsoleColor.brightBlack,
));
// Name + count
final count = tickets.length;
final label = _trunc(' ${col.name.toUpperCase()} ($count) ', innerW);
cells.add(_Cell(
'${label.padRight(innerW)}',
fg: isSelected ? ConsoleColor.black : color,
bg: isSelected ? color : null,
bold: isSelected,
));
// Separator (double line for selected, single for others)
cells.add(_Cell(
isSelected ? '${'' * innerW}' : '${'' * innerW}',
fg: isSelected ? color : ConsoleColor.brightBlack,
));
// Ticket area
final ticketAreaH = areaH - _colHeaderH - 1; // reserve 1 for bottom border
final maxVisible = max(1, (ticketAreaH - _indicatorRows) ~/ _ticketH);
// Compute scroll to keep selectedIdx in view
var scroll = 0;
if (isSelected && selectedIdx >= 0 && tickets.isNotEmpty) {
scroll = (selectedIdx - maxVisible + 1).clamp(0, max(0, tickets.length - maxVisible));
if (selectedIdx < scroll) scroll = selectedIdx;
}
final showAbove = scroll > 0;
final showBelow = tickets.isNotEmpty && (scroll + maxVisible) < tickets.length;
// "More above" indicator
if (showAbove) {
final msg = _trunc('$scroll above', innerW);
cells.add(_Cell('${msg.padRight(innerW)}', fg: ConsoleColor.brightYellow));
} else {
cells.add(_Cell('${' ' * innerW}', fg: ConsoleColor.brightBlack));
}
// Visible tickets
final visEnd = min(scroll + maxVisible, tickets.length);
for (int ti = scroll; ti < visEnd; ti++) {
_addTicketCells(cells, tickets[ti], innerW, ti == selectedIdx && isSelected);
}
// Empty state
if (tickets.isEmpty) {
cells.add(_Cell('${'·' * (innerW ~/ 2)}${' ' * (innerW - innerW ~/ 2)}', fg: ConsoleColor.brightBlack));
cells.add(_Cell('│ (empty)${' ' * (innerW - 9)}', fg: ConsoleColor.brightBlack));
cells.add(_Cell('${'·' * (innerW ~/ 2)}${' ' * (innerW - innerW ~/ 2)}', fg: ConsoleColor.brightBlack));
}
// "More below" indicator
if (showBelow) {
final remaining = tickets.length - scroll - maxVisible;
final msg = _trunc('$remaining below', innerW);
cells.add(_Cell('${msg.padRight(innerW)}', fg: ConsoleColor.brightYellow));
} else {
cells.add(_Cell('${' ' * innerW}', fg: ConsoleColor.brightBlack));
}
// Fill remaining space before bottom border
while (cells.length < areaH - 1) {
cells.add(_Cell('${' ' * innerW}', fg: ConsoleColor.brightBlack));
}
// Bottom border (always at areaH - 1)
if (cells.length > areaH - 1) cells.length = areaH - 1;
cells.add(_Cell(
isSelected ? '${'' * innerW}' : '${'' * innerW}',
fg: isSelected ? color : ConsoleColor.brightBlack,
));
// Pad to exact height
while (cells.length < areaH) {
cells.add(_Cell(' ' * colW));
}
return cells;
}
static void _addTicketCells(List<_Cell> cells, Ticket ticket, int innerW, bool isSel) {
final bg = isSel ? ConsoleColor.blue : null;
final titleFg = isSel ? ConsoleColor.brightWhite : null;
final bullet = isSel ? '' : ' ';
final typeColor = _typeColor(ticket.type);
// Row 1: bullet + ID + type badge
final badge = ' [${_trunc(ticket.type, 7)}]';
final idLine = '$bullet ${ticket.id}$badge';
cells.add(_Cell(
'${_trunc(idLine, innerW).padRight(innerW)}',
fg: isSel ? ConsoleColor.brightWhite : typeColor,
bg: bg,
bold: isSel,
));
// Row 2: title
final titleLine = ' ${_trunc(ticket.title, innerW - 2)}';
cells.add(_Cell(
'${titleLine.padRight(innerW)}',
fg: titleFg,
bg: bg,
));
// Row 3: labels / milestone / blank
final String tagLine;
if (ticket.labels.isNotEmpty) {
tagLine = ' ${ticket.labels.take(3).map((l) => '#$l').join(' ')}';
} else if (ticket.milestones.isNotEmpty) {
tagLine = ' @ ${ticket.milestones.first}';
} else {
tagLine = '';
}
cells.add(_Cell(
'${_trunc(tagLine, innerW).padRight(innerW)}',
fg: isSel ? ConsoleColor.brightCyan : ConsoleColor.brightBlack,
bg: bg,
));
}
static void _writeHeaderBar(
Console console,
KanbanConfig config,
String searchQuery,
bool searchMode,
int w,
) {
console.setBackgroundColor(ConsoleColor.blue);
console.setForegroundColor(ConsoleColor.brightWhite);
console.setTextStyle(bold: true);
final left = ' DEW Kanban [${config.prefix}]';
final right = searchMode
? ' / ${searchQuery}_ '
: (searchQuery.isNotEmpty ? ' filter: "$searchQuery" ' : '');
final line = '$left${right.padLeft(max(0, w - left.length))}';
console.write(_trunc(line, w).padRight(w));
console.resetColorAttributes();
}
static void _writeFooterBar({
required Console console,
required String statusMsg,
required bool searchMode,
required String searchQuery,
required int w,
required int colIdx,
required int numCols,
required int numVisible,
required List<ColumnConfig> columns,
}) {
console.writeLine();
// Separator
console.setForegroundColor(ConsoleColor.brightBlack);
console.write('' * w);
console.resetColorAttributes();
console.writeLine();
if (statusMsg.isNotEmpty) {
console.setForegroundColor(ConsoleColor.brightYellow);
console.write(' $statusMsg');
console.resetColorAttributes();
} else if (searchMode) {
console.setForegroundColor(ConsoleColor.brightCyan);
console.write(' Search: ${searchQuery}_ (Enter to apply, Esc to clear)');
console.resetColorAttributes();
} else {
// Column position indicator + help
console.setForegroundColor(ConsoleColor.brightBlack);
final pos = numCols > numVisible ? ' [${colIdx + 1}/$numCols cols]' : '';
const help = ' [j/k] nav [h/l] col [</>] move [enter] detail [/] filter [r] reload [q] quit';
console.write(_trunc('$pos$help', w).padRight(w));
console.resetColorAttributes();
}
}
//
// Detail rendering
//
static void _renderDetail({
required Console console,
required Ticket ticket,
required KanbanConfig config,
required int scroll,
required int w,
required int h,
}) {
_writeHeaderBar(console, config, '', false, w);
console.writeLine();
// Ticket title bar
console.setBackgroundColor(ConsoleColor.blue);
console.setForegroundColor(ConsoleColor.brightWhite);
console.setTextStyle(bold: true);
final titleBar = ' ${ticket.id} ${_trunc(ticket.title, w - ticket.id.length - 6)}';
console.write(titleBar.padRight(w));
console.resetColorAttributes();
console.writeLine();
// Build scrollable content lines
final lines = _buildDetailLines(ticket, w - 4);
final contentH = h - 5; // header + blank + title + separator + footer
final maxScroll = max(0, lines.length - contentH);
final s = scroll.clamp(0, maxScroll);
final visible = lines.sublist(s, min(s + contentH, lines.length));
for (final line in visible) {
_applyCell(console, line);
console.resetColorAttributes();
console.writeLine();
}
for (int i = visible.length; i < contentH; i++) {
console.writeLine();
}
// Footer
console.setForegroundColor(ConsoleColor.brightBlack);
console.write('' * w);
console.resetColorAttributes();
console.writeLine();
console.setForegroundColor(ConsoleColor.brightBlack);
final scrollInfo = lines.isNotEmpty
? ' [${s + 1}-${min(s + contentH, lines.length)}/${lines.length}]'
: '';
console.write(' [j/k↑↓] scroll$scrollInfo [b/Esc] back [q] quit'.padRight(w));
console.resetColorAttributes();
}
static List<_Cell> _buildDetailLines(Ticket ticket, int w) {
final lines = <_Cell>[];
void hr([String? label]) {
if (label != null) {
final tag = ' $label ';
final dashes = max(0, w - tag.length + 2);
lines.add(_Cell('$tag${'' * dashes}', fg: ConsoleColor.brightBlue, bold: true));
} else {
lines.add(_Cell(' ${'' * w}', fg: ConsoleColor.brightBlack));
}
}
void kv(String key, String value) {
lines.add(
_Cell(
' ${key.padRight(12)}: ${_trunc(value, w - 16)}',
fg: ConsoleColor.white,
),
);
}
void blank() => lines.add(const _Cell(''));
// Metadata section
hr('INFO');
kv('Type', ticket.type);
kv('Column', ticket.column);
kv('Created', _fmtDate(ticket.created));
if (ticket.milestones.isNotEmpty) kv('Milestones', ticket.milestones.join(', '));
if (ticket.labels.isNotEmpty) kv('Labels', ticket.labels.map((l) => '#$l').join(' '));
if (ticket.links.isNotEmpty) {
for (final link in ticket.links) {
kv(link.type, link.targetId);
}
}
// Body
if (ticket.body.isNotEmpty) {
blank();
hr('BODY');
blank();
for (final para in ticket.body.split('\n')) {
for (final wl in _wordWrap(para, w)) {
lines.add(_Cell(' $wl', fg: ConsoleColor.white));
}
}
}
// Comments
for (int ci = 0; ci < ticket.comments.length; ci++) {
blank();
hr('COMMENT ${ci + 1}');
blank();
for (final para in ticket.comments[ci].split('\n')) {
for (final wl in _wordWrap(para, w)) {
lines.add(_Cell(' $wl', fg: ConsoleColor.white));
}
}
}
blank();
return lines;
}
//
// Utilities
//
static void _applyCell(Console console, _Cell cell) {
if (cell.bg != null) console.setBackgroundColor(cell.bg!);
if (cell.fg != null) console.setForegroundColor(cell.fg!);
if (cell.bold) console.setTextStyle(bold: true);
console.write(cell.text);
}
static Map<String, List<Ticket>> _groupByColumn(
List<Ticket> tickets,
KanbanConfig config,
) {
final map = <String, List<Ticket>>{};
for (final col in config.columns) {
map[col.id] = [];
}
for (final t in tickets) {
(map[t.column] ??= []).add(t);
}
return map;
}
static List<Ticket> _filtered(List<Ticket> tickets, String query) {
if (query.isEmpty) return tickets;
final q = query.toLowerCase();
return tickets
.where(
(t) =>
t.id.toLowerCase().contains(q) ||
t.title.toLowerCase().contains(q) ||
t.type.toLowerCase().contains(q) ||
t.labels.any((l) => l.toLowerCase().contains(q)) ||
t.body.toLowerCase().contains(q),
)
.toList();
}
static ConsoleColor _colColor(String color) => switch (color.toLowerCase()) {
'red' => ConsoleColor.red,
'green' => ConsoleColor.green,
'yellow' => ConsoleColor.yellow,
'blue' => ConsoleColor.blue,
'magenta' || 'purple' => ConsoleColor.magenta,
'cyan' || 'teal' => ConsoleColor.cyan,
'white' => ConsoleColor.white,
'brightred' || 'bright_red' => ConsoleColor.brightRed,
'brightgreen' || 'bright_green' => ConsoleColor.brightGreen,
'brightyellow' || 'bright_yellow' => ConsoleColor.brightYellow,
'brightblue' || 'bright_blue' => ConsoleColor.brightBlue,
'brightmagenta' || 'bright_magenta' => ConsoleColor.brightMagenta,
'brightcyan' || 'bright_cyan' => ConsoleColor.brightCyan,
'brightwhite' || 'bright_white' => ConsoleColor.brightWhite,
_ => ConsoleColor.cyan,
};
static ConsoleColor _typeColor(String type) => switch (type.toLowerCase()) {
'bug' => ConsoleColor.red,
'task' => ConsoleColor.blue,
'feature' || 'feat' => ConsoleColor.green,
'chore' || 'spike' => ConsoleColor.yellow,
'epic' => ConsoleColor.magenta,
'story' => ConsoleColor.cyan,
_ => ConsoleColor.white,
};
static String _trunc(String s, int maxLen) {
if (maxLen <= 0) return '';
if (s.length <= maxLen) return s;
if (maxLen == 1) return s[0];
return '${s.substring(0, maxLen - 1)}';
}
static String _fmtDate(DateTime dt) =>
'${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
static List<String> _wordWrap(String text, int width) {
if (text.isEmpty) return [''];
if (text.length <= width) return [text];
final words = text.split(' ');
final result = <String>[];
final buf = StringBuffer();
for (final word in words) {
if (buf.isNotEmpty && buf.length + 1 + word.length > width) {
result.add(buf.toString());
buf.clear();
}
if (buf.isNotEmpty) buf.write(' ');
buf.write(word);
}
if (buf.isNotEmpty) result.add(buf.toString());
return result;
}
}

View file

@ -14,6 +14,7 @@ import 'commands/list_command.dart';
import 'commands/move_command.dart'; import 'commands/move_command.dart';
import 'commands/search_command.dart'; import 'commands/search_command.dart';
import 'commands/stats_command.dart'; import 'commands/stats_command.dart';
import 'commands/tui_command.dart';
import 'commands/unarchive_command.dart'; import 'commands/unarchive_command.dart';
import 'commands/unlink_command.dart'; import 'commands/unlink_command.dart';
import 'commands/update_command.dart'; import 'commands/update_command.dart';
@ -34,6 +35,7 @@ class KanbanCommand extends DewCommand {
addSubcommand(AddCommentCommand(fs: fs)); addSubcommand(AddCommentCommand(fs: fs));
addSubcommand(GetConfigCommand(fs: fs)); addSubcommand(GetConfigCommand(fs: fs));
addSubcommand(StatsCommand(fs: fs)); addSubcommand(StatsCommand(fs: fs));
addSubcommand(TuiCommand(fs: fs));
addSubcommand(LinkCommand(fs: fs)); addSubcommand(LinkCommand(fs: fs));
addSubcommand(UnlinkCommand(fs: fs)); addSubcommand(UnlinkCommand(fs: fs));
} }

View file

@ -12,6 +12,7 @@ environment:
dependencies: dependencies:
dew_core: dew_core:
path: ../core path: ../core
dart_console: ^4.1.2
file: ^7.0.1 file: ^7.0.1
path: ^1.9.0 path: ^1.9.0
yaml: ^3.1.0 yaml: ^3.1.0

View file

@ -49,6 +49,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -89,6 +97,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.2" version: "0.4.2"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@ -129,6 +145,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
dart_console:
dependency: transitive
description:
name: dart_console
sha256: bf62b8016530fef83557c1f01867c281d0937dceb84204128819e6e925ddf73f
url: "https://pub.dev"
source: hosted
version: "4.1.4"
dart_mcp: dart_mcp:
dependency: transitive dependency: transitive
description: description:
@ -137,6 +161,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" version: "0.5.0"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file: file:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -193,6 +225,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
intl:
dependency: transitive
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.18.1"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -537,6 +577,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xml: xml:
dependency: transitive dependency: transitive
description: description: