commit 188819b26c02dbb34d0ed5d30785ac8623bdf88e Author: Chen Asraf Date: Tue Jan 12 02:56:41 2021 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d64647 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Files and directories created by pub +.dart_tool/ +.packages + +# Conventional directory for build outputs +build/ + +# Directory created by dartdoc +doc/api/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..87b1e56 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "dart_todo", + "request": "launch", + "program": "${workspaceFolder}/bin/todo.dart", + "args": [ + "${workspaceFolder}/example.todo" + ], + "type": "dart" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..687440b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version, created by Stagehand diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd03181 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. + +Created from templates made available by Stagehand under a BSD-style +[license](https://github.com/dart-lang/stagehand/blob/master/LICENSE). diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a686c1b --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,14 @@ +# Defines a default set of lint rules enforced for +# projects at Google. For details and rationale, +# see https://github.com/dart-lang/pedantic#enabled-lints. +include: package:pedantic/analysis_options.yaml + +# For lint rules and documentation, see http://dart-lang.github.io/linter/lints. +# Uncomment to specify additional rules. +# linter: +# rules: +# - camel_case_types + +analyzer: +# exclude: +# - path/to/excluded/files/** diff --git a/bin/todo.dart b/bin/todo.dart new file mode 100644 index 0000000..80c228a --- /dev/null +++ b/bin/todo.dart @@ -0,0 +1,10 @@ +import 'dart:io'; + +import 'package:dart_todo/gui.dart'; +import 'package:dart_todo/todo.dart'; + +void main(List arguments) async { + final project = Project(File(arguments.elementAt(0))); + await project.loadTodos(); + GUI(project: project); +} diff --git a/example.todo b/example.todo new file mode 100644 index 0000000..0ec6f8f --- /dev/null +++ b/example.todo @@ -0,0 +1,6 @@ +- [ ] Come up with plan +- [ ] Destroy the world +- [ ] Create solution +- [ ] Charge lots of $$$ for solution +- [ ] test +- [ ] another test diff --git a/lib/file_handler.dart b/lib/file_handler.dart new file mode 100644 index 0000000..e232b03 --- /dev/null +++ b/lib/file_handler.dart @@ -0,0 +1 @@ +part of 'todo.dart'; diff --git a/lib/gui.dart b/lib/gui.dart new file mode 100644 index 0000000..865600e --- /dev/null +++ b/lib/gui.dart @@ -0,0 +1,155 @@ +import 'dart:io'; + +import 'package:dart_console/dart_console.dart'; +import 'package:dart_todo/todo.dart'; + +class GUI { + final Project project; + final Console console; + + bool frozen = false; + int activeIndex = 0; + + GUI({this.project}) : console = Console() { + stdin.echoMode = false; + stdin.lineMode = false; + stdin.listen(_stdinListener); + _paint(); + } + + int get lines => stdout.terminalLines; + int get width => stdout.terminalColumns; + int get maxLines => project.todos.length; + String get verticalBorder => '-' * (width - 3); + + Todo get currentTodo => project.todos.elementAt(activeIndex); + + void _paint() { + if (frozen) { + return; + } + console.clearScreen(); + _print(verticalBorder); + _print(''); + + for (var i = 0; i < project.todos.length; i++) { + final todo = project.todos.elementAt(i); + final selected = i == activeIndex; + final indent = 4; + final line = [ + ' ' * (selected ? indent - 1 : indent), + selected ? '> ' : ' ', + (todo.done ? '√' : '×').padRight(3), + todo.title, + ].where((s) => s.isNotEmpty).join(' '); + _print(line); + } + final spareLines = lines - project.todos.length - 6; + if (spareLines > 0) { + for (var i = 0; i < spareLines; i++) { + _print(''); + } + } + _print((' ' * 10) + '[enter/space] - toggle | [esc/q] - quit'); + _print(verticalBorder); + } + + String _ensureLength(String message) => + '|' + message.padRight(width - '||\n'.length, ' ') + '|\n'; + void _print(String message) => console.write(_ensureLength(message)); + + void _stdinListener(List charCodes) { + if (!frozen) { + _handleGUIInput(keyMap[charCodes.last]); + } else { + final title = String.fromCharCodes(charCodes); + project.addTodo(Todo(title: title)); + _exitTextInputMode(); + } + _paint(); + } + + void _handleGUIInput(Key key) { + switch (key) { + case Key.down: + activeIndex++; + if (activeIndex >= maxLines) { + activeIndex = 0; + } + break; + case Key.up: + activeIndex--; + if (activeIndex < 0) { + activeIndex = maxLines - 1; + } + break; + case Key.enter: + case Key.space: + currentTodo.toggle(); + break; + case Key.c: + case Key.a: + _enterTextInputMode(); + console.write('Enter title: '); + break; + case Key.e: + //TODO: edit current todo + break; + case Key.d: + case Key.backspace: + project.removeTodo(currentTodo); + break; + case Key.q: + case Key.esc: + exit(0); + break; + case Key.left: + case Key.right: + break; + } + } + + void _enterTextInputMode() { + frozen = true; + stdin.echoMode = true; + stdin.lineMode = true; + } + + void _exitTextInputMode() { + frozen = false; + stdin.echoMode = false; + stdin.lineMode = false; + } + + final keyMap = { + 32: Key.space, + 65: Key.up, + 66: Key.down, + 67: Key.right, + 68: Key.left, + 10: Key.enter, + 27: Key.esc, + 113: Key.q, + 123: Key.backspace, + 97: Key.a, + 99: Key.c, + 100: Key.d, + 101: Key.e, + }; +} + +enum Key { + up, + right, + down, + left, + enter, + esc, + space, + backspace, + q, + a, + c, + d, + e, +} diff --git a/lib/todo.dart b/lib/todo.dart new file mode 100644 index 0000000..5bdb68d --- /dev/null +++ b/lib/todo.dart @@ -0,0 +1,128 @@ +import 'dart:io'; +part 'file_handler.dart'; + +class Todo { + String _title; + bool _done; + final void Function() onUpdate; + + Todo({ + String title, + bool done, + this.onUpdate, + }) : _title = title, + _done = done ?? false; + + Todo.parseLine(String line, {this.onUpdate}) { + final genericException = FormatException('Todo is malformed', line, 0); + final checkedException = FormatException( + 'Todo does not contain "checked" token (v/x)', + line, + line.indexOf(RegExp(r'\[')) + 1); + final pattern = RegExp(r'^- \[[ x]\]'); + try { + final match = pattern.matchAsPrefix(line); + + if (match == null) { + throw genericException; + } + + final split = [ + line.substring(0, match.end), + line.substring(match.end + 1), + ]; + + if (split.any((i) => i?.isNotEmpty != true)) { + throw genericException; + } + + switch (split[0].replaceAll(' ', '')) { + case '-[]': + _done = false; + break; + case '-[x]': + _done = true; + break; + default: + throw checkedException; + } + + _title = split[1]; + } on RangeError catch (_) { + throw genericException; + } + } + + String get title => _title; + set title(String value) => _update(_title = value); + + bool get done => _done; + set done(bool value) => _update(_done = value); + + void _update([dynamic _expr]) { + if (_expr is void Function()) { + _expr.call(); + } + onUpdate?.call(); + } + + void toggle() { + done = !done; + } + + Todo copyWith({ + String title, + bool done, + final void Function() onUpdate, + }) => + Todo( + title: title ?? this.title, + done: done ?? this.done ?? false, + onUpdate: onUpdate ?? this.onUpdate, + ); + + String toMarkdown() => '- [${done ? "x" : " "}] $title'; + + @override + String toString() => '$runtimeType($title, done: $done)'; +} + +class Project { + List todos = []; + File file; + + Project(this.file); + + void _dumpToFile() { + file.writeAsString(toMarkdown()); + } + + void create(String name) { + _checkAndCreateFile(File('$name.todo')); + } + + void loadTodos() async { + final content = await file.readAsLinesSync(); + for (final line in content) { + todos.add(Todo.parseLine(line, onUpdate: _dumpToFile)); + } + } + + Future _checkAndCreateFile(File file) async { + if (await file.exists() == false) { + await file.create(); + } + } + + String toMarkdown() => todos.map((t) => t.toMarkdown()).join('\r\n'); + + void addTodo(Todo todo) { + todos.add(todo.copyWith(onUpdate: _dumpToFile)); + _dumpToFile(); + } + + void removeTodo(Todo todo) { + todos.removeWhere((t) => t.title == todo.title); + _dumpToFile(); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..b592e80 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,383 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "14.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.41.1" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.3" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.13" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + dart_console: + dependency: "direct main" + description: + name: dart_console + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.2" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.1" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.4" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.1" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.4" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.2" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.4" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.9" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.4" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7" + node_interop: + dependency: transitive + description: + name: node_interop + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + node_io: + dependency: transitive + description: + name: node_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.12" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + path: + dependency: "direct main" + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + pedantic: + dependency: "direct dev" + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.9" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.9+1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.9" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.6" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.7" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.18+1" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.11+4" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+15" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.4" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.4" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" +sdks: + dart: ">=2.10.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..265079b --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,14 @@ +name: dart_todo +description: A fully fledged, simple to use to do terminal app +version: 1.0.0 +# homepage: https://www.example.com +environment: + sdk: ">=2.10.0 <3.0.0" + +dependencies: + dart_console: ^0.6.2 + path: ^1.7.0 + +dev_dependencies: + pedantic: ^1.9.0 + test: ^1.14.4 diff --git a/test/dart_todo_test.dart b/test/dart_todo_test.dart new file mode 100644 index 0000000..c8df8a8 --- /dev/null +++ b/test/dart_todo_test.dart @@ -0,0 +1,27 @@ +import 'package:dart_todo/todo.dart'; +import 'package:test/test.dart'; + +void main() { + test('create todo done', () { + final correctDone = '- [x] I did this'; + final todoDone = Todo.parseLine(correctDone); + + expect(todoDone.done, equals(true)); + expect(todoDone.title, equals('I did this')); + }); + test('create todo not done', () { + final correctNotDone = '- [ ] I did not do this'; + final todoNotDone = Todo.parseLine(correctNotDone); + + expect(todoNotDone.done, equals(false)); + expect(todoNotDone.title, equals('I did not do this')); + }); + test('create todo with bad format - throws', () { + final incorrect1 = "I don't know if I did this"; + final incorrect2 = '- [ ]'; + final incorrect3 = "[] I don't know if I did this"; + expect(() => Todo.parseLine(incorrect1), throwsFormatException); + expect(() => Todo.parseLine(incorrect2), throwsFormatException); + expect(() => Todo.parseLine(incorrect3), throwsFormatException); + }); +}