Skip to content

Commit 804e39a

Browse files
authored
Interactive CLI (#84)
* Very basic terminal with Lanterna * Working cascading text * First prototype * Added commands support * Support paste action * Updated version * Added support for custom commands * Added README * Color-coded logs
1 parent e6b280b commit 804e39a

8 files changed

Lines changed: 480 additions & 0 deletions

File tree

core/src/main/java/xyz/gianlu/librespot/BytesArrayList.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package xyz.gianlu.librespot;
22

33
import org.jetbrains.annotations.NotNull;
4+
import xyz.gianlu.librespot.common.Utils;
45

56
import java.io.InputStream;
67
import java.util.Arrays;
@@ -70,6 +71,15 @@ public String toString() {
7071
return Arrays.deepToString(toArray());
7172
}
7273

74+
@NotNull
75+
public String toHex() {
76+
String[] array = new String[size()];
77+
byte[][] copy = toArray();
78+
for (int i = 0; i < copy.length; i++)
79+
array[i] = Utils.bytesToHex(copy[i]);
80+
return Arrays.toString(array);
81+
}
82+
7383
@NotNull
7484
public InputStream stream() {
7585
return new InternalStream();

interactive-cli/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Interactive CLI
2+
This module provides support for an "interactive" command-line interface designed mainly for debugging and automation.
3+
The interface is very basic (no scrolling or copying is possible), but, in exchange, I've added support for custom commands without the need of recompiling.
4+
5+
## Custom commands
6+
Custom commands allow to simplify some operations by acting as aliases. You can specify a file with the `--custom-commands` option, the file stores some JSON values as described below.
7+
8+
#### JSON schema
9+
Every command has these values:
10+
- `startsWith`, the alias name
11+
- `command`, the alias will resolve to this command (use `%s` to specify arguments)
12+
- `arguments`, the number of arguments
13+
14+
Some example commands are already included inside `interactive-cli/commands.json`.
15+
16+
17+
## WIP
18+
As mentioned before, this is very basic. Please open an issue (or PR) if you think something extra is needed.

interactive-cli/commands.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"commands": [
3+
{
4+
"startsWith": "resolve-context",
5+
"arguments": 1,
6+
"command": "request GET hm://context-resolve/v1/%s"
7+
}
8+
]
9+
}

interactive-cli/pom.xml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<parent>
6+
<groupId>xyz.gianlu.librespot</groupId>
7+
<artifactId>librespot-java</artifactId>
8+
<version>0.5.1</version>
9+
<relativePath>../</relativePath>
10+
</parent>
11+
12+
<artifactId>interactive-cli</artifactId>
13+
<packaging>jar</packaging>
14+
15+
<name>librespot-java interactive CLI</name>
16+
<build>
17+
<finalName>interactive-cli</finalName>
18+
<plugins>
19+
<plugin>
20+
<groupId>org.apache.maven.plugins</groupId>
21+
<artifactId>maven-assembly-plugin</artifactId>
22+
<executions>
23+
<execution>
24+
<phase>package</phase>
25+
<goals>
26+
<goal>single</goal>
27+
</goals>
28+
<configuration>
29+
<archive>
30+
<manifest>
31+
<mainClass>xyz.gianlu.librespot.interactivecli.Main</mainClass>
32+
</manifest>
33+
</archive>
34+
<descriptorRefs>
35+
<descriptorRef>jar-with-dependencies</descriptorRef>
36+
</descriptorRefs>
37+
</configuration>
38+
</execution>
39+
</executions>
40+
</plugin>
41+
</plugins>
42+
</build>
43+
44+
<dependencies>
45+
<dependency>
46+
<groupId>xyz.gianlu.librespot</groupId>
47+
<artifactId>librespot-core</artifactId>
48+
<version>${project.version}</version>
49+
</dependency>
50+
<dependency>
51+
<groupId>xyz.gianlu.librespot</groupId>
52+
<artifactId>librespot-common</artifactId>
53+
<version>${project.version}</version>
54+
</dependency>
55+
56+
<!-- GUI -->
57+
<dependency>
58+
<groupId>com.googlecode.lanterna</groupId>
59+
<artifactId>lanterna</artifactId>
60+
<version>3.0.1</version>
61+
</dependency>
62+
</dependencies>
63+
</project>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package xyz.gianlu.librespot.interactivecli;
2+
3+
import com.google.gson.JsonObject;
4+
import org.apache.log4j.Logger;
5+
import org.jetbrains.annotations.NotNull;
6+
import xyz.gianlu.librespot.core.Session;
7+
import xyz.gianlu.librespot.mercury.MercuryClient;
8+
import xyz.gianlu.librespot.mercury.RawMercuryRequest;
9+
10+
import java.io.IOException;
11+
import java.util.Arrays;
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
15+
/**
16+
* @author Gianlu
17+
*/
18+
public class CommandsHandler {
19+
private final static Logger LOGGER = Logger.getLogger(CommandsHandler.class);
20+
private final Session session;
21+
private final Map<String, CustomCommand> customCommands = new HashMap<>();
22+
23+
public CommandsHandler(@NotNull Session session) {
24+
this.session = session;
25+
}
26+
27+
void handle(@NotNull String cmd) throws IOException {
28+
if (cmd.startsWith("play")) {
29+
session.player().play();
30+
} else if (cmd.startsWith("pause")) {
31+
session.player().pause();
32+
} else if (cmd.startsWith("next")) {
33+
session.player().next();
34+
} else if (cmd.startsWith("prev")) {
35+
session.player().previous();
36+
} else if (cmd.startsWith("currentlyPlaying")) {
37+
LOGGER.info("Currently playing: " + session.player().currentPlayableId());
38+
} else if (cmd.startsWith("request")) {
39+
String[] split = cmd.split("\\s");
40+
if (split.length != 3) {
41+
LOGGER.warn("Invalid command!");
42+
return;
43+
}
44+
45+
MercuryClient.Response resp = session.mercury().sendSync(RawMercuryRequest.newBuilder()
46+
.setMethod(split[1]).setUri(split[2])
47+
.build());
48+
49+
LOGGER.info("Uri: " + resp.uri);
50+
LOGGER.info("Status code: " + resp.statusCode);
51+
LOGGER.info("Payload: " + resp.payload.toHex());
52+
} else {
53+
for (String custom : customCommands.keySet()) {
54+
if (cmd.startsWith(custom)) {
55+
customCommands.get(custom).handle(cmd);
56+
return;
57+
}
58+
}
59+
60+
LOGGER.warn("Unknown command: " + cmd);
61+
}
62+
}
63+
64+
void addCustomCommand(@NotNull JsonObject obj) {
65+
String startsWith = obj.get("startsWith").getAsString();
66+
customCommands.put(startsWith, new CustomCommand(obj));
67+
}
68+
69+
private class CustomCommand {
70+
final int arguments;
71+
final String command;
72+
final String startsWith;
73+
74+
CustomCommand(@NotNull JsonObject obj) {
75+
this.arguments = obj.get("arguments").getAsInt();
76+
this.command = obj.get("command").getAsString();
77+
this.startsWith = obj.get("startsWith").getAsString();
78+
79+
if (command.startsWith(startsWith))
80+
throw new IllegalArgumentException("Check your command! You'll recurse infinitely.");
81+
}
82+
83+
void handle(@NotNull String cmd) throws IOException {
84+
String[] split = cmd.split("\\s");
85+
if (split.length - 1 != arguments) {
86+
LOGGER.warn(String.format("Invalid command! Required %d argument(s), but given %d.", arguments, split.length - 1));
87+
return;
88+
}
89+
90+
String[] args = Arrays.copyOfRange(split, 1, split.length);
91+
CommandsHandler.this.handle(String.format(command, (Object[]) args));
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)