Compare commits

...

41 Commits

Author SHA1 Message Date
b0be0ba8b4 HomeContext new ui system 2023-02-05 02:56:18 +01:00
a4e4e103bd something something ui 2023-02-05 02:56:04 +01:00
3e7d993662 printer 2023-02-04 23:24:57 +01:00
dabc42ea25 print help in rs command 2023-02-04 20:44:02 +01:00
af8241505f added getHight and getWidth to Rect 2023-02-04 20:43:39 +01:00
d56b871554 improved canvas render to context 2023-02-04 20:41:10 +01:00
8934d40c0b use new event delegate in HomeContext 2023-01-30 16:44:52 +01:00
8f7739c26a implemented UIEvents delegate in WindowContext 2023-01-30 16:44:17 +01:00
a1ce5957d1 use simple list renderer in HomeContext 2023-01-30 16:06:46 +01:00
9e128eaad2 added simple list renderer 2023-01-30 16:06:27 +01:00
881aad743d added renderToContext to Canvas 2023-01-30 16:06:13 +01:00
df86bae738 removed unused import 2023-01-30 03:15:13 +01:00
c731dcef36 i fogor again 2023-01-30 03:14:54 +01:00
e14138c7a0 i forgor 2023-01-30 03:04:53 +01:00
d684360547 renamed MainTerm to HomeContext to avoid confusion 2023-01-30 03:04:20 +01:00
a9eb569a02 moved log to own package 2023-01-30 02:59:55 +01:00
77d330a71d removed unused allContexts 2023-01-30 02:49:43 +01:00
391c6b19fd refactored Logging 2023-01-30 02:47:54 +01:00
632173d122 README feature update 2023-01-28 03:58:26 +01:00
7aa1306077 documentation 2023-01-28 03:56:32 +01:00
15b7112348 changed startup 2023-01-28 03:56:21 +01:00
2cbcc86dce added main term (may rename later) 2023-01-28 03:56:07 +01:00
f71505ce28 removed unused file 2023-01-28 03:55:46 +01:00
cb1f892e6d added UIApp and renamed CLIBase 2023-01-28 03:55:32 +01:00
b54a25eec6 added stacktrace to debug flags 2023-01-28 02:33:38 +01:00
a87f50eec7 added disk cli program 2023-01-27 14:34:25 +01:00
95a9ab63d0 added side fromString 2023-01-27 14:34:06 +01:00
f6c0feda4b catch setDiskLabel error 2023-01-27 13:50:51 +01:00
916831743e removed get side in redstone 2023-01-27 13:39:20 +01:00
72e71c8e36 added Restone cli 2022-12-19 23:28:46 +01:00
8180599c04 Net cli update on new icmp 2022-12-19 22:57:41 +01:00
0b965d32b6 added more icmp stuff 2022-12-19 22:33:17 +01:00
f988c79e2f improved Routing 2022-12-19 21:25:39 +01:00
45a8851e2a private routing method 2022-12-19 21:11:01 +01:00
a6ed7818da another big refactor 2022-12-19 21:06:23 +01:00
3cb1811dcb improved terminal stuff 2022-12-19 17:35:52 +01:00
c6e5a836ea Net improvments 2022-12-19 17:35:14 +01:00
a7dbdff535 i don't even know anymore 2022-12-17 15:08:07 +01:00
2b8aa06117 added terminal app with example hello world 2022-12-15 14:21:26 +01:00
1863462b44 fixed missing char event in windowContext 2022-12-15 14:20:46 +01:00
209e40d0d5 added entrypoint 2022-12-15 14:19:57 +01:00
72 changed files with 1598 additions and 352 deletions

View File

@@ -14,7 +14,7 @@ all: clean build
build: $(MIN_PATH)
debug: HAXE_FLAGS += -D webconsole --debug
debug: HAXE_FLAGS += -D webconsole -D error_stack --debug
debug: build
$(HAXE_PATH): $(shell find src -name '*.hx')

View File

@@ -7,6 +7,7 @@ General purpose "operation system" for [ComputerCraft](https://tweaked.cc/) buil
- Hardware abstraction
- Virtual screens to switch between multiple GUI apps
- Reactive UI framework
- Solid base to easily applications
# Building

View File

@@ -1,5 +1,5 @@
-p src
--main Startup
--main kernel.Entrypoint
--library cctweaked:git:https://git.kapelle.org/niklas/cctweaked-haxelib.git
--library tink_core

View File

@@ -1,78 +1,10 @@
import kernel.http.HTTPRequest.Http;
import util.Observable;
import lib.ui.reactive.TextElement;
import lib.ui.reactive.ReactiveUI;
import kernel.ui.WindowManager;
import kernel.net.Net;
import kernel.KernelEvents;
import kernel.Log;
import kernel.Init;
using util.Extender.LambdaExtender;
import lib.HomeContext;
class Startup {
public static function main() {
Init.initKernel();
httpTest();
}
var main = new HomeContext();
private static function httpTest() {
Log.moveToOutput("main");
// var url = "https://mock.codes/400";
// var url = "https://jsonplaceholder.typicode.com/todos/1";
var url = "https://domainnotexsist.net/aaaa";
Http.request(url).handle((outcome)->{
switch outcome{
case Success(data):
Log.debug(data.body);
case Failure(failure):
Log.error(failure.reason);
}
});
}
private static function uiTest() {
var context = WindowManager.instance.createNewContext();
var text1 = new Observable("My text");
var text2 = new Observable("Another one");
var ui = new ReactiveUI(context,[
new TextElement(text1,Orange,White,{
onClick: (p) ->{
text1.set("Click");
}
}),
new TextElement(text2,Black,White,{
onClick: (p)->{
text2.set("Click");
}
})
]);
ui.render();
WindowManager.instance.focusContextToOutput(context,"main");
}
private static function sendTest() {
Net.instance.registerProto("ping",(pack)->{
Log.debug("Message: " + pack.data);
pack.respond("Hello from: "+Net.instance.networkID);
});
KernelEvents.instance.onChar.handle((char)->{
if (char == "s"){
Log.debug("Sending to 4");
Net.instance.sendAndAwait(4,"ping","Hello world").handle((result)->{
switch (result){
case Success(pack):
Log.debug("Response: "+pack.data);
case Failure(err):
Log.error(err);
}
});
}
});
main.run();
}
}

137
src/bin/Disk.hx Normal file
View File

@@ -0,0 +1,137 @@
package bin;
import kernel.peripherals.Peripherals.Peripheral;
import lib.cli.TermHandle;
import lib.cli.CLIApp;
using tink.CoreApi;
using Lambda;
class Disk extends CLIApp {
private var handle:TermHandle;
public function new() {}
public function invoke(handle:TermHandle):Future<Bool> {
this.handle = handle;
var subcommand = handle.args[0];
var driveAddr:Null<String> = handle.args[1];
switch (subcommand) {
case "ls":
Peripheral.instance.getDrives().foreach(drive -> {
var addr = drive.getAddr();
var label = drive.getDiskLabel();
var id = drive.getDiskID();
if (drive.isDiskPresent()){
if (drive.hasAudio()){
handle.writeLn('${addr} => ${label} [AUDIO]');
}else{
handle.writeLn('${addr} => ${label} (${id})');
}
}else {
handle.writeLn('${addr} => [NO DISK]');
}
return true;
});
case "play":
var drive = Peripheral.instance.getDrive(driveAddr);
if (drive == null){
handle.writeLn("Drive not found: " + driveAddr);
return Future.sync(false);
}
if (!drive.isDiskPresent()){
handle.writeLn("No disk in drive: " + driveAddr);
return Future.sync(false);
}
if (!drive.hasAudio()){
handle.writeLn("Disk in drive " + driveAddr + " does not have audio");
return Future.sync(false);
}
drive.playAudio();
case "stop":
var drive = Peripheral.instance.getDrive(driveAddr);
if (drive == null){
handle.writeLn("Drive not found: " + driveAddr);
return Future.sync(false);
}
if (!drive.isDiskPresent()){
handle.writeLn("No disk in drive: " + driveAddr);
return Future.sync(false);
}
if (!drive.hasAudio()){
handle.writeLn("Disk in drive: " + driveAddr + " does not have audio");
return Future.sync(false);
}
drive.stopAudio();
case "eject":
var drive = Peripheral.instance.getDrive(driveAddr);
if (drive == null){
handle.writeLn("Drive not found: " + driveAddr);
return Future.sync(false);
}
if (!drive.isDiskPresent()){
handle.writeLn("No disk in drive: " + driveAddr);
return Future.sync(false);
}
drive.ejectDisk();
case "lable":
var drive = Peripheral.instance.getDrive(driveAddr);
var label:String = handle.args[2];
if (drive == null){
handle.writeLn("Drive not found: " + driveAddr);
return Future.sync(false);
}
if (!drive.isDiskPresent()){
handle.writeLn("No disk in drive: " + driveAddr);
return Future.sync(false);
}
if (label == null || label == ""){
handle.writeLn(drive.getDiskLabel());
}else{
var err = drive.setDiskLabel(label);
if (err != null){
handle.writeLn("Failed to set lable");
return Future.sync(false);
}
}
case "help":
case null:
printHelp();
default:
handle.writeLn("Unknown subcommand: " + subcommand);
printHelp();
return Future.sync(false);
}
return Future.sync(true);
}
private function printHelp() {
handle.writeLn("Usage: disk <subcommand> [args]");
handle.writeLn("Subcommands:");
handle.writeLn(" ls");
handle.writeLn(" play <drive>");
handle.writeLn(" stop <drive>");
handle.writeLn(" eject <drive>");
handle.writeLn(" label <drive> [label]");
}
}

21
src/bin/HelloWorld.hx Normal file
View File

@@ -0,0 +1,21 @@
package bin;
import lib.cli.TermHandle;
import lib.cli.CLIApp;
using tink.CoreApi;
class HelloWorld extends CLIApp {
public function new() {}
public function invoke(handle: TermHandle):Future<Bool> {
var world:String = "world";
if (handle.args.length > 0) {
world = handle.args[0];
}
handle.write('Hello, $world!');
return Future.sync(true);
}
}

106
src/bin/Net.hx Normal file
View File

@@ -0,0 +1,106 @@
package bin;
import kernel.peripherals.Peripherals.Peripheral;
import kernel.net.Routing;
import haxe.ds.ReadOnlyArray;
import lib.cli.TermHandle;
import lib.cli.CLIApp;
using tink.CoreApi;
class Net extends CLIApp {
private var handle:TermHandle;
public function new() {}
public function invoke(handle:TermHandle):Future<Bool> {
this.handle = handle;
var subcommand = handle.args[0];
var subcommand_args = handle.args.slice(1);
switch (subcommand) {
case "route":
return Future.sync(route(subcommand_args));
case "iface":
return Future.sync(iface(subcommand_args));
case "help":
printHelp();
return Future.sync(true);
case "ping":
return ping(subcommand_args);
case "proto":
return Future.sync(protos());
default:
handle.writeLn("Unknown subcommand: " + subcommand);
printHelp();
return Future.sync(false);
}
}
private function printHelp() {
handle.writeLn("net route");
handle.writeLn("net iface");
handle.writeLn("net help");
handle.writeLn("net proto");
}
private function route(args:ReadOnlyArray<String>):Bool {
var routes = Routing.instance.getRouteTable();
for(k => v in routes) {
handle.writeLn('${k} => ${v.interf.name()}(${v.cost})');
}
return true;
}
private function iface(args:ReadOnlyArray<String>):Bool {
var modems = Peripheral.instance.getModems();
for (modem in modems) {
handle.writeLn(modem.name());
}
return true;
}
function ping(args:ReadOnlyArray<String>): Future<Bool> {
return new Future<Bool>(trigger -> {
if (args.length != 1) {
handle.writeLn("Usage: net ping id");
trigger(false);
return null;
}
var toID:Null<Int> = Std.parseInt(args[0]);
if (toID == null) {
handle.writeLn("Invalid ID");
trigger(false);
return null;
}
kernel.net.Net.instance.ping(toID).handle(result -> {
switch (result){
case Success(_):
handle.writeLn("Ping succeeded");
trigger(true);
case Failure(failure):
handle.writeLn("Ping failed: " + failure);
trigger(false);
}
});
});
}
function protos():Bool {
var protos = kernel.net.Net.instance.getActiveProtocols();
for (proto in protos) {
handle.writeLn(proto);
}
return true;
}
}

44
src/bin/Redstone.hx Normal file
View File

@@ -0,0 +1,44 @@
package bin;
import kernel.peripherals.Peripherals.Peripheral;
import kernel.peripherals.Side;
import lib.cli.TermHandle;
import lib.cli.CLIApp;
using tink.CoreApi;
class Redstone extends CLIApp {
public function new() {}
public function invoke(handle:TermHandle):Future<Bool> {
var subcommand = handle.args[0];
if (subcommand == null) {
handle.writeLn("Usage: redstone <on|off|get> <side>");
return Future.sync(false);
}
var side:Null<Side> = handle.args[1];
if (side == null) {
handle.writeLn("Invalid side");
return Future.sync(false);
}
switch (subcommand) {
case "on":
Peripheral.instance.getRedstone(side).setOutput(true);
case "off":
Peripheral.instance.getRedstone(side).setOutput(false);
case "get":
var value = Peripheral.instance.getRedstone(side).getAnalogInput();
handle.writeLn("Analog input: " + value);
case "help":
handle.writeLn("Usage: redstone <on|off|get> <side>");
default:
handle.writeLn("Invalid subcommand");
return Future.sync(false);
}
return Future.sync(true);
}
}

166
src/bin/Terminal.hx Normal file
View File

@@ -0,0 +1,166 @@
package bin;
import lib.ui.UIApp;
import lib.cli.TermHandle;
import lib.cli.CLIApp;
import lib.Color;
import kernel.ui.WindowContext;
import kernel.ui.WindowManager;
using tink.CoreApi;
class Terminal extends UIApp {
private var context:WindowContext;
private var input:String = "";
private var backlog:Array<String> = [];
private var exitTrigger: Bool -> Void;
public function new() {}
public function invoke(context: WindowContext): Future<Bool> {
this.context = context;
this.context.onChar.handle(char -> {
this.input += char;
this.redrawInput();
});
this.context.onKey.handle(e -> {
if (e.keyCode == 259) {
this.input = this.input.substr(0, this.input.length - 1);
this.redrawInput();
} else if (e.keyCode == 257) {
this.backlog.push("> " + this.input);
var command = this.input;
this.input = "";
this.redrawBacklog();
this.redrawInput();
this.invokeCommand(command);
}
});
WindowManager.instance.focusContextToOutput(context, "main");
this.redrawInput();
return new Future<Bool>(cb -> {
this.exitTrigger = cb;
return null;
});
}
private function redrawBacklog() {
var size = this.context.getSize();
var linesAvailable = size.y - 1;
var start:Int = this.backlog.length - linesAvailable;
for (i in 0...linesAvailable) {
var line = this.backlog[start + i];
this.context.setCursorPos(0, i);
this.context.clearLine();
if (line != null) {
this.context.write(line);
}
}
this.moveCursorToInput();
}
private function redrawInput() {
var size = this.context.getSize();
this.context.setCursorPos(0, size.y - 1);
this.context.clearLine();
this.context.setTextColor(Color.Blue);
this.context.write("> ");
this.context.setTextColor(Color.White);
this.context.write(this.input);
this.context.setCursorBlink(true);
}
private function invokeCommand(command:String):Void {
var args = this.parseArgs(command);
if (args.length == 0) {
return;
}
var commandName = args[0];
// Handle built-in commands
switch (commandName) {
case "clear":
this.clear();
return;
}
var commandArgs:Array<String> = args.slice(1);
var hadInput = false;
var handle = new TermHandle(commandArgs, {
onWrite: (s:String) -> {
if (!hadInput) {
this.backlog.push("");
hadInput = true;
}
this.backlog[this.backlog.length - 1] += s;
this.redrawBacklog();
},
onNewLine: () -> {
this.backlog.push("");
this.redrawBacklog();
}
});
var prog:CLIApp = getProgByName(commandName);
if (prog == null) {
this.backlog.push("Command not found: " + commandName);
this.redrawBacklog();
return;
}
this.context.setCursorBlink(false);
prog.invoke(handle).handle((exitCode) -> {
// Cleanup extra newline
if (this.backlog[this.backlog.length - 1] == "") {
this.backlog.pop();
}
this.redrawInput();
});
}
private function parseArgs(command:String):Array<String> {
// TODO: tim and quote handling
return command.split(" ");
}
private function clear() {
this.backlog = [];
this.redrawBacklog();
}
private function getProgByName(name:String):CLIApp {
switch (name) {
case "hello":
return new HelloWorld();
case "net":
return new Net();
case "rs":
return new Redstone();
case "disk":
return new Disk();
default:
return null;
}
}
private function moveCursorToInput() {
var size = this.context.getSize();
this.context.setCursorPos(this.input.length + 2, size.y - 1);
}
}

14
src/kernel/Entrypoint.hx Normal file
View File

@@ -0,0 +1,14 @@
package kernel;
import kernel.log.Log;
class Entrypoint {
public static function main() {
Init.initKernel();
try {
Startup.main();
}catch(e){
Log.error('Error in startup: ${e.toString()}');
}
}
}

View File

@@ -4,7 +4,7 @@ import kernel.turtle.Turtle;
import haxe.MainLoop;
import kernel.net.Routing;
import cc.OS;
import util.Debug;
import lib.Debug;
import kernel.ui.WindowManager;
import kernel.peripherals.Peripherals.Peripheral;
import kernel.net.Net;
@@ -17,7 +17,6 @@ class Init {
WindowManager.instance = new WindowManager();
MainTerm.instance = new MainTerm();
Log.init();
if (Turtle.isTurtle()){
Turtle.instance = new Turtle();

View File

@@ -1,10 +1,11 @@
package kernel;
import util.Pos;
import kernel.log.Log;
import lib.Pos;
import cc.HTTP.HTTPResponse;
import lua.TableTools;
import lua.Coroutine;
import util.Vec.Vec2;
import lib.Vec.Vec2;
import haxe.Exception;
using tink.CoreApi;

View File

@@ -1,80 +0,0 @@
package kernel;
import kernel.ui.WindowContext;
import kernel.ui.WindowManager;
import kernel.ui.TermWriteable;
import lib.TermIO;
#if webconsole
import kernel.net.Net;
import util.Debug;
#end
/**
Log messages to specified output.
**/
class Log {
private static var context:WindowContext;
private static var writer:TermIO;
/**
Depends on: WindowManager
**/
@:allow(kernel.Init)
private static function init() {
Log.context = WindowManager.instance.createNewContext();
Log.writer = new TermIO(Log.context);
}
private static function setMainoutout(newOutput:TermWriteable) {
writer = new TermIO(newOutput);
}
public static function info(msg:Dynamic, ?pos:haxe.PosInfos) {
writer.writeLn(logLine("INFO",pos,msg));
#if webconsole
Debug.printWeb(logLine("INFO",pos,msg));
#end
}
public static function warn(msg:Dynamic, ?pos:haxe.PosInfos) {
writer.writeLn(logLine("WARN",pos,msg), Yellow);
#if webconsole
Debug.printWeb(logLine("WARN",pos,msg));
#end
}
public static function error(msg:Dynamic, ?pos:haxe.PosInfos) {
writer.writeLn(logLine("ERRO",pos,msg), Red);
#if webconsole
Debug.printWeb(logLine("ERRO",pos,msg));
#end
}
public static function debug(msg:Dynamic, ?pos:haxe.PosInfos) {
#if debug
writer.writeLn(logLine("DEBG",pos,msg), Gray);
#if webconsole
Debug.printWeb(logLine("DEBG",pos,msg));
#end
#end
}
public static function silly(msg:Dynamic, ?pos:haxe.PosInfos) {
writer.writeLn(logLine("SILY",pos,msg), LightGray);
#if webconsole
Debug.printWeb(logLine("SILY",pos,msg));
#end
}
public static function moveToOutput(addr:String) {
WindowManager.instance.focusContextToOutput(context, addr);
}
private static function logLine(tag: String,pos: haxe.PosInfos,msg: Dynamic): String {
#if debug
return '[$tag][${pos.className}:${pos.lineNumber}]: ${Std.string(msg)}';
#else
return '[$tag]: ${Std.string(msg)}';
#end
}
}

View File

@@ -1,12 +1,12 @@
package kernel;
import util.Pos;
using tink.CoreApi;
import lib.Pos;
import kernel.ui.TermWriteable;
import cc.Term;
import util.Vec.Vec2;
import util.Color;
import lib.Vec.Vec2;
import lib.Color;
using tink.CoreApi;
/**
Represents the main computer screen.

View File

@@ -1,9 +1,9 @@
package kernel;
using tink.CoreApi;
import cc.OS;
using tink.CoreApi;
/**
Wrapper class for using timer.
**/

48
src/kernel/log/Log.hx Normal file
View File

@@ -0,0 +1,48 @@
package kernel.log;
#if webconsole
import lib.Debug;
#end
/**
Log messages to specified output.
**/
class Log {
private static inline final MAX_LINES:Int = 100;
private static final logLines:Array<LogLine> = [];
public static function info(msg:Dynamic, ?pos:haxe.PosInfos) {
log({level: Info, message: Std.string(msg),time: 0});
}
public static function warn(msg:Dynamic, ?pos:haxe.PosInfos) {
log({level: Warn, message: Std.string(msg),time: 0});
}
public static function error(msg:Dynamic, ?pos:haxe.PosInfos) {
log({level: Error, message: Std.string(msg),time: 0});
}
public static function debug(msg:Dynamic, ?pos:haxe.PosInfos) {
#if debug
log({level: Debug, message: Std.string(msg),time: 0});
#end
}
public static function silly(msg:Dynamic, ?pos:haxe.PosInfos) {
log({level: Silly, message: Std.string(msg),time: 0});
}
private static function log(line: LogLine, ?pos:haxe.PosInfos) {
logLines.push(line);
if (logLines.length > MAX_LINES) {
logLines.shift();
}
#if webconsole
Debug.printWeb('[${Std.string(line.level)}] ${line.message}');
#end
}
}

View File

@@ -0,0 +1,9 @@
package kernel.log;
enum LogLevel {
Info;
Warn;
Error;
Debug;
Silly;
}

View File

@@ -0,0 +1,7 @@
package kernel.log;
typedef LogLine = {
level: LogLevel,
message: String,
time: Int,
}

View File

@@ -1,5 +1,6 @@
package kernel.net;
import kernel.log.Log;
using tink.CoreApi;
/**

View File

@@ -1,15 +1,15 @@
package kernel.net;
using tink.CoreApi;
import haxe.ds.ReadOnlyArray;
import kernel.net.Package.NetworkID;
import kernel.peripherals.Peripherals.Peripheral;
import kernel.Log;
import kernel.log.Log;
import kernel.Timer;
import cc.OS;
using tink.CoreApi;
using Lambda;
using util.Extender.LambdaExtender;
using lib.Extender.LambdaExtender;
/**
Class responsible for everything network related.
@@ -25,7 +25,7 @@ class Net {
public static inline final DEFAULT_TTL:Int = 10;
public final networkID:NetworkID = OS.getComputerID();
private final responseBus:Map<Int, Callback<Package>> = new Map();
private final responseBus:Map<Int, Callback<Outcome<Package,Error>>> = new Map();
private final protoHandlers:Map<String, Callback<Package>> = new Map();
private var interfaces:Array<INetworkInterface>;
@@ -37,6 +37,26 @@ class Net {
for (interf in interfaces){
setupInterf(interf);
}
setupPingHandle();
}
private function setupPingHandle() {
this.registerProto("icmp", pack -> {
switch pack.data.type {
case "ping":
this.respondTo(pack, "pong");
case "died":
// If we get a "died" message from a node when one of our packages ttl hits 0
// the `data.msgId` prop is the message id
var msgID:Int = pack.data.msgID;
if (responseBus.exists(msgID)) {
responseBus[msgID].invoke(Outcome.Failure(new Error("TTL reached 0")));
}
default:
Log.silly('Unknown icmp message: ${pack.data}');
}
});
}
private function setupInterf(interf: INetworkInterface) {
@@ -57,7 +77,7 @@ class Net {
case Response:
// Got a response to a send message. Invoke the callback
if (responseBus.exists(pack.msgID)) {
responseBus[pack.msgID].invoke(pack);
responseBus[pack.msgID].invoke(Outcome.Success(pack));
}
case RouteDiscover(_) | RouteDiscoverResponse(_) | RouteDiscoverUpdate(_):
// Delegate to Routing
@@ -91,6 +111,13 @@ class Net {
private function forwardPackage(pack: Package) {
if (pack.ttl == 0){
if (pack.type.match(Data(_))) {
// If the package is a data package and the ttl hits 0
// we send a "died" message to the sender
sendAndForget(pack.fromID, "icmp", {type:"died", msgID: pack.msgID});
}
// Drop package
return;
}
@@ -164,8 +191,16 @@ class Net {
var timeout:Timer = null;
responseBus[pack.msgID] = ((reponse:Package) -> {
resolve(reponse);
responseBus[pack.msgID] = ((reponse:Outcome<Package,Error>) -> {
switch reponse {
case Success(pack):
resolve(pack);
case Failure(err):
reject(err);
}
// Always remove the timeout
if (timeout != null) {
timeout.cancle();
}
@@ -198,4 +233,31 @@ class Net {
public function removeProto(proto:String) {
protoHandlers.remove(proto);
}
/**
Sends a ping package to the given id. Returns true if there was a response.
**/
public function ping(toID: NetworkID): Promise<Noise> {
return new Promise<Noise>((resolve,reject)->{
this.sendAndAwait(toID,"icmp",{type:"ping"}).handle(pack -> {
switch pack {
case Success(_):
resolve(Noise);
case Failure(err):
reject(err);
}
});
return null;
});
}
public function getActiveProtocols(): ReadOnlyArray<String> {
var arr = new Array<String>();
for (proto in protoHandlers.keys()) {
arr.push(proto);
}
return arr;
}
}

View File

@@ -1,15 +1,15 @@
package kernel.net;
import kernel.log.Log;
import kernel.peripherals.Peripherals.Peripheral;
import kernel.net.INetworkInterface;
import kernel.net.Package.NetworkID;
using tink.CoreApi;
import kernel.net.Package.NetworkID;
@:structInit typedef Route = {
interf:INetworkInterface,
rep: NetworkID,
rep:NetworkID,
cost:Int
}
@@ -22,13 +22,13 @@ class Routing {
**/
public static var instance:Routing;
public static inline final UPDATE_WAIT_TIME:Float = 3;
public static inline final UPDATE_WAIT_TIME:Float = 1;
public final onNewNeigbor:Signal<Int>;
private final onNewNeigborTrigger:SignalTrigger<NetworkID> = Signal.trigger();
private final routingTable:Map<NetworkID, Route> = new Map();
private var routeUpdateInQueue: Bool = false;
private var routeUpdateInQueue:Bool = false;
@:allow(kernel.Init)
private function new() {
@@ -37,61 +37,59 @@ class Routing {
@:allow(kernel.Init)
private function init() {
routingTable.set(Net.instance.networkID,{interf: Loopback.instance,cost:0,rep:Net.instance.networkID});
for (modem in Peripheral.instance.getModems()) {
modem.send(Net.BRODCAST_PORT, Net.instance.networkID, newRoutDiscoverPackage());
}
routingTable.set(Net.instance.networkID, {interf: Loopback.instance, cost: 0, rep: Net.instance.networkID});
brodcastRoutingTable();
}
/**
Prepares an brodcast of the current routing table. If a brodcast is already in queue it will be ignored.
After UPDATE_WAIT_TIME seconds the routing table will be brodcasted. This is done to prevent spamming the network.
**/
private function prepareRouteUpdate() {
if (this.routeUpdateInQueue){
if (this.routeUpdateInQueue) {
return;
}
this.routeUpdateInQueue = true;
new Timer(UPDATE_WAIT_TIME,()->{
brodcastUpdatedRoutes();
new Timer(UPDATE_WAIT_TIME, () -> {
brodcastRoutingTable();
this.routeUpdateInQueue = false;
});
}
private function brodcastUpdatedRoutes() {
var pack: Package = {
type: RouteDiscoverUpdate(genRouteList()),
toID: Net.BRODCAST_PORT,
msgID: null,
fromID: Net.instance.networkID,
data: null,
ttl: 0, // Prevent forwarding
};
/**
Brodcast the current routing table to all peers.
**/
private function brodcastRoutingTable() {
var pack = newRoutDiscoverPackage();
for (modem in Peripheral.instance.getModems()) {
modem.send(Net.BRODCAST_PORT, Net.instance.networkID,pack);
modem.send(Net.BRODCAST_PORT, Net.instance.networkID, pack);
}
}
/**
Handle incomming packages that involve route discovery.
**/
public function handleRoutePackage(pack:Package, interf:INetworkInterface):Void {
addPossibleRoute(pack.fromID,interf,0,pack.fromID);
@:allow(kernel.net.Net)
private function handleRoutePackage(pack:Package, interf:INetworkInterface):Void {
addPossibleRoute(pack.fromID, interf, 0, pack.fromID);
var shouldRespond: Bool = switch pack.type {
var shouldRespond:Bool = switch pack.type {
case RouteDiscoverResponse(routes):
for (route in routes) {
addPossibleRoute(route.id,interf,route.cost,pack.fromID);
addPossibleRoute(route.id, interf, route.cost, pack.fromID);
}
false;
case RouteDiscover(routes):
for (route in routes) {
addPossibleRoute(route.id,interf,route.cost,pack.fromID);
addPossibleRoute(route.id, interf, route.cost, pack.fromID);
}
true;
case RouteDiscoverUpdate(routes):
for (route in routes) {
addPossibleRoute(route.id,interf,route.cost,pack.fromID);
addPossibleRoute(route.id, interf, route.cost, pack.fromID);
}
false;
default:
@@ -99,7 +97,7 @@ class Routing {
false;
};
if (!shouldRespond){
if (!shouldRespond) {
return;
}
@@ -113,12 +111,15 @@ class Routing {
ttl: 0, // Prevent forwarding
}
interf.send(response.toID,Net.instance.networkID,response);
interf.send(response.toID, Net.instance.networkID, response);
}
private function genRouteList(): Array<{id:NetworkID,cost:Int}> {
var routes: Array<{id:NetworkID,cost:Int}> = [];
for (k => v in this.routingTable){
/**
Generate a list of routes to send to a peer based on the current routing table.
**/
private function genRouteList():Array<{id:NetworkID, cost:Int}> {
var routes:Array<{id:NetworkID, cost:Int}> = [];
for (k => v in this.routingTable) {
routes.push({
id: k,
cost: v.cost
@@ -144,36 +145,40 @@ class Routing {
Called when a route to a client has been disoverd.
Its possible to be called multiple times with the same id but different addr.
**/
private function addPossibleRoute(toID:Int, interf:INetworkInterface, cost:Int, rep: NetworkID) {
if (toID == Net.instance.networkID){
private function addPossibleRoute(toID:Int, interf:INetworkInterface, cost:Int, rep:NetworkID) {
if (toID == Net.instance.networkID) {
return;
}
var fullCost = cost+interf.getBaseRoutingCost();
var fullCost = cost + interf.getBaseRoutingCost();
if (this.routingTable.exists(toID)){
if (this.routingTable[toID].cost > fullCost){
this.routingTable[toID] = {interf:interf,cost:cost + interf.getBaseRoutingCost(),rep: rep};
if (this.routingTable.exists(toID)) {
if (this.routingTable[toID].cost > fullCost) {
this.routingTable[toID] = {interf: interf, cost: cost + interf.getBaseRoutingCost(), rep: rep};
this.prepareRouteUpdate();
}
}else{
this.routingTable[toID] = {interf:interf,cost:cost + interf.getBaseRoutingCost(),rep: rep};
} else {
this.routingTable[toID] = {interf: interf, cost: cost + interf.getBaseRoutingCost(), rep: rep};
this.onNewNeigborTrigger.trigger(toID);
this.prepareRouteUpdate();
}
}
public function getRouteToID(networkID:NetworkID):{interf: INetworkInterface, rep: NetworkID} {
public function getRouteToID(networkID:NetworkID):{interf:INetworkInterface, rep:NetworkID} {
if (networkID == Net.instance.networkID) {
return {interf: Loopback.instance,rep: Net.instance.networkID};
return {interf: Loopback.instance, rep: Net.instance.networkID};
}
var route = this.routingTable[networkID];
var route:Null<Route> = this.routingTable[networkID];
if (route == null){
if (route == null) {
return null;
}else{
return {interf: route.interf,rep: route.rep};
} else {
return {interf: route.interf, rep: route.rep};
}
}
public function getRouteTable():Map<NetworkID, Route> {
return this.routingTable;
}
}

View File

@@ -1,6 +1,5 @@
package kernel.peripherals;
import kernel.net.Package;
using tink.CoreApi;
class Drive implements IPeripheral {
@@ -52,8 +51,13 @@ class Drive implements IPeripheral {
this.native.setDiskLabel();
}
public inline function setDiskLabel(label: String) {
this.native.setDiskLabel(label);
public inline function setDiskLabel(label: String): Null<Error> {
try {
this.native.setDiskLabel(label);
return null;
} catch (e: Dynamic) {
return new Error("Invalid label");
}
}
public inline function hasData():Bool {

View File

@@ -1,10 +1,10 @@
package kernel.peripherals;
using tink.CoreApi;
import kernel.net.Package;
import kernel.net.INetworkInterface;
using tink.CoreApi;
class Modem implements INetworkInterface implements IPeripheral {
public final addr:String;
public var onMessage(default, null):Signal<Package>;

View File

@@ -21,6 +21,7 @@ class Peripheral {
private var modes: Array<Modem> = [];
private var drives:Array<Drive> = [];
private var redstone:Array<Redstone> = [];
private var printers:Array<Printer> = [];
@:allow(kernel.Init)
private function new() {
@@ -85,7 +86,24 @@ class Peripheral {
this.drives = allDrives.map(s -> return new Drive((cc.Peripheral.wrap(s) : Dynamic), s));
}
/**
Get redstone peripheral.
**/
public function getRedstone(side: String): Redstone {
return this.redstone.find(item -> item.getAddr() == side);
}
/**
Get all connected printers.
**/
public function getPrinters():ReadOnlyArray<Printer> {
return this.printers;
}
private function findPrinters() {
var allPrinters = cc.Peripheral.getNames().toArray().filter(s -> cc.Peripheral.getType(s) == "printer");
this.printers = allPrinters.map(s -> return new Printer((cc.Peripheral.wrap(s) : Dynamic), s));
}
}

View File

@@ -0,0 +1,56 @@
package kernel.peripherals;
import lib.Rect;
import lib.Pos;
class Printer implements IPeripheral {
private final native:cc.periphs.Printer.Printer;
private final addr:String;
public function new(native: cc.periphs.Printer.Printer, addr: String) {
this.native = native;
this.addr = addr;
}
public function getAddr():String {
return addr;
}
public function write(text: String){
}
public function getCurserPos(): Pos {
return new Pos({x: 0, y: 0});
}
public function setCurserPos(pos: Pos){
this.native.setCursorPos(pos.x, pos.y);
}
public function getPageSize(): Rect {
var pos = this.native.getPageSize();
return new Rect({x: 0, y: 0}, {x: pos.x, y: pos.y});
}
public function newPage(): Bool{
return this.native.newPage();
}
public function endPage(): Bool{
return this.native.endPage();
}
public function setPageTitle(title: String){
this.native.setPageTitle(title);
}
public function getInkLevel(): Float{
return this.native.getInkLevel();
}
public function getPaperLevel():Int {
return this.native.getPaperLevel();
}
}

View File

@@ -1,7 +1,7 @@
package kernel.peripherals;
import haxe.ds.ReadOnlyArray;
import util.Color;
import lib.Color;
using tink.CoreApi;

View File

@@ -1,11 +1,10 @@
package kernel.peripherals;
import util.Pos;
import lib.Pos;
import cc.Term.TerminalSize;
import kernel.ui.TermWriteable;
import util.Vec.Vec2;
import util.Color;
import lib.Vec.Vec2;
import lib.Color;
using tink.CoreApi;

View File

@@ -7,4 +7,24 @@ enum abstract Side(String) to String {
var Right = "right";
var Front = "front";
var Back = "back";
@:from
static public function fromString(s: String) {
switch (s) {
case "top":
return Top;
case "bottom":
return Bottom;
case "left":
return Left;
case "right":
return Right;
case "front":
return Front;
case "back":
return Back;
default:
return null;
}
}
}

View File

@@ -1,5 +1,6 @@
package kernel.turtle;
import kernel.log.Log;
import kernel.turtle.Types;
using tink.CoreApi;

View File

@@ -1,6 +1,6 @@
package kernel.ui;
import util.Color;
import lib.Color;
@:structInit class Pixel {
public var char:String;

View File

@@ -1,9 +1,8 @@
package kernel.ui;
import util.Pos;
import util.Vec.Vec2;
import util.Color;
import lib.Pos;
import lib.Vec.Vec2;
import lib.Color;
import kernel.ui.TermWriteable;
using tink.CoreApi;
@@ -89,6 +88,7 @@ class TermBuffer implements TermWriteable {
target.setCursorPos(cursorPos.x, cursorPos.y);
target.setTextColor(currentTextColor);
target.setBackgroundColor(currentBgColor);
target.setCursorBlink(cursorBlink);
}
private function safeWriteScreenBuffer(pos:Pos, char:String) {

View File

@@ -1,10 +1,10 @@
package kernel.ui;
import util.Pos;
using tink.CoreApi;
import lib.Pos;
import lib.Color;
import lib.Vec.Vec2;
import util.Color;
import util.Vec.Vec2;
using tink.CoreApi;
/**
Interface describing a terminal. E.g. the main computer screen or a external screen.

View File

@@ -1,12 +1,12 @@
package kernel.ui;
import util.Pos;
using tink.CoreApi;
import util.Vec.Vec2;
import util.Color;
import lib.Pos;
import lib.Vec.Vec2;
import lib.Color;
import kernel.ui.TermWriteable;
using tink.CoreApi;
/**
A term writer that can switch beetween its internal buffer and another termwriter.
The other target is most of the time a real screen

View File

@@ -1,19 +1,22 @@
package kernel.ui;
import util.Pos;
using tink.CoreApi;
import util.Color;
import lib.ui.rendere.UIEventDelegate;
import lib.Pos;
import lib.Color;
import kernel.ButtonType;
import util.Vec.Vec2;
import lib.Vec.Vec2;
import kernel.ui.TermWriteable;
using tink.CoreApi;
/**
The main object you interact with when writing anything to the screen.
**/
class WindowContext implements TermWriteable {
private final writer:VirtualTermWriter;
@:allow(kernel.ui.WindowManager) private var eventDelegate: Null<UIEventDelegate>;
public var onClick(default, null):Signal<{button:ButtonType, pos:Pos}>;
public var onKey(default, null):Signal<{keyCode:Int, isHeld:Bool}>;
public var onKeyUp(default, null):Signal<Int>;
@@ -21,6 +24,7 @@ class WindowContext implements TermWriteable {
public var onMouseScroll(default, null):Signal<{dir:Int, pos:Pos}>;
public var onMouseUp(default, null):Signal<{button:ButtonType, pos:Pos}>;
public var onPaste(default, null):Signal<String>;
public var onChar(default, null):Signal<String>;
@:allow(kernel.ui.WindowManager) private final onClickTrigger:SignalTrigger<{button:ButtonType, pos:Pos}>;
@:allow(kernel.ui.WindowManager) private final onKeyTrigger:SignalTrigger<{keyCode:Int, isHeld:Bool}>;
@@ -29,6 +33,7 @@ class WindowContext implements TermWriteable {
@:allow(kernel.ui.WindowManager) private final onMouseScrollTrigger:SignalTrigger<{dir:Int, pos:Pos}>;
@:allow(kernel.ui.WindowManager) private final onMouseUpTrigger:SignalTrigger<{button:ButtonType, pos:Pos}>;
@:allow(kernel.ui.WindowManager) private final onPasteTrigger:SignalTrigger<String>;
@:allow(kernel.ui.WindowManager) private final onCharTrigger:SignalTrigger<String>;
@:allow(kernel.ui.WindowManager)
private function new(writer:VirtualTermWriter) {
@@ -42,6 +47,7 @@ class WindowContext implements TermWriteable {
this.onMouseScrollTrigger = Signal.trigger();
this.onMouseUpTrigger = Signal.trigger();
this.onPasteTrigger = Signal.trigger();
this.onCharTrigger = Signal.trigger();
this.onClick = onClickTrigger.asSignal();
this.onKey = onKeyTrigger.asSignal();
@@ -50,6 +56,7 @@ class WindowContext implements TermWriteable {
this.onMouseScroll = onMouseScrollTrigger.asSignal();
this.onMouseUp = onMouseUpTrigger.asSignal();
this.onPaste = onPasteTrigger.asSignal();
this.onChar = onCharTrigger.asSignal();
}
public var onResize(default, null):Signal<Vec2<Int>>;
@@ -133,4 +140,12 @@ class WindowContext implements TermWriteable {
public function reset() {
writer.reset();
}
/**
Delegate events to an UIEventDelegate.
Set to null to stop delegating events.
**/
public function delegateEvents(delegate: Null<UIEventDelegate>){
this.eventDelegate = delegate;
}
}

View File

@@ -14,63 +14,106 @@ class WindowManager {
**/
public static var instance:WindowManager;
private var currentMainContext:WindowContext;
private final allContexts:Array<WindowContext> = new Array();
private final outputMap:Map<String, WindowContext> = new Map();
@:allow(kernel.Init)
private function new() {
KernelEvents.instance.onKey.handle(params -> {
if (currentMainContext != null) {
currentMainContext.onKeyTrigger.trigger(params);
if (currentMainContext.eventDelegate != null){
var foo = currentMainContext.eventDelegate.getEventHandlers();
foo.onKey != null ? foo.onKey.invoke(params) : null;
}else{
currentMainContext.onKeyTrigger.trigger(params);
}
}
});
KernelEvents.instance.onKeyUp.handle(keyCode -> {
if (currentMainContext != null) {
currentMainContext.onKeyUpTrigger.trigger(keyCode);
if (currentMainContext.eventDelegate != null){
var foo = currentMainContext.eventDelegate.getEventHandlers();
foo.onKeyUp != null ? foo.onKeyUp.invoke(keyCode) : null;
}else{
currentMainContext.onKeyUpTrigger.trigger(keyCode);
}
}
});
KernelEvents.instance.onMouseClick.handle(params -> {
if (currentMainContext != null) {
currentMainContext.onClickTrigger.trigger(params);
if (currentMainContext.eventDelegate != null){
var foo = currentMainContext.eventDelegate.getEventHandlers();
foo.onClick != null ? foo.onClick.invoke(params) : null;
}else{
currentMainContext.onClickTrigger.trigger(params);
}
}
});
KernelEvents.instance.onMouseDrag.handle(params -> {
if (currentMainContext != null) {
currentMainContext.onMouseDragTrigger.trigger(params);
if (currentMainContext.eventDelegate != null){
var foo = currentMainContext.eventDelegate.getEventHandlers();
foo.onMouseDrag != null ? foo.onMouseDrag.invoke(params) : null;
}else{
currentMainContext.onMouseDragTrigger.trigger(params);
}
}
});
KernelEvents.instance.onMouseScroll.handle(params -> {
if (currentMainContext != null) {
currentMainContext.onMouseScrollTrigger.trigger(params);
if (currentMainContext.eventDelegate != null){
var foo = currentMainContext.eventDelegate.getEventHandlers();
foo.onMouseScroll != null ? foo.onMouseScroll.invoke(params) : null;
}else{
currentMainContext.onMouseScrollTrigger.trigger(params);
}
}
});
KernelEvents.instance.onMouseUp.handle(params -> {
if (currentMainContext != null) {
currentMainContext.onMouseUpTrigger.trigger(params);
if (currentMainContext.eventDelegate != null){
var foo = currentMainContext.eventDelegate.getEventHandlers();
foo.onMouseUp != null ? foo.onMouseUp.invoke(params) : null;
}else{
currentMainContext.onMouseUpTrigger.trigger(params);
}
}
});
KernelEvents.instance.onPaste.handle(text -> {
if (currentMainContext != null) {
currentMainContext.onPasteTrigger.trigger(text);
if (currentMainContext.eventDelegate != null){
var foo = currentMainContext.eventDelegate.getEventHandlers();
foo.onPaste != null ? foo.onPaste.invoke(text) : null;
}else{
currentMainContext.onPasteTrigger.trigger(text);
}
}
});
KernelEvents.instance.onMonitorTouch.handle(params -> {
// TODO
});
KernelEvents.instance.onChar.handle(char -> {
if (currentMainContext != null) {
if (currentMainContext.eventDelegate != null){
var foo = currentMainContext.eventDelegate.getEventHandlers();
foo.onChar != null ? foo.onChar.invoke(char) : null;
}else{
currentMainContext.onCharTrigger.trigger(char);
}
}
});
}
public function createNewContext():WindowContext {
var newContext = new WindowContext(new VirtualTermWriter());
allContexts.push(newContext);
return newContext;
}
@@ -80,6 +123,9 @@ class WindowManager {
return arr;
}
/**
Move context to output. If output is "main", context will be moved to main screen.
**/
public function focusContextToOutput(context:WindowContext, output:String) {
var target:TermWriteable;
if (output == "main") {

View File

@@ -1,4 +1,4 @@
package util;
package lib;
/**
Macros with static information.

View File

@@ -1,4 +1,4 @@
package util;
package lib;
import kernel.peripherals.Redstone.BundleMask;

View File

@@ -1,9 +1,9 @@
package util;
package lib;
import kernel.log.Log;
import lua.NativeStringTools;
import lib.ui.Canvas;
import cc.ComputerCraft;
import kernel.Log;
#if webconsole
import cc.HTTP;
import kernel.net.Net;

View File

@@ -1,4 +1,4 @@
package util;
package lib;
import haxe.Exception;

109
src/lib/HomeContext.hx Normal file
View File

@@ -0,0 +1,109 @@
package lib;
import lib.ui.elements.UIElement;
import lib.ui.elements.TextElement;
import lib.ui.elements.RootElement;
import kernel.KernelEvents;
import bin.Terminal;
import kernel.ui.WindowManager;
import kernel.ui.WindowContext;
/**
The WindowContext that spawns all other contexts. This is the main terminal.
Listens to global hotkey "POS1"/"HOME" to switch back to this context.
**/
class HomeContext {
private static inline final MAX_CONTEXT:Int = 10;
private var ctx:WindowContext = null;
private final workspaces:Map<Int, WindowContext> = [];
private var currentWorkspace:Int = -1;
private var renderer:RootElement;
public function new() {}
public function run() {
// Create main terminal context
ctx = WindowManager.instance.createNewContext();
WindowManager.instance.focusContextToOutput(ctx, "main");
renderer = new RootElement();
ctx.delegateEvents(renderer);
render();
// Register global key bindings to react to main terminal
KernelEvents.instance.onKey.handle(e -> {
// Is HOME pressed?
// TODO: remove magic number
if (e.keyCode == 268) {
focusMainTerm();
}
});
}
/**
Adds a context to the list of contexts to manage. Returns the context ID. Returns -1 if the context could not be added.
**/
private function addContextNextWorkspace(context:WindowContext):Int {
for (i in 0...MAX_CONTEXT) {
if (!workspaces.exists(i)) {
workspaces[i] = context;
return i;
}
}
return -1;
}
private function addContextToWorkspace(context:WindowContext, id:Int) {
if (!workspaces.exists(id)) {
workspaces[id] = context;
}
}
private function focusContext(id:Int) {
if (workspaces.exists(id)) {
WindowManager.instance.focusContextToOutput(workspaces[id], "main");
currentWorkspace = id;
}
}
private function focusMainTerm() {
render();
WindowManager.instance.focusContextToOutput(ctx, "main");
currentWorkspace = -1;
}
private function handleSelectContext(contextId:Int) {
if (workspaces[contextId] != null) {
focusContext(contextId);
} else {
var newContext = WindowManager.instance.createNewContext();
// Create new terminal
var term = new Terminal();
term.invoke(newContext);
addContextToWorkspace(newContext, contextId);
focusContext(contextId);
}
}
private function render() {
ctx.clear();
var list = [
for (i in 0...MAX_CONTEXT) workspaces.exists(i) ? 'Switch to context ${i}' : 'Create new Terminal on context ${i}'
];
var children:Array<UIElement> = [
for (i => line in list) new TextElement(line, {onClick: this.handleSelectContext.bind(i)})
];
renderer.setChildren(children);
renderer.render().renderToContext(ctx);
}
}

View File

@@ -1,4 +1,4 @@
package util;
package lib;
class MathI {
public static function max(a:Int, b:Int):Int {

24
src/lib/ObjMerge.hx Normal file
View File

@@ -0,0 +1,24 @@
package lib;
class ObjMerge {
public static function merge<T>(obj1:T, obj2:T): T {
if (obj1 == null) {
return obj2;
}
if (obj2 == null) {
return obj1;
}
var rtn:T = Reflect.copy(obj1);
var fields = Reflect.fields(obj2);
for (field in fields) {
if (Reflect.getProperty(obj1, field) == null) {
Reflect.setProperty(rtn, field, Reflect.getProperty(obj2, field));
}
}
return (rtn:T);
}
}

View File

@@ -1,6 +1,6 @@
package util;
package lib;
import util.Vec.Vec2;
import lib.Vec.Vec2;
/**
Reporesents a Point in a 2D `Int` System.

View File

@@ -1,6 +1,6 @@
package util;
package lib;
import util.Vec.Vec3;
import lib.Vec.Vec3;
/**
Reporesents a Point in a 3D `Int` System.

46
src/lib/Rect.hx Normal file
View File

@@ -0,0 +1,46 @@
package lib;
class Rect {
private final tl:Pos;
private final br:Pos;
public function new(p1: Pos,p2:Pos) {
this.tl = {
x: MathI.min(p1.x,p2.x),
y: MathI.min(p1.y,p2.y)
};
this.br = {
x: MathI.max(p1.x,p2.x),
y: MathI.max(p1.y,p2.y)
};
}
public function getSize(): Int {
return getWidth() * getHight();
}
public function isInside(p: Pos): Bool {
return (p.x >= tl.x && p.x <= br.x) && (p.y >= tl.y && p.y <= br.y);
}
public function isOutside(p: Pos): Bool {
return !this.isInside(p);
}
public function getHight(): Int {
return br.y - tl.y;
}
public function getWidth(): Int {
return br.x - tl.x;
}
public function offset(pos: Pos) {
tl.x += pos.x;
tl.y += pos.y;
br.x += pos.x;
br.y += pos.y;
}
}

View File

@@ -1,6 +1,6 @@
package lib;
import util.Color;
import lib.Color;
import kernel.ui.TermWriteable;
/**

12
src/lib/Vec.hx Normal file
View File

@@ -0,0 +1,12 @@
package lib;
@:structInit class Vec2<T> {
public var x:T;
public var y:T;
}
@:structInit class Vec3<T> {
public var x:T;
public var y:T;
public var z:T;
}

7
src/lib/cli/CLIApp.hx Normal file
View File

@@ -0,0 +1,7 @@
package lib.cli;
using tink.CoreApi;
abstract class CLIApp {
public abstract function invoke(handle: TermHandle): Future<Bool>;
}

29
src/lib/cli/TermHandle.hx Normal file
View File

@@ -0,0 +1,29 @@
package lib.cli;
import haxe.ds.ReadOnlyArray;
typedef TermHandleEvents = {
onWrite: String->Void,
onNewLine: Void->Void,
}
class TermHandle {
public final args:ReadOnlyArray<String>;
private final events:TermHandleEvents;
@:allow(bin.Terminal)
private function new(args: Array<String>, events: TermHandleEvents) {
this.args = args;
this.events = events;
}
public function write(s:String) {
this.events.onWrite(s);
}
public function writeLn(s:String = "") {
if (s != "")
this.events.onWrite(s);
this.events.onNewLine();
}
}

View File

@@ -0,0 +1,27 @@
package lib.observable;
using tink.CoreApi;
class DummyObservable<T> implements Observable<T> {
private var value:T;
private function new(value:T) {
this.value = value;
}
public function set(value:T) {
throw new haxe.exceptions.NotImplementedException();
}
public function get():T {
return this.value;
}
public function subscribe(callback:Callback<T>):CallbackLink {
return null;
}
public static function dummy<T>(value: T): Observable<T> {
return new DummyObservable<T>(value);
}
}

View File

@@ -0,0 +1,9 @@
package lib.observable;
using tink.CoreApi;
interface Observable<T> {
public function set(value:T): Void;
public function get():T;
public function subscribe(callback:Callback<T>):CallbackLink;
}

View File

@@ -1,6 +1,6 @@
package util;
package lib.observable;
class ObservableArray<T> extends Observable<Array<T>> {
class ObservableArray<T> extends ObservableValue<Array<T>> {
public function new(value: Array<T>) {
super(value);
}

View File

@@ -1,8 +1,8 @@
package util;
package lib.observable;
using tink.CoreApi;
class Observable<T> {
class ObservableValue<T> implements Observable<T> {
private var value:T;
private var callbacks:CallbackList<T> = new CallbackList();

View File

@@ -1,8 +1,9 @@
package lib.turtle;
import util.Pos;
import util.Pos3;
import lib.Pos;
import lib.Pos3;
import kernel.turtle.Turtle;
using tink.CoreApi;
class TurtleExecuter {

View File

@@ -10,7 +10,6 @@ import lua.NativeStringTools;
using tink.CoreApi;
/**
Save a set of turtle instructions to a string and execute them.
**/

View File

@@ -1,7 +1,8 @@
package lib.ui;
import util.Pos;
import util.Rect;
import kernel.ui.WindowContext;
import lib.Pos;
import lib.Rect;
import kernel.ui.Pixel;
abstract Canvas(Array<Array<Pixel>>) to Array<Array<Pixel>> from Array<Array<Pixel>>{
@@ -63,10 +64,65 @@ abstract Canvas(Array<Array<Pixel>>) to Array<Array<Pixel>> from Array<Array<Pix
public function getBounds(): Rect {
return new Rect({x:0,y:0},{
x: maxWidth(),
y: height()
x: maxWidth() - 1,
y: height() - 1
});
}
/**
Renders the canvas directly to the context
**/
public function renderToContext(ctx: WindowContext){
var lastBgColor: Color = null;
var lastTextColor: Color = null;
for (lineIndex => line in this) {
if (line == null || line.length == 0) {
// Line is empty
continue;
}
ctx.setCursorPos(0, lineIndex);
var pritableLine = "";
for (pixelIndex => pixel in line) {
if (pixel == null) {
// If the pixel is null we need to skip it with setCurosrPos.
// Otherwise we will print an empty space with bg color
ctx.write(pritableLine);
pritableLine = "";
ctx.setCursorPos(pixelIndex + 1, lineIndex);
continue;
}
if (pixel.bg != lastBgColor) {
// The background color has changed, we need to print the current line and set the new background color
ctx.write(pritableLine);
pritableLine = "";
// Set the new background color
ctx.setBackgroundColor(pixel.bg);
lastBgColor = pixel.bg;
}
// Same as above but for the text color
if (pixel.textColor != lastTextColor) {
// The text color has changed, we need to print the current line and set the new text color
ctx.write(pritableLine);
pritableLine = "";
// Set the new text color
ctx.setTextColor(pixel.textColor);
lastTextColor = pixel.textColor;
}
pritableLine += pixel.char;
}
ctx.write(pritableLine);
}
}
}
class CanvasKeyValueIterator {

8
src/lib/ui/Dimensions.hx Normal file
View File

@@ -0,0 +1,8 @@
package lib.ui;
typedef Dimensions = {
public var ?top:Int;
public var ?bottom:Int;
public var ?left:Int;
public var ?right:Int;
}

9
src/lib/ui/UIApp.hx Normal file
View File

@@ -0,0 +1,9 @@
package lib.ui;
import kernel.ui.WindowContext;
using tink.CoreApi;
abstract class UIApp {
public abstract function invoke(context: WindowContext): Future<Bool>;
}

View File

@@ -1,8 +1,8 @@
package lib.ui;
import util.Pos;
import util.Vec.Vec2;
import lib.Pos;
import kernel.ButtonType;
using tink.CoreApi;
typedef UIEvents = {
@@ -13,4 +13,5 @@ typedef UIEvents = {
public var ?onMouseScroll:Callback<{dir:Int, pos:Pos}>;
public var ?onMouseUp:Callback<{button:ButtonType, pos:Pos}>;
public var ?onPaste:Callback<String>;
public var ?onChar:Callback<String>;
}

View File

@@ -0,0 +1,26 @@
package lib.ui.elements;
import lib.ui.rendere.UIEventDelegate;
abstract EventMap(List<{bound:Rect, delegate:UIEventDelegate}>) {
inline public function new(?i:List<{bound:Rect, delegate:UIEventDelegate}>) {
if (i == null) {
this = new List<{bound:Rect, delegate:UIEventDelegate}>();
} else {
this = i;
}
}
public function findResponseableDelegate(pos:Pos):UIEventDelegate {
for (i in this) {
if (i.bound.isInside(pos)) {
return i.delegate;
}
}
return null;
}
public function addElement(element:UIEventDelegate, bound:Rect) {
this.add({bound: bound, delegate: element});
}
}

View File

@@ -0,0 +1,42 @@
package lib.ui.elements;
class RootElement implements UIElement {
private var children:Array<UIElement>;
private final eventManager:UIEventManager = new UIEventManager();
public function new(?children:Array<UIElement>) {
if (children == null) {
children = [];
} else {
this.children = children;
}
}
public function getEventHandlers():UIEvents {
return eventManager.getEventHandlers();
}
public function setChildren(children:Array<UIElement>) {
this.children = children;
}
public function render():Canvas {
var canvas = new Canvas();
var offset = new Pos({x: 0, y: 0});
this.eventManager.clearMap();
for (child in children) {
var childCanvas = child.render();
var bounds = childCanvas.getBounds();
bounds.offset(offset);
this.eventManager.addMapElement(child, bounds);
canvas.combine(childCanvas, offset);
offset = new Pos({x: 0, y: offset.y + bounds.getHight() + 1});
}
return canvas;
}
}

View File

@@ -0,0 +1,36 @@
package lib.ui.elements;
import lib.ui.elements.UIElement;
class TextElement implements UIElement {
public var text:String;
private final uiEvents:UIEvents;
public function new(text:String, ?uiEvents:UIEvents) {
this.text = text;
this.uiEvents = uiEvents;
}
public function set(text:String) {
this.text = text;
}
public function get():String {
return this.text;
}
public function getEventHandlers():UIEvents {
return uiEvents;
}
public function render():Canvas {
var canvas = new Canvas();
for (i in 0...this.text.length) {
var c = this.text.charAt(i);
canvas.set({x: i, y: 0}, {bg: Black, textColor: White, char: c});
}
return canvas;
}
}

View File

@@ -0,0 +1,7 @@
package lib.ui.elements;
import lib.ui.rendere.UIEventDelegate;
interface UIElement extends UIEventDelegate {
public function render():Canvas;
}

View File

@@ -0,0 +1,60 @@
package lib.ui.elements;
import kernel.log.Log;
import kernel.ButtonType;
import lib.ui.rendere.UIEventDelegate;
class UIEventManager implements UIEventDelegate {
private var map:EventMap = new EventMap();
public function new() {}
public function clearMap() {
this.map = new EventMap();
}
public function addMapElement(element:UIEventDelegate, bound:Rect) {
this.map.addElement(element, bound);
}
public function getEventHandlers():UIEvents {
return {
onClick: handleClickEvent,
onKey: handleKeyEvent,
onKeyUp: handleKeyUpEvent,
onMouseDrag: handleMouseDragEvent,
onMouseScroll: handleMouseScrollEvent,
onMouseUp: handleMouseUpEvent,
onPaste: handlePasteEvent,
onChar: handleCharEvent,
};
}
private function handleClickEvent(params:{button:ButtonType, pos:Pos}) {
var element = this.map.findResponseableDelegate(params.pos);
if (element == null) {
return;
}
var handlers = element.getEventHandlers();
if (handlers == null || handlers.onClick == null) {
return;
}
handlers.onClick.invoke(params);
}
private function handleCharEvent(char:String) {}
private function handlePasteEvent(text:String) {}
private function handleMouseUpEvent(params:{button:ButtonType, pos:Pos}) {}
private function handleMouseScrollEvent(params:{dir:Int, pos:Pos}) {}
private function handleMouseDragEvent(params:{button:ButtonType, pos:Pos}) {}
private function handleKeyEvent(params:{keyCode:Int, isHeld:Bool}) {}
private function handleKeyUpEvent(key:Int) {}
}

View File

@@ -1,9 +1,9 @@
package lib.ui.reactive;
import util.Pos;
import lib.Pos;
import util.Rect;
import util.ObservableArray;
import util.Vec.Vec2;
import lib.Vec.Vec2;
class ListElement extends UIElement {

View File

@@ -1,40 +1,77 @@
package lib.ui.reactive;
import util.Pos;
import lib.ui.Dimensions;
import util.ObjMerge;
import lib.Pos;
import util.Observable;
import lib.Color;
import lib.Vec.Vec2;
using tink.CoreApi;
import util.Color;
import util.Vec.Vec2;
import util.MathI;
typedef TextElementArgs = {
public var ?text:Observable<String>;
public var ?simpleText: String;
public var ?bg:Color;
public var ?fg:Color;
public var ?padding: Dimensions;
public var ?margin: Dimensions;
}
class TextElement extends UIElement {
private final text:Observable<String>;
private final bg:Color;
private final fg:Color;
private var args:TextElementArgs;
public function new(text:Observable<String>, ?background:Color = Black, ?textColor:Color = White, events: UIEvents = null) {
public function new(args: TextElementArgs,events: UIEvents = null) {
super(events);
this.args = args;
this.text = text;
this.bg = background;
this.fg = textColor;
if (args.text != null) {
args.text.subscribe(value -> {
this.changedTrigger.trigger(null);
});
}else if (args.simpleText == null) {
throw new Error("text or simpleText must be set");
}
}
this.text.subscribe(value -> {
this.changedTrigger.trigger(null);
});
private function getText() {
if (args.text != null) {
return args.text.get();
} else {
return args.simpleText;
}
}
public function render(bounds:Vec2<Int>,offset: Pos):Canvas {
var rtn = new Canvas();
for (i in 0...MathI.min(Math.floor(text.get().length / bounds.x) + 1, bounds.y)) {
var line = (text.get().substr(i * bounds.x, bounds.x));
for (char in 0...line.length) {
rtn.set({x: char, y: i}, {textColor: fg, char: line.charAt(char), bg: bg});
var fullText = getText();
var writePoint: Pos = {x: 0,y: 0};
for (pos in 0...fullText.length) {
var char = fullText.charAt(pos);
if (char == "\n"){
writePoint = {x: 0, y: writePoint.y + 1};
continue;
}
if (writePoint.y >= bounds.y) {
break;
}
if (writePoint.x >= bounds.x) {
writePoint = {x: 0, y: writePoint.y + 1};
}
rtn.set(writePoint, {char: char,textColor: this.args.fg, bg: this.args.bg});
writePoint = {x: writePoint.x + 1, y: writePoint.y};
}
return rtn;
return UIElement.applyPaddignAndMargin(rtn, this.args.padding, this.args.margin);
}
}

View File

@@ -0,0 +1,60 @@
package lib.ui.reactive;
import lib.Debug;
import lib.Vec.Vec2;
import lib.Pos;
class TurtleController extends UIElement {
public function new() {
super();
}
private function addButton(label:String): Canvas {
var txt = new TextElement({simpleText: '$label', fg: Red,bg: Orange});
return txt.render({x:3,y:3},{x:0,y:0});
}
public function render(bounds:Vec2<Int>, offset:Pos):Canvas {
var canvas: Canvas = new Canvas();
canvas.combine(addButton("F"),{x:1,y:1});
canvas.combine(addButton("F"),{x:5,y:2});
return canvas;
}
}
// var innerText = switch (sqr) {
// case 0: "D";
// case 1: "F";
// case 2: "U";
// case 3: "L";
// case 4: "B";
// case 5: "R";
// case 6: "D";
// case 7: "D";
// case 8: "D";
// default: "?";
// };
// canvas.set({x: offsetX + 0, y: offsetY + 0}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 1, y: offsetY + 0}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 2, y: offsetY + 0}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 3, y: offsetY + 0}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 0, y: offsetY + 1}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 1, y: offsetY + 1}, {char: innerText,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 2, y: offsetY + 1}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 3, y: offsetY + 1}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 0, y: offsetY + 2}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 1, y: offsetY + 2}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 2, y: offsetY + 2}, {char: borderChar,bg: bgColor,textColor: textColor});
// canvas.set({x: offsetX + 3, y: offsetY + 2}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 0, y: offsetY + 3}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 1, y: offsetY + 3}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 2, y: offsetY + 3}, {char: " ",bg: spaceColor,textColor: textColor});
// canvas.set({x: offsetX + 3, y: offsetY + 3}, {char: " ",bg: spaceColor,textColor: textColor});

View File

@@ -1,8 +1,9 @@
package lib.ui.reactive;
import util.Pos;
import util.Rect;
import util.Vec.Vec2;
import util.ObjMerge;
import lib.Pos;
import lib.Rect;
import lib.Vec.Vec2;
using tink.CoreApi;
@@ -33,4 +34,14 @@ abstract class UIElement {
return null;
};
public static function applyPaddignAndMargin(canvas:Canvas,padding: Dimensions, margin: Dimensions):Canvas{
var passing = ObjMerge.merge(padding, {top: 0, left: 0, bottom: 0, right: 0});
var margin = ObjMerge.merge(margin, {top: 0, left: 0, bottom: 0, right: 0});
var rtn = new Canvas();
rtn.combine(canvas,{x:margin.left + padding.left,y: margin.top + padding.top});
return rtn;
}
}

View File

@@ -0,0 +1,38 @@
package lib.ui.rendere;
class List implements UIEventDelegate{
private final onElementClick: Null<Int->Void>;
public function new(?onElementClick: Int->Void) {
this.onElementClick = onElementClick;
}
public function render(list:Array<String>): Canvas {
var canvas = new Canvas();
for (line in 0...list.length) {
for (char in 0...list[line].length) {
var c = list[line].charAt(char);
canvas.set({x: char, y: line}, {
char: c,
textColor: Color.White,
bg: Color.Black,
});
}
}
return canvas;
}
public function getEventHandlers(): UIEvents{
return {
onClick: handleClick
};
}
private function handleClick(e: {button:kernel.ButtonType, pos:Pos}): Void{
if (this.onElementClick == null){
return;
}
this.onElementClick(e.pos.y);
}
}

View File

@@ -0,0 +1,5 @@
package lib.ui.rendere;
interface UIEventDelegate {
public function getEventHandlers(): UIEvents;
}

View File

@@ -1,33 +0,0 @@
package util;
import util.Vec.Vec2;
class Rect {
private final tr:Pos;
private final bl:Pos;
public function new(p1: Pos,p2:Pos) {
this.tr = {
x: MathI.max(p1.x,p2.x),
y: MathI.max(p1.y,p2.y)
};
this.bl = {
x: MathI.min(p1.x,p2.x),
y: MathI.min(p1.y,p2.y)
};
}
public function getSize(): Int {
return (tr.x - bl.x) * (tr.y - bl.y);
}
public function isInside(p: Pos): Bool {
return (p.x >= bl.x && p.x <= tr.x) && (p.y >= bl.y && p.y <= tr.y);
}
public function isOutside(p: Pos): Bool {
return !this.isInside(p);
}
}

View File

@@ -1,12 +0,0 @@
package util;
@:structInit class Vec2<T> {
public final x:T;
public final y:T;
}
@:structInit class Vec3<T> {
public final x:T;
public final y:T;
public final z:T;
}