Initial Commit

This commit is contained in:
Sarah Jamie Lewis 2022-10-08 13:52:10 -07:00
parent 996ef3e358
commit da902700a9
31 changed files with 2302 additions and 0 deletions

44
.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
.metadata Normal file
View File

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c
base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c
- platform: linux
create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c
base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

29
analysis_options.yaml Normal file
View File

@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

BIN
fonts/iosevka-light.ttf Normal file

Binary file not shown.

70
lib/annotation.dart Normal file
View File

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:subwrite/sourcelink.dart';
import 'package:subwrite/theme.dart';
enum AnnotationType {
highlight,
error,
todo,
note,
}
class LineAnnotation {
SourceLink sourceLink;
AnnotationType annotationType;
String type;
SourceLink? reflink;
String? description;
// the tool that generated this annotation
String tool;
@override
bool operator ==(Object other) {
return other is LineAnnotation && other.annotationType == annotationType && other.description == description && other.sourceLink == sourceLink;
}
@override
int get hashCode {
var hash = Object.hash(sourceLink, annotationType, description);
return hash;
}
LineAnnotation(this.sourceLink, this.tool, this.annotationType, this.type, {this.reflink, this.description});
Icon getIcon(double size) {
if (type == "note") {
return Icon(
Icons.note,
color: literal,
size: size,
);
}
if (type == "todo") {
return Icon(
Icons.details,
color: variable,
size: size,
);
}
if (type == "incomplete") {
return Icon(
Icons.label,
color: constant,
size: size,
);
}
if (type == "edit") {
return Icon(
Icons.label,
color: literal,
size: size,
);
}
return Icon(
Icons.warning_amber_outlined,
color: Colors.red,
size: size,
);
}
}

28
lib/cursor.dart Normal file
View File

@ -0,0 +1,28 @@
class Cursor {
Cursor({this.line = 0, this.column = 0, this.anchorLine = 0, this.anchorColumn = 0});
int line = 0;
int column = 0;
int anchorLine = 0;
int anchorColumn = 0;
Cursor copy() {
return Cursor(line: line, column: column, anchorLine: anchorLine, anchorColumn: anchorColumn);
}
Cursor normalized() {
Cursor res = copy();
if (line > anchorLine || (line == anchorLine && column > anchorColumn)) {
res.line = anchorLine;
res.column = anchorColumn;
res.anchorLine = line;
res.anchorColumn = column;
return res;
}
return res;
}
bool hasSelection() {
return line != anchorLine || column != anchorColumn;
}
}

126
lib/cursor_provider.dart Normal file
View File

@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'cursor.dart';
import 'line_info.dart';
class CursorProvider extends ChangeNotifier {
Cursor cursor = Cursor(line: 1, column: 0);
bool inMouseSelection = false;
int get line => cursor.line;
int get column => cursor.column;
void publishCursor(List<LineInfo> elements) {
var ncursor = cursor.normalized();
elements.forEach((element) {
element.setCursor(ncursor);
});
}
void moveCursorToDocStart() {
clearSelection();
cursor.line = 0;
cursor.column = 0;
cursor = cursor.normalized();
notifyListeners();
}
void moveCursorToLineStart() {
cursor.column = 0;
notifyListeners();
}
void clearSelection() {
inMouseSelection = false;
cursor.anchorLine = cursor.line;
cursor.anchorColumn = cursor.column;
notifyListeners();
}
void moveCursorToLineEnd(int lineLength) {
cursor.column = lineLength;
notifyListeners();
}
void moveCursor(int dx, int dy, List<LineInfo> backingLines, {bool keepAnchor = false}) {
//print(cursor.line.toString() + " " + cursor.column.toString() + " " + dx.toString() + " " + dy.toString() + " " + backingLines[cursor.line - 1].text.length.toString());
if (dx != 0) {
if (cursor.column + dx < 0) {
moveCursor(0, -1, backingLines, keepAnchor: keepAnchor);
if (cursor.line > 1) {
moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
}
} else if (cursor.column + dx > backingLines[cursor.line - 1].text.length) {
moveCursor(0, 1, backingLines, keepAnchor: keepAnchor);
moveCursorToLineStart();
} else {
cursor.column += dx;
}
}
if (dy != 0) {
// Only allow the cursor line to change if in bounds
if (cursor.line + dy > 0 && cursor.line + dy <= backingLines.length) {
cursor.line += dy;
// Change cursor to line end
if (cursor.column > backingLines[cursor.line - 1].text.length) {
moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
}
}
}
if (!keepAnchor) {
clearSelection();
}
notifyListeners();
}
void mouseDownEnd() {
inMouseSelection = false;
}
void mouseDownMoveUpdate(int col, List<LineInfo> backingLines) {
if (cursor.column != col) {
var lineLength = backingLines[cursor.line - 1].text.length;
cursor.column = col.clamp(0, lineLength);
if (inMouseSelection == false) {
clearSelection();
}
publishCursor(backingLines);
notifyListeners();
} else if (inMouseSelection == false) {
clearSelection();
publishCursor(backingLines);
notifyListeners();
}
inMouseSelection = true;
}
void mouseDownUpdate(int line, int col, List<LineInfo> backingLines) {
if (cursor.line != line || cursor.column != col) {
cursor.line = line;
var lineLength = backingLines[cursor.line - 1].text.length;
cursor.column = col.clamp(0, lineLength);
if (inMouseSelection == false) {
clearSelection();
}
publishCursor(backingLines);
notifyListeners();
} else if (inMouseSelection == false) {
clearSelection();
publishCursor(backingLines);
notifyListeners();
}
inMouseSelection = true;
}
bool hasSelection() {
return cursor.hasSelection();
}
Cursor normalized() {
return cursor.normalized();
}
}

21
lib/editor.dart Normal file
View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:subwrite/view.dart';
import 'input.dart';
class Editor extends StatefulWidget {
const Editor({super.key});
@override
State<Editor> createState() => _Editor();
}
class _Editor extends State<Editor> {
@override
Widget build(BuildContext context) {
return InputListener(LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
//print("opening document " + path + " " + startLine.toString());
return View();
}));
}
}

472
lib/file.dart Normal file
View File

@ -0,0 +1,472 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:subwrite/annotation.dart';
import 'package:subwrite/section.dart';
import 'package:subwrite/sourcelink.dart';
import 'package:subwrite/suggestion.dart';
import 'cursor_provider.dart';
import 'line_info.dart';
class LineFile extends ChangeNotifier {
List<LineInfo> backingLines = List.empty(growable: true);
CursorProvider cursor = CursorProvider();
FocusNode focus = FocusNode();
bool showSuggestions = false;
late String projectPath;
late String path;
ScrollController scrollController = ScrollController();
void openFile(String path, int startLine) {
this.path = path;
File f = File(path);
var line = 0;
f.readAsLinesSync().forEach((text) {
backingLines.insert(line, LineInfo(path, line + 1, text.replaceAll("\t", " ").trimRight()));
line += 1;
});
// handle the empty file case
if (backingLines.isEmpty) {
backingLines.insert(0, LineInfo(path, 1, ""));
}
parse();
cursor.moveCursorToDocStart();
focus.requestFocus();
scrollController = ScrollController(initialScrollOffset: (startLine * 17.0 - (17.0 * 10.0)).clamp(0.0, double.infinity));
notifyListeners();
}
void cancelSuggestions() {
if (showSuggestions) {
suggestionIndex = 0;
showSuggestions = false;
notifyListeners();
}
}
ScrollController suggestionListController = ScrollController(initialScrollOffset: 0.0);
int suggestionIndex = 0;
List<Suggestion> suggestions = List.empty();
// Future<List<Suggestion>> loadSuggestions(IDE ide) async {
// return ide.getSuggestions(projectPath, path, cursor.line, cursor.column, base64()).then((list) {
// suggestions = list;
// return list;
// });
// }
void suggestListUp() {
suggestionIndex = (suggestionIndex - 1) % suggestions.length;
if (suggestions.length - suggestionIndex >= 14 || suggestionIndex == 0 || suggestionIndex == suggestions.length - 1) {
suggestionListController.animateTo(suggestionIndex * 13.0, duration: const Duration(milliseconds: 100), curve: Curves.decelerate);
}
notifyListeners();
}
void suggestListDown() {
suggestionIndex = (suggestionIndex + 1) % suggestions.length;
if (suggestions.length - suggestionIndex >= 14 || suggestionIndex == 0 || suggestionIndex == suggestions.length - 1) {
suggestionListController.animateTo(suggestionIndex * 13.0, duration: const Duration(milliseconds: 100), curve: Curves.decelerate);
}
notifyListeners();
}
void suggestListInsert() {
suggestionIndex = (suggestionIndex) % suggestions.length;
for (var char = suggestions[suggestionIndex].partialLen; char < suggestions[suggestionIndex].name.length; char++) {
insertChar(suggestions[suggestionIndex].name[char]);
}
cursor.publishCursor(backingLines);
notifyListeners();
}
KeyEventResult handleKey(RawKeyEvent event) {
// only process key down events...
if (event.runtimeType.toString() == 'RawKeyDownEvent') {
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
cursor.moveCursor(-1, 0, backingLines, keepAnchor: event.isShiftPressed);
cursor.publishCursor(backingLines);
cancelSuggestions();
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
cursor.moveCursor(1, 0, backingLines, keepAnchor: event.isShiftPressed);
cursor.publishCursor(backingLines);
cancelSuggestions();
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
if (showSuggestions == false) {
cursor.moveCursor(0, -1, backingLines, keepAnchor: event.isShiftPressed);
cursor.publishCursor(backingLines);
} else {
suggestListUp();
}
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
if (showSuggestions == false) {
cursor.moveCursor(0, 1, backingLines, keepAnchor: event.isShiftPressed);
cursor.publishCursor(backingLines);
} else {
suggestListDown();
}
} else if (event.logicalKey == LogicalKeyboardKey.home) {
cancelSuggestions();
cursor.clearSelection();
cursor.moveCursorToLineStart();
cursor.publishCursor(backingLines);
} else if (event.logicalKey == LogicalKeyboardKey.end) {
cancelSuggestions();
cursor.clearSelection();
cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
cursor.publishCursor(backingLines);
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
if (showSuggestions == false) {
insertLine();
cancelSuggestions();
cursor.clearSelection();
cursor.publishCursor(backingLines);
} else {
suggestListInsert();
}
} else if (event.logicalKey == LogicalKeyboardKey.tab) {
insertChar(" ");
cancelSuggestions();
cursor.publishCursor(backingLines);
} else if (event.logicalKey == LogicalKeyboardKey.delete) {
handleDelete();
cancelSuggestions();
cursor.clearSelection();
cursor.publishCursor(backingLines);
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
handleBackspace();
cancelSuggestions();
cursor.clearSelection();
cursor.publishCursor(backingLines);
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
showSuggestions = false;
notifyListeners();
} else {
if (event.isControlPressed) {
if (event.logicalKey == LogicalKeyboardKey.space) {
showSuggestions = true;
notifyListeners();
}
if (event.character != null && event.character == 's') {
saveFile();
notifyListeners();
}
if (event.character != null && event.character == 'c') {
copy();
}
if (event.character != null && event.character == 'v') {
paste();
notifyListeners();
}
} else {
if (event.logicalKey == LogicalKeyboardKey.space) {
insertChar(" ");
} else if (event.character != null) {
insertChar(event.character!);
}
}
cursor.publishCursor(backingLines);
}
}
parse();
return KeyEventResult.handled;
}
void saveFile() {
String content = '';
for (var l in backingLines) {
content += l.text + '\n';
}
File(path).writeAsStringSync(content);
}
void handleDelete() {
if (cursor.hasSelection()) {
deleteSelected();
return;
}
// if at the end of the line then we need to move the next line onto
// our line, and delete what remains
if (cursor.column == backingLines[cursor.line - 1].text.length) {
backingLines[cursor.line - 1].text += backingLines[cursor.line].text;
backingLines[cursor.line - 1].notifyListeners();
backingLines.removeAt(cursor.line);
renumberFrom(cursor.line);
} else {
// otherwise simply delete a char
var beforeDeletedChar = backingLines[cursor.line - 1].text.substring(0, cursor.column);
var afterDeletedChar = backingLines[cursor.line - 1].text.substring(cursor.column + 1);
backingLines[cursor.line - 1].text = beforeDeletedChar + afterDeletedChar;
backingLines[cursor.line - 1].notifyListeners();
}
}
void deleteSelected() {
var ncursor = cursor.normalized();
if (ncursor.hasSelection()) {
if (ncursor.anchorLine == ncursor.line) {
// simple case, just trim column
var beforeDeletedChar = backingLines[ncursor.line - 1].text.substring(0, ncursor.column);
var afterDeletedChar = backingLines[ncursor.line - 1].text.substring(ncursor.anchorColumn);
backingLines[ncursor.line - 1].text = beforeDeletedChar + afterDeletedChar;
backingLines[ncursor.line - 1].notifyListeners();
cursor.clearSelection();
} else {
// First Line
var withDeletedRemainder = backingLines[ncursor.line - 1].text.substring(0, ncursor.column);
var withDeletedPrefix = backingLines[ncursor.anchorLine - 1].text.substring(ncursor.anchorColumn);
backingLines[ncursor.line - 1].text = withDeletedRemainder + withDeletedPrefix;
// Delete 2nd to n-1 Lines
for (var line = (ncursor.anchorLine - 1); line >= ncursor.line; line -= 1) {
print("deleting $line ${backingLines[line].text}");
backingLines.removeAt(line);
}
renumberFrom(ncursor.line);
cursor.clearSelection();
}
}
cursor.clearSelection();
}
void handleBackspace() {
if (cursor.hasSelection()) {
deleteSelected();
return;
}
if (cursor.column > 0) {
var beforeDeletedChar = backingLines[cursor.line - 1].text.substring(0, cursor.column - 1);
var afterDeletedChar = backingLines[cursor.line - 1].text.substring(cursor.column);
backingLines[cursor.line - 1].text = beforeDeletedChar + afterDeletedChar;
backingLines[cursor.line - 1].notifyListeners();
cursor.moveCursor(-1, 0, backingLines);
} else if (cursor.line > 1) {
// join current line to previous line
cursor.moveCursor(0, -1, backingLines);
cursor.moveCursorToLineEnd(backingLines[cursor.line - 1].text.length);
backingLines[cursor.line - 1].text += backingLines[cursor.line].text;
backingLines[cursor.line - 1].notifyListeners();
// delete current line
backingLines.removeAt(cursor.line);
renumberFrom(cursor.line);
// renumber
}
}
void insertChar(String ch) {
var beforeInsertedChar = backingLines[cursor.line - 1].text.substring(0, cursor.column);
var afterInsertedChar = backingLines[cursor.line - 1].text.substring(cursor.column);
backingLines[cursor.line - 1].text = beforeInsertedChar + ch + afterInsertedChar;
cursor.moveCursor(ch.length, 0, backingLines);
backingLines[cursor.line - 1].notify();
}
void insertLine() {
// get the content that will be moved to the next line
var restOfLine = backingLines[cursor.line - 1].text.substring(cursor.column);
// split off the line after the cursor
backingLines[cursor.line - 1].text = backingLines[cursor.line - 1].text.substring(0, cursor.column);
// insert a line directly into this position
backingLines.insert(cursor.line, LineInfo(path, cursor.line + 1, restOfLine));
// Move the Cursor Down
cursor.moveCursor(0, 1, backingLines);
// renumber all the lines afterwards
renumberFrom(cursor.line - 1);
cursor.moveCursorToLineStart();
}
void renumberFrom(int startLine) {
for (int line = startLine; line < backingLines.length; line++) {
backingLines[line].lineNumber = line + 1;
backingLines[line].notifyListeners();
}
notifyListeners();
}
int lines() {
return backingLines.length;
}
final _boldRegex = RegExp(
r'(\*\*([^*]*)\*\*)',
caseSensitive: false,
dotAll: true,
);
final _codeRegex = RegExp(
r'(\`([^`]*)\`)',
caseSensitive: false,
dotAll: true,
);
List<Section> sections = List.empty(growable: true);
// rebuild the annotations manager
void parse() {
sections.clear();
for (var i = 0; i < backingLines.length; i++) {
var line = backingLines[i];
backingLines[i].annotations.removeWhere((element) => element.tool == "markdown");
if (line.text.startsWith("//")) {
backingLines[i].annotations.add(
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.highlight, "comment"));
if (line.text.contains("TODO")) {
backingLines[i].annotations.add(
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.todo, "todo"));
}
}
if (line.text.startsWith("#")) {
// this is a header
backingLines[i].annotations.add(
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.highlight, "header"));
var level = 0;
for (var lvl = 0; lvl < 6; lvl++) {
if (line.text.length > lvl) {
if (line.text[lvl] == '#') {
level += 1;
} else {
break;
}
}
}
sections.add(Section(backingLines[i].lineNumber, level, line.text.substring(level + 1)));
}
if (line.text.startsWith(" ")) {
backingLines[i].annotations.add(
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: 0, colEnd: line.text.length + 1, lineEnd: i), "markdown", AnnotationType.highlight, "code"));
}
_boldRegex.allMatches(backingLines[i].text).forEach((match) {
backingLines[i].annotations.add(
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: match.start, colEnd: match.end + 1, lineEnd: i), "markdown", AnnotationType.highlight, "bold"));
});
_codeRegex.allMatches(backingLines[i].text).forEach((match) {
backingLines[i].annotations.add(
LineAnnotation(SourceLink(path, lineStart: backingLines[i].lineNumber, colStart: match.start, colEnd: match.end + 1, lineEnd: i), "markdown", AnnotationType.highlight, "code"));
});
}
notifyListeners();
}
String base64() {
String content = '';
backingLines.forEach((l) {
content += l.text + '\n';
});
return base64Encode(utf8.encode(content));
}
void copy() {
var ncursor = cursor.normalized();
var firstLine = backingLines[ncursor.line - 1].text.substring(ncursor.column) + "\n";
for (var line = ncursor.line + 1; line < ncursor.anchorLine; line++) {
firstLine += backingLines[line - 1].text + "\n";
}
var textToCopy = firstLine;
if (ncursor.anchorLine != ncursor.line) {
var lastLine = backingLines[ncursor.anchorLine - 1].text.substring(0, ncursor.anchorColumn);
textToCopy += lastLine;
}
Clipboard.setData(ClipboardData(text: textToCopy));
}
void paste() {
deleteSelected(); // if we have a selection we need to delete it before pasting new text
Clipboard.getData("text/plain").then((value) {
if (value != null && value.text != null) {
for (var i = 0; i < value.text!.length; i++) {
var char = value.text![i];
if (char == "\n") {
insertLine();
} else {
insertChar(char);
}
}
}
});
}
void mouseDownUpdate(int line, int col) {
cancelSuggestions();
cursor.mouseDownUpdate(line, col, backingLines);
}
void mouseDownMoveUpdate(int col) {
cancelSuggestions();
cursor.mouseDownMoveUpdate(col, backingLines);
}
void mouseDownEnd() {
cursor.mouseDownEnd();
}
void moveLines(int startLine, int endLine, int newLine) {
print("want to move from $startLine (: $endLine) to $newLine\n");
List<LineInfo> insert = List.empty(growable: true);
// backup the section
for (int line = startLine; line < endLine; line++) {
insert.add(backingLines[line - 1]);
}
// remove the section
for (int line = startLine; line < endLine; line++) {
backingLines.removeAt(startLine - 1);
}
// if we want to put a line that was on e.g. 5 on 10 then we have to remove 5...
// (unlike if it was on 10 and we want to put it on 5...it hasn't changed)
if (newLine > startLine) {
newLine -= endLine - startLine;
}
// reinsert
for (int line = 0; line < insert.length; line++) {
backingLines.insert(newLine - 1 + line, insert[line]);
}
renumberFrom(0);
parse();
notifyListeners();
}
addNote(String note) {
var notetype = "note";
if (note.startsWith("[incomplete]")) {
notetype = "incomplete";
} else if (note.startsWith("[edit]")) {
notetype = "edit";
}
backingLines[cursor.line-1].annotations.add(
LineAnnotation(
SourceLink(
path,
lineStart: cursor.line,
),
"notes",
AnnotationType.note,
notetype,
description: note));
notifyListeners();
}
}

111
lib/highlighter.dart Normal file
View File

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:subwrite/sourcelink.dart';
import 'package:subwrite/theme.dart';
import 'annotation.dart';
double fontSize = 14;
double gutterFontSize = 12;
class LineDecoration {
int start = 0;
int end = 0;
Color color = Colors.white;
Color background = Colors.white;
bool underline = false;
bool italic = false;
bool bold = false;
SourceLink? message;
}
class Highlighter {
Highlighter() {}
List<InlineSpan> run(BuildContext context, String text, int line, bool highlightRisk, TextStyle style, List<LineAnnotation>? insights, int? cursorStart, int? cursorEnd, bool lineSelected) {
TextStyle defaultStyle = style.copyWith(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, height: 1.2);
List<InlineSpan> res = <InlineSpan>[];
List<LineDecoration> decors = <LineDecoration>[];
insights
?.where(
(element) => element.annotationType == AnnotationType.highlight,
)
.forEach((element) {
LineDecoration d = LineDecoration();
//print("${element.sourceLink.colStart} ${element.sourceLink.colEnd}");
d.start = element.sourceLink.colStart! - 1;
d.end = element.sourceLink.colEnd! - 2;
d.background = Colors.transparent;
switch (element.type) {
case "header":
d.color = header;
break;
case "bold":
d.color = keyword;
d.bold = true;
break;
case "code":
d.color = function;
break;
}
decors.add(d);
});
// Assume that decorators do not overlap...
decors.sort((a, b) {
return a.start.compareTo(b.start);
});
text += ' ';
for (int i = 0; i < text.length; i++) {
String current = text[i];
TextStyle style = defaultStyle.copyWith();
// decorate
for (var d in decors) {
// if we are in this decoration...
if (i >= d.start && i <= d.end) {
style = style.copyWith(color: d.color);
if (d.bold = true) {
style = style.copyWith(fontWeight: FontWeight.bold);
}
}
}
// is within selection
if (lineSelected) {
style = style.copyWith(backgroundColor: selection);
} else if (cursorStart != null && cursorEnd != null) {
if (i >= cursorStart && i < cursorEnd) {
style = style.copyWith(backgroundColor: selection);
}
} else if (cursorStart != null && cursorEnd == null) {
if (i >= cursorStart) {
style = style.copyWith(backgroundColor: selection);
}
} else if (cursorStart == null && cursorEnd != null) {
if (i < cursorEnd) {
style = style.copyWith(backgroundColor: selection);
}
}
if (highlightRisk) {
style = style.copyWith(
backgroundColor: risk.withOpacity(0.50),
);
}
if (current == " ") {
res.add(TextSpan(text: "_", style: style.copyWith(color: style.backgroundColor), semanticsLabel: " ", mouseCursor: SystemMouseCursors.text));
} else {
// we need to add a '\ufeff' (Zero Width No-Break Space) here to avoid RichText doing ridiculous
// breaking logic with hyphens / slashes etc.
res.add(TextSpan(text: '$current\ufeff', style: style, mouseCursor: SystemMouseCursors.text));
}
}
return res;
}
}

109
lib/input.dart Normal file
View File

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:subwrite/theme.dart';
import 'file.dart';
class InputListener extends StatefulWidget {
late Widget child;
InputListener(this.child);
@override
_InputListener createState() => _InputListener();
}
class _InputListener extends State<InputListener> {
late FocusNode focusNode;
@override
void initState() {
super.initState();
focusNode = FocusNode();
}
@override
void dispose() {
super.dispose();
focusNode.dispose();
}
void newTextDialog(context, String title, String hint, Function(String) callback) {
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
TextEditingController controller = TextEditingController();
return AlertDialog(
shape: ContinuousRectangleBorder(side: BorderSide(color: sidebarAlt)),
backgroundColor: sidebar,
title: Align(alignment: Alignment.topCenter, child: Text(title)),
titleTextStyle: TextStyle(fontSize: 10.0),
titlePadding: EdgeInsets.all(5.0),
content: TextField(
autofocus: true,
controller: controller,
onSubmitted: (newValue) {
callback(newValue);
Navigator.of(context).pop();
},
style: TextStyle(color: Colors.white, fontSize: 10.0),
cursorColor: Colors.white,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(color: Colors.white38),
isDense: true,
fillColor: sidebarHighlight,
focusColor: sidebarHighlight,
labelStyle: TextStyle(color: Colors.white),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(width: 1, color: sidebarAlt),
borderRadius: BorderRadius.zero,
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(width: 1, color: sidebarHighlight),
borderRadius: BorderRadius.zero,
)),
),
contentPadding: EdgeInsets.symmetric(vertical: 1.0, horizontal: 5.0),
actions: [
ElevatedButton.icon(icon: Icon(Icons.label, color: literal,), onPressed: () {
controller.text = "[edit]${controller.text}";
callback(controller.text);
Navigator.of(context).pop();
}, label: Text("Edit"),style: ButtonStyle(backgroundColor: MaterialStateProperty.all(tabs), foregroundColor: MaterialStateProperty.all(foreground))),
ElevatedButton.icon(icon: Icon(Icons.label, color: constant,), onPressed: () {
controller.text = "[incomplete]${controller.text}";
callback(controller.text);
Navigator.of(context).pop();
}, label: Text("Incomplete"),style: ButtonStyle(backgroundColor: MaterialStateProperty.all(tabs), foregroundColor: MaterialStateProperty.all(foreground))),
],
);
},
).then((value) => null);
}
@override
Widget build(BuildContext context) {
if (!focusNode.hasFocus) {
focusNode.requestFocus();
}
LineFile doc = Provider.of<LineFile>(context);
return GestureDetector(
child: Focus(
child: widget.child,
focusNode: focusNode,
autofocus: true,
onKey: (FocusNode node, RawKeyEvent event) {
doc.handleKey(event);
if (event.logicalKey == LogicalKeyboardKey.escape) {
newTextDialog(context, "New Note", "Note", (note) => {doc.addNote(note)});
}
return KeyEventResult.handled;
}));
}
}

251
lib/line_info.dart Normal file
View File

@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:subwrite/theme.dart';
import 'annotation.dart';
import 'cursor.dart';
import 'file.dart';
import 'highlighter.dart';
double gutterWidth = 64;
double rightMargin = 32;
class LineInfo extends ChangeNotifier {
String path;
String text;
int lineNumber;
int? cursorStart;
int? cursorEnd;
bool lineSelected = false;
List<LineAnnotation> annotations = List.empty(growable: true);
LineInfo(this.path, this.lineNumber, this.text);
Widget build(BuildContext context) {
bool highlightRisk = false;
var backgroundStyle = TextStyle(backgroundColor: cursorStart != null ? sidebarHighlight : background);
Highlighter hl = Highlighter();
List<InlineSpan> spans = hl.run(context, text, lineNumber, highlightRisk, backgroundStyle, annotations, cursorStart, cursorEnd, lineSelected);
final gutterStyle = TextStyle(fontFamily: 'Iosevka', decoration: null, decorationColor: null, fontSize: gutterFontSize, color: comment, backgroundColor: sidebarAlt, height: 1.0);
return LayoutBuilder(
builder: (context, constraints) {
var highlightedLine = RichText(
text: TextSpan(
children: spans,
style: backgroundStyle,
),
);
List<Widget> stackElements = List.empty(growable: true);
stackElements.add(highlightedLine);
var charWidth = (fontSize / 2.0);
var lineHeight = (fontSize * 1.2);
var usableSize = (constraints.maxWidth - gutterWidth - rightMargin);
var sidebarContent = List<Widget>.empty(growable: true);
sidebarContent.add(Text('$lineNumber'.padLeft(5), style: gutterStyle));
var icons = annotations.where((element) => element.annotationType == AnnotationType.note).map((e) {
highlightRisk = true;
return Padding(padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: Tooltip(message: e.description!, child: e.getIcon(15.0)));
}).toList();
if (icons != null) {
sidebarContent.addAll(icons);
}
icons = annotations.where((element) => element.annotationType == AnnotationType.todo).map((e) {
highlightRisk = true;
return Padding(padding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 0.0), child: e.getIcon(15.0));
}).toList();
if (icons != null) {
sidebarContent.addAll(icons);
}
if (cursorStart != null) {
var maxCharsBeforeWrapping = (usableSize / charWidth).floorToDouble();
var cursorPosTop = (cursorStart! / maxCharsBeforeWrapping).floorToDouble() * lineHeight;
var cursorPosLeft = ((cursorStart! % maxCharsBeforeWrapping) * charWidth) - charWidth / 2.0;
TextStyle cusrsorStyle = TextStyle(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, backgroundColor: Colors.transparent, letterSpacing: -2.0);
var cursorPos = Positioned(top: cursorPosTop.toDouble(), left: cursorPosLeft.toDouble(), child: Text("|", style: cusrsorStyle));
stackElements.add(cursorPos);
}
var idealHeight = (lineHeight * (1.0 + ((text.length * charWidth) / usableSize).floor())).ceilToDouble();
var cursorOverlay = Stack(children: stackElements);
return Listener(
onPointerDown: (event) {
//var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0);
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
var file = Provider.of<LineFile>(context, listen: false);
if (event.down && !inMargin) {
file.mouseDownUpdate(lineNumber, c.round());
}
},
onPointerMove: (event) {
//var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0)
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
var file = Provider.of<LineFile>(context, listen: false);
if (event.down && !inMargin) {
file.mouseDownMoveUpdate(c.round());
} else {
file.mouseDownEnd();
}
},
child: MouseRegion(
onEnter: (event) {
//var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0);
var inMargin = (event.localPosition.dx - gutterWidth) > usableSize;
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
var file = Provider.of<LineFile>(context, listen: false);
if (event.down && !inMargin) {
file.mouseDownUpdate(lineNumber, c.round());
} else {
file.mouseDownEnd();
}
},
onHover: (event) {
//var c = (event.localPosition.dx - gutterWidth) / (fontSize / 2.0);
var c = cursorFromMouse(usableSize, event.localPosition.dx - gutterWidth, event.localPosition.dy);
var file = Provider.of<LineFile>(context, listen: false);
if (event.down) {
// note should never happen because flutter only fires hover events when the mouse it up...
file.mouseDownUpdate(lineNumber, c.round());
} else {
file.mouseDownEnd();
}
},
onExit: (event) {
notifyListeners();
},
child: Container(
color: backgroundStyle.backgroundColor,
padding: EdgeInsets.zero,
margin: EdgeInsets.zero,
height: idealHeight,
width: constraints.maxWidth,
child: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(
padding: EdgeInsets.zero,
decoration: BoxDecoration(
color: sidebarAlt,
),
width: gutterWidth - 1,
height: idealHeight,
margin: EdgeInsets.only(right: 1, top: 0, bottom: 0, left: 0),
alignment: Alignment.centerLeft,
child: Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.start, children: sidebarContent)),
Container(
padding: EdgeInsets.zero,
margin: EdgeInsets.only(right: rightMargin, top: 0, bottom: 0, left: 0),
decoration: BoxDecoration(
color: backgroundStyle.backgroundColor,
),
width: constraints.maxWidth - rightMargin - gutterWidth,
height: idealHeight,
child: cursorOverlay,
),
]))));
},
);
}
void setCursor(Cursor cursor) {
if (cursor.hasSelection()) {
// if we are part of a selection then act like it
if (cursor.line < lineNumber && cursor.anchorLine > lineNumber) {
if (lineSelected == false) {
lineSelected = true;
cursorStart = null;
cursorEnd = null;
notifyListeners();
}
} else if (cursor.line == lineNumber && cursor.anchorLine > lineNumber) {
// else if the selection starts on this line...
// obliterate selection
if (lineSelected == true) {
lineSelected = false;
}
cursorStart = cursor.column;
cursorEnd = null;
notifyListeners();
} else if (cursor.line < lineNumber && cursor.anchorLine == lineNumber) {
// else if the selection ends on this line...
// obliterate selection
if (lineSelected == true) {
lineSelected = false;
}
cursorStart = null;
cursorEnd = cursor.anchorColumn;
notifyListeners();
} else if (cursor.line == cursor.line && cursor.anchorLine == lineNumber) {
if (lineSelected == true) {
lineSelected = false;
}
cursorStart = cursor.column;
cursorEnd = cursor.anchorColumn;
notifyListeners();
} else {
// obliterate selection
if (lineSelected == true) {
lineSelected = false;
notifyListeners();
}
if (cursorStart != null) {
cursorStart = null;
notifyListeners();
}
if (cursorEnd != null) {
cursorEnd = null;
notifyListeners();
}
}
} else {
// obliterate selection
if (lineSelected == true) {
lineSelected = false;
cursorStart = null;
cursorEnd = null;
notifyListeners();
}
if (cursor.line == lineNumber) {
cursorStart = cursor.column;
cursorEnd = cursor.anchorColumn;
notifyListeners();
} else if (cursorStart != null || cursorEnd != null) {
cursorStart = null;
cursorEnd = null;
notifyListeners();
}
}
}
void updateText(String line) {
text = line;
notifyListeners();
}
void notify() {
notifyListeners();
}
}
int cursorFromMouse(double usableSize, double mx, double my) {
var charWidth = (fontSize / 2.0);
var lineHeight = (fontSize * 1.2);
var maxCharsBeforeWrapping = (usableSize / charWidth).floorToDouble();
var prevLines = (my / lineHeight).floorToDouble() * maxCharsBeforeWrapping;
var charsInThisLine = ((mx) / charWidth).floorToDouble();
return (prevLines + charsInThisLine).floor();
}

63
lib/main.dart Normal file
View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:subwrite/file.dart';
import 'package:subwrite/theme.dart';
import 'editor.dart';
import 'outline.dart';
void main(List<String> args) {
var docfile = LineFile();
docfile.openFile("./example.md", 0);
var doc = ChangeNotifierProvider.value(value: docfile);
runApp(MultiProvider(
providers: [doc],
builder: (context, child) {
return MyApp();
}));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'subwrite',
theme: ThemeData(scrollbarTheme: ScrollbarThemeData(thumbColor: MaterialStateProperty.all(header))),
home: const SarahDownApp(),
);
}
}
class SarahDownApp extends StatefulWidget {
const SarahDownApp({super.key});
@override
State<SarahDownApp> createState() => _SarahDownApp();
}
class _SarahDownApp extends State<SarahDownApp> {
@override
Widget build(BuildContext context) {
return Material(
color: sidebarAlt,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
flex: 2,
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: [Expanded(child: Outline()), Container(color: comment)])),
Flexible(flex: 5, child: Editor())
],
));
}
}

74
lib/outline.dart Normal file
View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:subwrite/file.dart';
import 'package:subwrite/theme.dart';
class Outline extends StatefulWidget {
const Outline({super.key});
@override
State<Outline> createState() => _Outline();
}
class _Outline extends State<Outline> {
@override
Widget build(BuildContext context) {
var sections = Provider.of<LineFile>(context).sections;
return Material(
color: tabs,
child: ReorderableListView.builder(
header: Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
child: Column(children: [Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(
Icons.edit,
color: foreground,
),
Text("subwrite", style: TextStyle(fontSize: 16.0, color: foreground, fontWeight: FontWeight.bold, fontFamily: "Iosevka"))
]), Divider(color: foreground,)])),
footer: Padding(padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 10.0), child: Divider(color: foreground, thickness: 4.0)),
onReorder: (int oldIndex, int newIndex) {
var doc = Provider.of<LineFile>(context, listen: false);
var startLine = sections[oldIndex].lineNumber;
var endLine = doc.lines();
for (var index = oldIndex + 1; index < sections.length; index++) {
print("${sections[oldIndex].level} > ${sections[index].level}");
if (sections[oldIndex].level < sections[index].level) {
// sub section...skipping
} else {
endLine = sections[index].lineNumber;
break;
}
}
var newLine = newIndex >= sections.length ? doc.lines() : sections[newIndex].lineNumber;
doc.moveLines(startLine, endLine, newLine);
},
buildDefaultDragHandles: false,
itemBuilder: (BuildContext context, int index) {
return ListTile(
key: ValueKey(index),
tileColor: tabs,
title: Text(
sections[index].title.padLeft(sections[index].title.length + sections[index].level - 1, ' '),
style: TextStyle(color: foreground, fontSize: 14.0 - sections[index].level, fontFamily: "Iosevka"),
),
onTap: () {
Provider.of<LineFile>(context, listen: false).scrollController.jumpTo(sections[index].lineNumber * 17.0);
},
trailing: ReorderableDragStartListener(
index: index,
child: Icon(
Icons.drag_handle,
color: foreground,
),
),
);
},
itemCount: sections.length,
));
}
}

7
lib/section.dart Normal file
View File

@ -0,0 +1,7 @@
class Section {
int lineNumber;
int level;
String title;
Section(this.lineNumber, this.level, this.title);
}

51
lib/sourcelink.dart Normal file
View File

@ -0,0 +1,51 @@
class SourceLink {
late final String file;
final int? lineStart;
final int? colStart;
final int? lineEnd;
final int? colEnd;
SourceLink(this.file, {this.lineStart, this.colStart, this.lineEnd, this.colEnd});
String lineLink() {
return file + ":" + lineStart.toString();
}
@override
bool operator ==(Object other) {
return other is SourceLink && other.hashCode == hashCode;
}
@override
int get hashCode {
var hash = Object.hash(file, lineStart, colStart, lineEnd, colEnd);
return hash;
}
}
SourceLink? parseSourceLink(String line) {
String? file;
int? lineStart;
int? colStart;
int? lineEnd;
int? colEnd;
var parts = line.split(":");
if (parts.isNotEmpty && parts[0].isNotEmpty) {
file = parts[0];
} else {
return null;
}
if (parts.length > 1 && parts[1].trim().isNotEmpty) {
lineStart = int.parse(parts[1]);
}
if (parts.length > 2 && parts[2].trim().isNotEmpty) {
colStart = int.parse(parts[2]);
}
if (parts.length > 3 && parts[3].trim().isNotEmpty) {
lineEnd = int.parse(parts[3]);
}
if (parts.length > 4 && parts[4].trim().isNotEmpty) {
colEnd = int.parse(parts[4]);
}
return SourceLink(file, lineStart: lineStart, lineEnd: lineEnd, colStart: colStart, colEnd: colEnd);
}

42
lib/suggestion.dart Normal file
View File

@ -0,0 +1,42 @@
import 'package:flutter/cupertino.dart';
import 'package:subwrite/theme.dart';
class Suggestion {
String classType;
String name;
String typeSignature;
int partialLen;
Suggestion(this.classType, this.name, this.partialLen, this.typeSignature);
@override
bool operator ==(Object other) {
return other is Suggestion && hashCode == other.hashCode;
}
@override
int get hashCode => classType.hashCode + name.hashCode + typeSignature.hashCode;
Widget getWidget() {
var color = function;
switch (classType) {
case "const":
color = constant;
break;
case "var":
color = variable;
break;
case "package":
color = keyword;
break;
case "type":
color = header;
break;
}
return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
RichText(text: TextSpan(text: name, style: TextStyle(color: color, fontFamily: "Iosevka", fontSize: 11.0, fontWeight: FontWeight.bold))),
RichText(text: TextSpan(text: typeSignature, style: const TextStyle(fontFamily: "Iosevka", fontSize: 10.0))),
]);
}
}

21
lib/theme.dart Normal file
View File

@ -0,0 +1,21 @@
import 'dart:ui';
Color foreground = const Color(0xfff8f8f2);
Color background = const Color(0xff000000); //Color(0xFF1A0F1C);
Color comment = const Color(0xffd684e8);
Color selection = const Color(0xff777889);
Color constant = const Color(0xff8be9fd);
Color header = const Color(0xffbd93f9);
Color keyword = const Color(0xffffb86c);
Color variable = const Color(0xffff79c6);
Color function = const Color(0xfff1fa8c);
Color literal = const Color(0xff50fa7b);
Color field = header;
Color risk = const Color(0xff45353b);
Color sidebarAlt = const Color(0xFF110F15);
Color sidebar = const Color(0xff202023);
Color tabs = const Color(0xFF342036);
Color sidebarHighlight = const Color(0xFF38213D);

46
lib/view.dart Normal file
View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'file.dart';
import 'line_info.dart';
class View extends StatefulWidget {
View({Key? key}) : super(key: key);
@override
_View createState() => _View();
}
class _View extends State<View> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
LineFile doc = Provider.of<LineFile>(context);
return Scrollbar(
controller: doc.scrollController,
trackVisibility: true,
thumbVisibility: true,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
controller: doc.scrollController,
itemCount: doc.lines(),
padding: EdgeInsets.zero,
shrinkWrap: true,
itemBuilder: (BuildContext bcontext, int index) {
return ChangeNotifierProvider.value(
value: doc.backingLines[index],
builder: (lcontext, lineinfo) {
return Provider.of<LineInfo>(lcontext).build(lcontext);
});
}));
}
}

1
linux/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
flutter/ephemeral

138
linux/CMakeLists.txt Normal file
View File

@ -0,0 +1,138 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "sarahdown")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.sarahdown")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Define the application target. To change its name, change BINARY_NAME above,
# not the value here, or `flutter run` will no longer work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

View File

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -0,0 +1,23 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

6
linux/main.cc Normal file
View File

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

104
linux/my_application.cc Normal file
View File

@ -0,0 +1,104 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "subwrite");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "subwrite");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

18
linux/my_application.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

175
pubspec.lock Normal file
View File

@ -0,0 +1,175 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.9.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.0"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
lints:
dependency: transitive
description:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.12"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.5"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.3"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.12"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
sdks:
dart: ">=2.18.0 <3.0.0"
flutter: ">=1.16.0"

98
pubspec.yaml Normal file
View File

@ -0,0 +1,98 @@
name: subwrite
description: A Ridiculously Personalized Markdown Editor
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=2.18.0 <3.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
provider: ^6.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
fonts:
- family: Iosevka
fonts:
- asset: fonts/iosevka-light.ttf
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

30
test/widget_test.dart Normal file
View File

@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:subwrite/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}