Skip to content

Commit 0119c62

Browse files
authored
add dummy terminal (#27)
1 parent 4ca6249 commit 0119c62

14 files changed

+325
-159
lines changed

.github/actions/setup-flutter/action.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ runs:
66
steps:
77
- uses: subosito/flutter-action@v2
88
with:
9-
flutter-version: "3.16.0"
9+
flutter-version: "3.16.8"
1010
channel: "stable"
1111
cache: true

.github/dependabot.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@ updates:
77
interval: "weekly"
88
assignees:
99
- "akaiser"
10+
- package-ecosystem: "github-actions"
11+
directory: "/"
12+
schedule:
13+
interval: "weekly"
14+
assignees:
15+
- "akaiser"

analysis_options.yaml

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
include: package:very_good_analysis/analysis_options.yaml
1+
include: package:lints/recommended.yaml
2+
3+
analyzer:
4+
language:
5+
strict-casts: true
6+
strict-inference: true
7+
strict-raw-types: true
28

39
linter:
410
rules:
5-
always_put_required_named_parameters_first: false
6-
avoid_multiple_declarations_per_line: false
7-
depend_on_referenced_packages: false
8-
implicit_call_tearoffs: false
9-
no_leading_underscores_for_local_identifiers: false
11+
always_use_package_imports: true
12+
prefer_const_constructors: true
13+
prefer_const_constructors_in_immutables: true
14+
prefer_const_declarations: true
15+
prefer_const_literals_to_create_immutables: true
1016
prefer_expression_function_bodies: true
11-
public_member_api_docs: false
12-
use_key_in_widget_constructors: false
17+
prefer_final_fields: true
18+
prefer_final_in_for_each: true
19+
prefer_final_locals: true
20+
prefer_single_quotes: true
21+
require_trailing_commas: true

lib/_data.dart

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:mdi/apps/calculator.dart';
44
import 'package:mdi/apps/manual_count.dart';
55
import 'package:mdi/apps/some_grid_view.dart';
66
import 'package:mdi/apps/some_split_view.dart';
7+
import 'package:mdi/apps/terminal.dart';
78
import 'package:mdi/apps/tik_tak_toe.dart';
89
import 'package:mdi/apps/use_keyboard.dart';
910
import 'package:mdi/desktop/desktop_app.dart';
@@ -54,4 +55,10 @@ const standaloneApps = <DesktopApp>[
5455
height: 330,
5556
isFixedSize: true,
5657
),
58+
// TODO(albert): do not forget about the Editor!
59+
DesktopApp(
60+
'Terminal',
61+
Icons.text_snippet,
62+
Terminal(),
63+
),
5764
];

lib/apps/calculator.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,11 @@ class _Button extends StatelessWidget {
192192

193193
@override
194194
Widget build(BuildContext context) {
195-
final _onPressed = onPressed;
195+
final onPressed = this.onPressed;
196196
return Expanded(
197197
flex: flex,
198198
child: TextButton(
199-
onPressed: _onPressed != null ? () => _onPressed(text) : null,
199+
onPressed: onPressed != null ? () => onPressed(text) : null,
200200
style: ButtonStyle(
201201
padding: MaterialStateProperty.all(EdgeInsets.zero),
202202
foregroundColor: MaterialStateProperty.all(_textColor),

lib/apps/some_grid_view.dart

+11-7
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,17 @@ class _SomeGridViewState extends State<SomeGridView> {
6868
),
6969
),
7070
Expanded(
71-
child: SimpleGridView(
72-
columnCount: _columnCount,
73-
rowCount: _rowCount,
74-
cellPadding: _cellPadding,
75-
cellBackgroundColor: Colors.blueAccent,
76-
cellBuilder: (_, xIndex, yIndex) => Center(
77-
child: Text('$xIndex:$yIndex'),
71+
child: Padding(
72+
padding: const EdgeInsets.all(_cellPadding),
73+
child: SimpleGridView(
74+
columnCount: _columnCount,
75+
rowCount: _rowCount,
76+
cellBuilder: (context, xIndex, yIndex) => Container(
77+
margin: const EdgeInsets.all(_cellPadding),
78+
alignment: Alignment.center,
79+
color: Colors.blueAccent,
80+
child: Text('$xIndex:$yIndex'),
81+
),
7882
),
7983
),
8084
),

lib/apps/terminal.dart

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/scheduler.dart';
3+
import 'package:flutter/services.dart';
4+
import 'package:mdi/_prefs.dart';
5+
6+
const _promptIntro = 'Welcome to Dummy Terminal (v0.0.1)';
7+
const _promptPrefix = r'user@local ~ $ ';
8+
const _textStyle = TextStyle(fontSize: 14, color: Colors.white);
9+
10+
const _charRevealDuration = Duration(milliseconds: 200);
11+
const _cursorBlinkDuration = Duration(milliseconds: 400);
12+
13+
class Terminal extends StatefulWidget {
14+
const Terminal({super.key});
15+
16+
@override
17+
State<Terminal> createState() => _TerminalState();
18+
}
19+
20+
class _TerminalState extends State<Terminal> {
21+
late final _scrollController = ScrollController();
22+
late final _focus = FocusNode();
23+
24+
final _lines = <String>[];
25+
final _chars = <String>[_promptPrefix];
26+
27+
@override
28+
void dispose() {
29+
_scrollController.dispose();
30+
_focus.dispose();
31+
super.dispose();
32+
}
33+
34+
void _onKey(RawKeyEvent event) {
35+
if (event is RawKeyDownEvent) {
36+
if (event.logicalKey == LogicalKeyboardKey.enter) {
37+
setState(() {
38+
_lines.add(_chars.join());
39+
_chars
40+
..clear()
41+
..add(_promptPrefix);
42+
});
43+
SchedulerBinding.instance.addPostFrameCallback(
44+
(_) async => _scrollController.jumpTo(
45+
_scrollController.position.maxScrollExtent,
46+
),
47+
);
48+
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
49+
if (_chars.length > 1) {
50+
setState(_chars.removeLast);
51+
}
52+
} else {
53+
final character = event.character;
54+
if (character != null) {
55+
setState(() => _chars.add(character));
56+
}
57+
}
58+
}
59+
}
60+
61+
@override
62+
Widget build(BuildContext context) => MouseRegion(
63+
// TODO(albert): check if this can be done on higher level
64+
// when window receives focus for example.
65+
onEnter: (_) => _focus.requestFocus(),
66+
child: Padding(
67+
padding: const EdgeInsets.only(left: 6),
68+
child: RawKeyboardListener(
69+
focusNode: _focus,
70+
autofocus: true,
71+
onKey: _onKey,
72+
child: RawScrollbar(
73+
thumbColor: titleBarTextColor,
74+
thickness: 5,
75+
child: DefaultTextStyle.merge(
76+
style: _textStyle,
77+
child: ListView(
78+
controller: _scrollController,
79+
children: [
80+
const Text(_promptIntro),
81+
const SizedBox(height: 8),
82+
..._lines.map(Text.new),
83+
Wrap(
84+
children: [
85+
..._chars.map(_Character.new),
86+
const _Cursor(),
87+
],
88+
),
89+
],
90+
),
91+
),
92+
),
93+
),
94+
),
95+
);
96+
}
97+
98+
class _Character extends StatelessWidget {
99+
const _Character(this.char);
100+
101+
final String char;
102+
103+
@override
104+
Widget build(BuildContext context) => _Animated(
105+
_charRevealDuration,
106+
onInit: (controller) => controller.forward(),
107+
child: Text(char),
108+
);
109+
}
110+
111+
class _Cursor extends StatelessWidget {
112+
const _Cursor();
113+
114+
@override
115+
Widget build(BuildContext context) => _Animated(
116+
_cursorBlinkDuration,
117+
onInit: (controller) => controller.repeat(reverse: true),
118+
child: const Text('▌'),
119+
);
120+
}
121+
122+
class _Animated extends StatefulWidget {
123+
const _Animated(
124+
this.animationDuration, {
125+
required this.onInit,
126+
required this.child,
127+
});
128+
129+
final Duration animationDuration;
130+
final void Function(AnimationController controller) onInit;
131+
final Widget child;
132+
133+
@override
134+
_AnimatedState createState() => _AnimatedState();
135+
}
136+
137+
class _AnimatedState extends State<_Animated>
138+
with SingleTickerProviderStateMixin {
139+
late final _controller = AnimationController(
140+
vsync: this,
141+
duration: widget.animationDuration,
142+
);
143+
144+
@override
145+
void initState() {
146+
super.initState();
147+
widget.onInit(_controller);
148+
}
149+
150+
@override
151+
void dispose() {
152+
_controller.dispose();
153+
super.dispose();
154+
}
155+
156+
@override
157+
Widget build(BuildContext context) => FadeTransition(
158+
opacity: _controller,
159+
child: widget.child,
160+
);
161+
}

lib/apps/tik_tak_toe.dart

+40-40
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import 'package:flutter/foundation.dart';
21
import 'package:flutter/material.dart';
32
import 'package:mdi/apps/widgets/simple_grid_view.dart';
43

@@ -37,48 +36,36 @@ class _TikTakToeState extends State<TikTakToe> {
3736
bool _isCrossTurn = true;
3837

3938
@override
40-
Widget build(BuildContext context) => SimpleGridView(
41-
columnCount: _columnCount,
42-
rowCount: _rowCount,
43-
cellBuilder: (context, xIndex, yIndex) {
44-
final current = '$xIndex:$yIndex';
45-
final existing = _selection[current];
46-
47-
return GestureDetector(
48-
onTap: existing == null
49-
? () {
50-
setState(
51-
() {
52-
final type = _isCrossTurn ? _Type.X : _Type.O;
53-
_selection[current] = type;
39+
Widget build(BuildContext context) => Padding(
40+
padding: const EdgeInsets.all(_cellPadding),
41+
child: SimpleGridView(
42+
columnCount: _columnCount,
43+
rowCount: _rowCount,
44+
cellBuilder: (context, xIndex, yIndex) {
45+
final current = '$xIndex:$yIndex';
46+
final existing = _selection[current];
47+
48+
return _Cell(
49+
child: existing != null
50+
? FittedBox(
51+
child: Icon(
52+
existing == _Type.X
53+
? Icons.close
54+
: Icons.radio_button_unchecked,
55+
color: Colors.white,
56+
),
57+
)
58+
: GestureDetector(
59+
onTap: () async {
60+
_selection[current] = _isCrossTurn ? _Type.X : _Type.O;
5461
_isCrossTurn = !_isCrossTurn;
62+
setState(() {});
63+
await _lookupWinner();
5564
},
56-
);
57-
_lookupWinner();
58-
}
59-
: null,
60-
child: existing != null
61-
? FittedBox(
62-
child: Icon(
63-
existing == _Type.X
64-
? Icons.close
65-
: Icons.radio_button_unchecked,
66-
color: Colors.white,
6765
),
68-
)
69-
: kDebugMode
70-
? Text(
71-
current,
72-
style: const TextStyle(
73-
fontSize: 12,
74-
color: Colors.grey,
75-
),
76-
)
77-
: null,
78-
);
79-
},
80-
cellPadding: _cellPadding,
81-
cellBackgroundColor: Colors.black,
66+
);
67+
},
68+
),
8269
);
8370

8471
Future<void> _lookupWinner() async {
@@ -132,3 +119,16 @@ class _TikTakToeState extends State<TikTakToe> {
132119
),
133120
);
134121
}
122+
123+
class _Cell extends StatelessWidget {
124+
const _Cell({required this.child});
125+
126+
final Widget child;
127+
128+
@override
129+
Widget build(BuildContext context) => Container(
130+
margin: const EdgeInsets.all(_cellPadding),
131+
color: Colors.black,
132+
child: child,
133+
);
134+
}

0 commit comments

Comments
 (0)