commit b3dc25152c5bcb8a7b5009c689b077a1e6f950c3
parent 93c23879dc64d8291320b28235d3a5e45b773173
Author: thing1 <thing1@seacrossedlovers.xyz>
Date: Thu, 26 Feb 2026 18:37:39 +0000
added ed!
Diffstat:
8 files changed, 343 insertions(+), 2 deletions(-)
diff --git a/:wq b/:wq
@@ -0,0 +1,26 @@
+export type option = enum {
+ CANT,
+ WILL,
+ MUST,
+};
+
+export type cmdfn = fn(file: *lines, current: size, args: []str) void;
+
+export type cmds = struct {
+ // the char to call the command
+ cmd: rune,
+ // help text for the command
+ msg: str,
+
+ // does the command allow a range
+ range: option,
+
+ // does the command allow a repeater
+ repeat: option,
+
+ // the number of arguments a cmd can take
+ args: size,
+
+ // function
+ f: *cmdfn,
+};
diff --git a/Makefile b/Makefile
@@ -7,13 +7,21 @@ DESTDIR=
PREFIX=/usr/local
BINDIR=$(PREFIX)/bin
-all: bin/ls bin/rainbow bin/cat bin/uniq bin/split bin/wc bin/yes
+all: bin/ls bin/rainbow bin/cat bin/uniq bin/split bin/wc bin/yes bin/mc bin/ed
clean:
rm -rf bin/*
+check:
+ hare test cmd/*.ha
+ hare test cmd/ed/
+
.PHONY: all check clean install uninstall
+EDSRC != find cmd/ed/ -name *.ha
+bin/ed: $(EDSRC)
+ $(HARE) build $(HAREFLAGS) -o $@ cmd/ed/
+
bin/ls: cmd/ls.ha
$(HARE) build $(HAREFLAGS) -o $@ cmd/ls.ha
@@ -35,3 +43,6 @@ bin/wc: cmd/wc.ha
bin/yes: cmd/yes.ha
$(HARE) build $(HAREFLAGS) -o $@ cmd/yes.ha
+bin/mc: cmd/mc.ha
+ $(HARE) build $(HAREFLAGS) -o $@ cmd/mc.ha
+
diff --git a/TODO.md b/TODO.md
@@ -1,3 +1,4 @@
+- mc (finish)
- fmt
- build system
- shell
diff --git a/cmd/ed/cmds.ha b/cmd/ed/cmds.ha
@@ -0,0 +1,26 @@
+export type option = enum {
+ CANT,
+ WILL,
+ MUST,
+};
+
+export type cmdfn = fn(file: *lines, current: size, args: []str) void;
+
+export type cmds = struct {
+ // the char to call the command
+ cmd: rune,
+ // help text for the command
+ msg: str,
+
+ // does the command allow a range
+ range: option,
+
+ // does the command allow a repeater
+ repeat: option,
+
+ // the number of arguments a cmd can take
+ args: size,
+
+ // function
+ f: *cmdfn,
+};
diff --git a/cmd/ed/file.ha b/cmd/ed/file.ha
@@ -0,0 +1,171 @@
+use fmt;
+
+use util;
+
+export type lines = struct {
+ next: nullable *lines,
+ prev: nullable *lines,
+ s: str
+};
+
+// returns a new allocated line, the caller should free this value
+export fn newline(s: str) *lines = alloc(lines{next = null, prev = null, s = s})!;
+@test fn newline() void = assert(newline("hello world").s == "hello world");
+
+// returns the last line of [[lines]]
+export fn getend(ls: *lines) *lines = {
+ let current = ls;
+ for (let i = 0; true; i += 1)
+ current = match (current.next) {
+ case let ls: *lines => yield ls;
+ case null => return current;
+ };
+};
+@test fn getend() void = {
+ let line = newline("hello world");
+ assert(getend(line) == line);
+};
+@test fn getend() void = {
+ let line = newline("hello world");
+ line.next = newline("bye world");
+ assert(getend(line) == line.next);
+};
+
+// inserts [[l]] at [[where]] into [[ls]], and returns a new value for [[ls]],
+// the caller should override [[ls]], with the return value
+export fn insertline(ls: *lines, l: *lines, where: size) *lines = {
+ if (where == 0) {
+ l.next = ls; //todo, make this work for multiple lines in l
+ ls.prev = l;
+ return l;
+ };
+
+ let current = getline(ls, where - 1);
+
+ let tmp = match (current.next) {
+ case let ls: *lines => yield ls;
+ case null =>
+ current.next = l;
+ l.prev = current;
+ return ls;
+ };
+ let end = getend(l);
+
+ current.next = l;
+ l.prev = current;
+ end.next = tmp;
+ tmp.prev = end;
+
+ return ls;
+};
+@test fn insertline() void = {
+ let ls = newline("file");
+ let line = newline("hello world");
+ ls = insertline(ls, line, 1);
+ assert((ls.next as *lines).s == "hello world");
+ assert(ls.s == "file");
+};
+@test fn insertline() void = {
+ let ls = newline("file");
+ let line = newline("hello world");
+ ls = insertline(ls, line, 0);
+ assert(ls.s == "hello world");
+ assert((ls.next as *lines).s == "file");
+};
+
+// gets the [[where]]'th line from [[ls]]
+export fn getline(ls: *lines, where: size) *lines = {
+ let current = ls;
+ for (let i = 0z; i < where; i += 1)
+ current = match (current.next) {
+ case let ls: *lines => yield ls;
+ case null => abort();
+ };
+
+ return current;
+};
+@test fn getline() void = {
+ let ls = newline("file");
+ let line = newline("hello world");
+ ls = insertline(ls, line, 1);
+ assert(getline(ls, 0).s == "file");
+};
+@test fn getline() void = {
+ let ls = newline("file");
+ let line = newline("hello world");
+ ls = insertline(ls, line, 1);
+ assert(getline(ls, 1).s == "hello world");
+};
+@test fn getline() void = {
+ let ls = newline("file");
+ let line = newline("hello world");
+ ls = insertline(ls, line, 0);
+ assert(getline(ls, 0).s == "hello world");
+};
+@test fn getline() void = {
+ let ls = newline("file");
+ let line = newline("hello world");
+ ls = insertline(ls, line, 0);
+ assert(getline(ls, 1).s == "file");
+};
+
+// counts how many lines are in [[ls]]
+export fn countlines(ls: *lines) size = {
+ let current = ls;
+ for (let i = 0z; true; i += 1)
+ current = match (current.next) {
+ case let ls: *lines => yield ls;
+ case null => return i + 1;
+ };
+};
+@test fn countlines() void = {
+ let ls = newline("file");
+ let line = newline("hello world");
+ ls = insertline(ls, line, 0);
+ assert(countlines(ls) == 2);
+};
+
+// deletes line at [[where]] from [[ls]], the caller should override [[ls]] with
+// the return value
+export fn deleteline(ls: *lines, where: size) *lines = {
+ if (where == 0) return ls.next as *lines;
+
+ let current = getline(ls, where);
+
+ (current.prev as *lines).next = current.next;
+ if (where != countlines(ls))
+ (current.next as *lines).prev = current.prev;
+
+ free(current);
+ return ls;
+};
+@test fn deleteline() void = {
+ let ls = newline("file");
+ let line = newline("hello world");
+ let line2 = newline("fuck");
+ ls = insertline(ls, line, 1);
+ ls = insertline(ls, line2, 2);
+ ls = deleteline(ls, 0);
+ assert(ls.s == "hello world");
+ assert((ls.next as *lines).s == "fuck");
+};
+@test fn deleteline() void = {
+ let ls = newline("file");
+ let line = newline("hello world");
+ let line2 = newline("fuck");
+ ls = insertline(ls, line, 1);
+ ls = insertline(ls, line2, 2);
+ ls = deleteline(ls, 1);
+ assert(ls.s == "file");
+ assert((ls.next as *lines).s == "fuck");
+};
+@test fn deleteline() void = {
+ let ls = newline("file");
+ let line = newline("hello world");
+ let line2 = newline("fuck");
+ ls = insertline(ls, line, 1);
+ ls = insertline(ls, line2, 2);
+ ls = deleteline(ls, 2);
+ assert(ls.s == "file");
+ assert((ls.next as *lines).s == "hello world");
+};
diff --git a/cmd/ed/main.ha b/cmd/ed/main.ha
@@ -0,0 +1,44 @@
+use fmt;
+use io;
+use bufio;
+use strings;
+use fs;
+use os;
+
+use util;
+
+fn loadfile(h: io::handle) nullable *lines = {
+ let ls: nullable *lines = null;
+ let sc = bufio::newscanner(h);
+ defer bufio::finish(&sc);
+
+ for (true) {
+ match (bufio::scan_line(&sc)) {
+ case let s: str =>
+ match (ls) {
+ case let ls: *lines =>
+ ls.next = newline(strings::dup(s)!);
+ (ls.next as *lines).prev = ls;
+ case null => ls = newline(strings::dup(s)!);
+ };
+ case io::EOF => break;
+ case => abort();
+ };
+ };
+
+ return ls;
+};
+
+fn openfile(name: str) nullable *lines = {
+ let h = match(os::open(name)) {
+ case let h: io::file => yield h;
+ case let e: fs::error => util::die(fs::strerror(e));
+ };
+ defer io::close(h)!;
+
+ return loadfile(h);
+};
+
+export fn main() void = {
+ abort();
+};
diff --git a/cmd/mc.ha b/cmd/mc.ha
@@ -0,0 +1,62 @@
+use fmt;
+use os;
+use io;
+use bufio;
+use unix::tty;
+use strings;
+use strconv;
+use getopt;
+
+use util;
+
+let target = 8z;
+
+export fn main() void = {
+ const cmd = getopt::parse(os::args,
+ "Multi column output",
+ ('c', "count", "Target count (default 8)")
+ );
+ for (let opt .. cmd.opts) {
+ switch (opt.0) {
+ case 'c' =>
+ target = match (strconv::stou(opt.1)) {
+ case let i: uint => yield i: size;
+ case strconv::invalid => util::die("value must be an integer");
+ case strconv::overflow => util::die("value too large");
+ };
+ case => abort();
+ };
+ };
+
+ let tty = tty::open()!;
+ let sz = tty::winsize(tty)!;
+ let sc = bufio::newscanner(os::stdin);
+ defer io::close(&sc)!;
+
+ let lines: []str = [];
+ defer free(lines);
+
+ let longest = 0z;
+
+ for (true) {
+ match (bufio::scan_line(&sc)) {
+ case let s: str =>
+ append(lines, strings::dup(s)!)!;
+ if (len(s) > longest) longest = len(s);
+ case io::EOF => break;
+ case => abort(); // TODO change
+ };
+ };
+
+ let colcount = sz.columns / (longest + 3);
+ if (target < colcount) colcount = target;
+ let colwidth = longest + 3;
+
+ for (let i = 0z; i < len(lines); i += 1) {
+ if (i != 0 && i % colcount == 0) fmt::println()!;
+ fmt::print(
+ strings::rpad(lines[i], ' ', colwidth)!
+ )!;
+ };
+ fmt::println()!;
+};
diff --git a/cmd/uniq.ha b/cmd/uniq.ha
@@ -22,7 +22,7 @@ fn run() void = {
for (true) {
match (bufio::scan_line(&sc)) {
- case let s: str => append(lines, s)!;
+ case let s: str => append(lines, strings::dup(s)!)!;
case io::EOF => break;
case let e: utf8::invalid => util::die(utf8::strerror(e));
case let e: io::error => util::die(io::strerror(e));