Skip to content

Commit 8d7609b

Browse files
committed
[add] split config and loop commands
1 parent 9c90b03 commit 8d7609b

23 files changed

+372
-72
lines changed

lib/core/core.dart

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export 'command.dart';
2+
export 'command_matcher.dart';
23
export 'config.dart';

lib/features/configure/configure.dart

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'dart:async';
2+
import 'dart:io';
3+
4+
import 'package:args/command_runner.dart';
5+
import 'package:terrun/services/services.dart';
6+
import 'package:terrun/services/shell/shell.dart';
7+
8+
import 'defaults.dart';
9+
10+
class ConfigureCommand extends Command<void> {
11+
final DisplayService display;
12+
final ShellService shell;
13+
final Env env;
14+
15+
ConfigureCommand(
16+
this.display,
17+
this.shell,
18+
this.env,
19+
);
20+
21+
@override
22+
String get description => 'Creates your starter terrun.yaml file if it doesn\'t exist.';
23+
24+
@override
25+
String get name => 'configure';
26+
27+
@override
28+
Future<void> run() async {
29+
var defaultLocation = '${env.home}/.config/terrun/terrun.yaml';
30+
if (File(defaultLocation).existsSync()) {
31+
display.drawMessage('File $defaultLocation already exists');
32+
return;
33+
}
34+
35+
final lines = [
36+
'mkdir -p ~/.config/terrun',
37+
'touch $defaultLocation',
38+
'echo \'$defaultsYaml\' > $defaultLocation',
39+
];
40+
41+
display.drawMessage('Writing config file...', type: MessageType.success);
42+
for (final line in lines) {
43+
final result = await shell.run(line);
44+
if (result.exitCode != 0) {
45+
display.drawMessage(
46+
'error while executing "$line":',
47+
type: MessageType.error,
48+
);
49+
display.drawMessage(result.stdout);
50+
display.drawMessage(result.stderr);
51+
}
52+
}
53+
}
54+
}

lib/features/configure/defaults.dart

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// ignore_for_file: unnecessary_string_escapes
2+
3+
final defaultsYaml='''commands:
4+
j:
5+
name: apps
6+
children:
7+
j:
8+
name: Chrome
9+
command: open -a Google\ Chrome.app
10+
t:
11+
name: Telegram
12+
command: open -a telegram
13+
p:
14+
name: Spotify
15+
command: open -a spotify
16+
k:
17+
name: Slack
18+
command: open -a slack
19+
d:
20+
name: VSCode
21+
command: open -a Visual\ Studio\ Code.app
22+
23+
# todo(user): customize it
24+
# f:
25+
# name: projects
26+
# children:
27+
# a:
28+
# name: project a
29+
# command: code ~/path/to/my_projects/a
30+
# b:
31+
# name: project b
32+
# command: code ~/path/to/my_projects/b
33+
34+
# hooks:
35+
# preRun:
36+
# #this emits the shortcut to hide the terminal window
37+
# - say 'good job \$USER'
38+
# - command: osascript -e 'tell application "System Events" to key code 24 using {shift down, control down}'
39+
''';

lib/features/features.dart

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export 'loop.dart';

lib/features/loop.dart

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:args/command_runner.dart' as runner;
5+
import 'package:terrun/core/core.dart';
6+
import 'package:terrun/services/services.dart';
7+
8+
class LoopCommand extends runner.Command<void> {
9+
final RunnerService shellRunner;
10+
final DisplayService display;
11+
final Env env;
12+
13+
LoopCommand(
14+
this.shellRunner,
15+
this.display,
16+
this.env,
17+
);
18+
19+
@override
20+
String get description => 'Runs commands defined in config file';
21+
22+
@override
23+
String get name => 'loop';
24+
25+
@override
26+
Future<void> run() async {
27+
final matcher = CommandMatcher();
28+
29+
final configContent = ConfigReader(env).read();
30+
if (configContent == null) {
31+
display.drawMessage(
32+
'Error: config.yaml file not found. Run `terrun config` to add it.',
33+
type: MessageType.error,
34+
);
35+
return;
36+
}
37+
38+
final config = ConfigParser().parse(configContent);
39+
final commands = config.commands;
40+
41+
display.drawMatchingCommands('', commands);
42+
43+
while (true) {
44+
String input = '';
45+
bool isPrefix = true;
46+
Command? selectedCommand;
47+
48+
while (isPrefix) {
49+
final character = utf8.decode([stdin.readByteSync()]);
50+
final newInput = input + character;
51+
isPrefix = matcher.getPotentialMatches(commands, newInput) > 0;
52+
if (isPrefix) {
53+
input = newInput;
54+
display.drawMatchingCommands(input, commands);
55+
selectedCommand = matcher.getFromTree(commands, input);
56+
57+
if (selectedCommand != null) {
58+
input = '';
59+
stdout.writeln('running command:$selectedCommand');
60+
break;
61+
}
62+
} else {
63+
input = '';
64+
display.clear();
65+
display.drawMatchingCommands(input, commands);
66+
67+
var errorMessage = 'Error: "$newInput" didnt matchin any of keys above';
68+
print(errorMessage.colored(1));
69+
}
70+
}
71+
72+
if (selectedCommand != null) {
73+
if (selectedCommand.script != null) {
74+
await shellRunner.run(
75+
selectedCommand.script!,
76+
config.hooks,
77+
);
78+
}
79+
selectedCommand = null;
80+
display.drawMatchingCommands('', commands);
81+
}
82+
}
83+
}
84+
}

lib/main.dart

+28-55
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,33 @@
1-
import 'dart:convert';
2-
import 'dart:io';
3-
4-
import 'package:terrun/core/command_matcher.dart';
5-
6-
import 'core/core.dart';
7-
import 'services/services.dart';
1+
import 'package:args/command_runner.dart';
2+
import 'package:terrun/features/configure/configure.dart';
3+
import 'package:terrun/features/loop.dart';
4+
import 'package:terrun/services/services.dart';
5+
import 'package:terrun/services/shell/shell_service.dart';
6+
import 'package:terrun/services/shell/shell_service_impl.dart';
87

98
Future<void> app(List<String> args) async {
10-
final matcher = CommandMatcher();
11-
final RunnerService runner = ProcessRunnerService();
12-
final DisplayService display = ConsoleDisplaySevice()..init();
13-
14-
final configContent = ConfigReader().read();
15-
final config = ConfigParser().parse(configContent);
16-
final commands = config.commands;
17-
18-
display.drawMatchingCommands('', commands);
9+
var env = Env.load();
1910

20-
while (true) {
21-
String input = '';
22-
bool isPrefix = true;
23-
Command? selectedCommand;
24-
25-
while (isPrefix) {
26-
final character = utf8.decode([stdin.readByteSync()]);
27-
final newInput = input + character;
28-
isPrefix = matcher.getPotentialMatches(commands, newInput) > 0;
29-
if (isPrefix) {
30-
input = newInput;
31-
display.drawMatchingCommands(input, commands);
32-
selectedCommand = matcher.getFromTree(commands, input);
33-
34-
if (selectedCommand != null) {
35-
input = '';
36-
stdout.writeln('running command:$selectedCommand');
37-
break;
38-
}
39-
} else {
40-
input = '';
41-
display.clear();
42-
display.drawMatchingCommands(input, commands);
43-
44-
var errorMessage = 'Error: "$newInput" didnt matchin any of keys above';
45-
print(errorMessage.colored(1));
46-
}
47-
}
11+
final ShellService shell = ShellServiceImpl();
12+
final RunnerService shellRunner = ProcessRunnerService(shell);
13+
final DisplayService display = ConsoleDisplaySevice()..init();
4814

49-
if (selectedCommand != null) {
50-
if (selectedCommand.script != null) {
51-
await runner.run(
52-
selectedCommand.script!,
53-
config.hooks,
54-
);
55-
}
56-
selectedCommand = null;
57-
display.drawMatchingCommands('', commands);
58-
}
59-
}
15+
final commandRunner = CommandRunner(
16+
'terrun',
17+
'run anything with minimum keystrokes',
18+
);
19+
20+
commandRunner
21+
..addCommand(ConfigureCommand(
22+
display,
23+
shell,
24+
env,
25+
))
26+
..addCommand(LoopCommand(
27+
shellRunner,
28+
display,
29+
env,
30+
));
31+
32+
await commandRunner.run(args);
6033
}
+22-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
11
import 'dart:io';
22

3+
import 'package:terrun/services/services.dart';
4+
35
class ConfigReader {
4-
String read() {
5-
return File('config.yaml').readAsStringSync();
6+
final _filename = 'terrun.yaml';
7+
final Env env;
8+
9+
ConfigReader(this.env);
10+
11+
String? read({String? customConfigPath}) {
12+
final configCandidates = [
13+
'${env.home}/.config/terrun/',
14+
'./',
15+
];
16+
final firstFoundConfigPath = configCandidates.map((dir) => dir + _filename).firstWhere(
17+
(file) => File(file).existsSync(),
18+
orElse: () => '',
19+
);
20+
try {
21+
final file = File(customConfigPath ?? firstFoundConfigPath);
22+
return file.readAsStringSync();
23+
} catch (_) {
24+
return null;
25+
}
626
}
727
}

lib/services/display/console_display_service.dart

+21
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import 'dart:io';
22

3+
import 'package:terrun/services/display/theme.dart';
34
import 'package:terrun/services/services.dart';
45

56
import '../../core/core.dart';
67

78
class ConsoleDisplaySevice implements DisplayService {
9+
10+
final theme = Theme(
11+
info: 15,
12+
error: 1,
13+
success: 2,
14+
);
15+
816
@override
917
void drawMatchingCommands(
1018
String input,
@@ -49,4 +57,17 @@ class ConsoleDisplaySevice implements DisplayService {
4957
stdout.writeln('Input: ${input.colored(15, bg: 0)}');
5058
stdout.writeln('----------------------');
5159
}
60+
61+
@override
62+
void drawMessage(String message, {MessageType? type = MessageType.info}) {
63+
_clear();
64+
final color = {
65+
MessageType.success: theme.success,
66+
MessageType.error: theme.error,
67+
MessageType.info: theme.info,
68+
}[type] ??
69+
theme.info;
70+
71+
stderr.writeln(message.colored(color));
72+
}
5273
}
+10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import '../../core/core.dart';
22

3+
enum MessageType {
4+
info,
5+
success,
6+
error,
7+
}
8+
39
abstract class DisplayService {
410
void drawMatchingCommands(
511
String input,
612
Map<String, Command> commands,
713
);
814
void init();
915
void clear();
16+
void drawMessage(
17+
String message, {
18+
MessageType? type = MessageType.info,
19+
});
1020
}

lib/services/display/theme.dart

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// https://raw.github.com/google/ansicolor-dart/master/ansicolor-dart.png
2+
3+
//will be configurable in yaml later
4+
class Theme {
5+
final int error;
6+
final int success;
7+
final int info;
8+
9+
Theme({
10+
required this.error,
11+
required this.success,
12+
required this.info,
13+
});
14+
}

lib/services/env/env.dart

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import 'dart:io';
2+
3+
class Env {
4+
final String home;
5+
final String shell;
6+
7+
Env(
8+
this.home,
9+
this.shell,
10+
);
11+
12+
factory Env.load() {
13+
final values = Platform.environment;
14+
15+
return Env(
16+
values['HOME']!,
17+
values['SHELL']!,
18+
);
19+
}
20+
}

0 commit comments

Comments
 (0)