Compare commits
16 Commits
24a81a9d88
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| d921a2b144 | |||
| 31d7c63114 | |||
| e575f1b3d3 | |||
| e55136e433 | |||
| 9b31e4081d | |||
| 017983d672 | |||
| da0032b531 | |||
| 8021dfce8a | |||
| e15a63ddb0 | |||
| dddb8648f4 | |||
| ec7cfb4448 | |||
| 6b81c63251 | |||
| 8f5d7d6df2 | |||
| 1ca8a9ce98 | |||
| 61f85cfdda | |||
| a2543ef78e |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/bulb
|
||||
30
Makefile
Normal file
30
Makefile
Normal file
@@ -0,0 +1,30 @@
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
HARE=hare
|
||||
HAREFLAGS=
|
||||
|
||||
DESTDIR=
|
||||
PREFIX=/usr/local
|
||||
BINDIR=$(PREFIX)/bin
|
||||
MANDIR=$(PREFIX)/share/man
|
||||
|
||||
all: bulb
|
||||
|
||||
bulb:
|
||||
$(HARE) build $(HAREFLAGS) -o $@ cmd/$@/
|
||||
|
||||
check:
|
||||
$(HARE) test $(HAREFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f bulb
|
||||
|
||||
install:
|
||||
mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1
|
||||
install -Dm755 bulb $(DESTDIR)$(BINDIR)/bulb
|
||||
install -Dm755 doc/*.1 $(DESTDIR)$(MANDIR)/man1
|
||||
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)$(BINDIR)/bulb
|
||||
|
||||
.PHONY: all check clean install uninstall
|
||||
28
README.md
28
README.md
@@ -22,27 +22,15 @@ re-writing chat history.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
To list available boards:
|
||||
|
||||
```
|
||||
bulb
|
||||
```
|
||||
bulb: bulletin board
|
||||
|
||||
To post a message:
|
||||
Usage: bulb [-hlpu] [-b <board>] [-n <number>]
|
||||
|
||||
-h: print this help text
|
||||
-b <board>: specify a board other than general
|
||||
-l: list available boards and exit
|
||||
-n <number>: display N most recent messages
|
||||
-p: post a message (from stdin)
|
||||
-u: operate undercover (anonymously)
|
||||
```
|
||||
bulb post Message text
|
||||
bulb post -b <board-name> Message text
|
||||
```
|
||||
|
||||
To read recent messages:
|
||||
|
||||
```
|
||||
bulb read
|
||||
bulb read -b <board-name>
|
||||
bulb read -n <message-count>
|
||||
```
|
||||
|
||||
Note that when not specified, the board name defaults to `general`. For this
|
||||
reason, it is recommended to ensure that a board with this name exists.
|
||||
|
||||
119
bulb/bulb.ha
119
bulb/bulb.ha
@@ -1,119 +0,0 @@
|
||||
use fs;
|
||||
use fmt;
|
||||
use io;
|
||||
use path;
|
||||
use os;
|
||||
use strings;
|
||||
|
||||
let bulb_path: []str = [];
|
||||
@init fn bulb_path() void = bulb_path = strings::split(os::tryenv("BULBPATH", "/var/bulb"), ":");
|
||||
@fini fn bulb_path() void = free(bulb_path);
|
||||
|
||||
export type no_such_board = !void;
|
||||
export type invalid_board = !void;
|
||||
export type error = (io::error | fs::error | no_such_board | invalid_board);
|
||||
|
||||
export fn strerror(err: error) str = match(err) {
|
||||
case no_such_board => return "No such board";
|
||||
case invalid_board => return "Invalid board name";
|
||||
case let err: fs::error => return fs::strerror(err);
|
||||
case let err: io::error => return io::strerror(err);
|
||||
};
|
||||
|
||||
export fn read(board: str, number: int) (void | error) = {
|
||||
validate_board_name(board)?;
|
||||
let location = look_up_board(board)?;
|
||||
let file = os::open(location)?;
|
||||
defer io::close(file)!;
|
||||
seek_to_nth_last_line(file, number)?;
|
||||
io::copy(os::stdout, file)?;
|
||||
};
|
||||
|
||||
export fn post(board: str, user_name: (str | void), message: str) (void | error) = {
|
||||
validate_board_name(board)?;
|
||||
let location = look_up_board(board)?;
|
||||
let file = os::open(location, fs::flag::APPEND | fs::flag::WRONLY)?;
|
||||
defer io::close(file)!;
|
||||
|
||||
let user = match(user_name) {
|
||||
case let user: str => yield(user);
|
||||
case void => yield "anonymous";
|
||||
};
|
||||
fmt::fprintf(file, "{}: {}\n", user, message)?;
|
||||
};
|
||||
|
||||
export fn list() (void | error) = {
|
||||
for (let directory .. bulb_path) list_boards_in(directory)?;
|
||||
};
|
||||
|
||||
export fn look_up_board(board: str) (str | no_such_board | invalid_board) = {
|
||||
validate_board_name(board)?;
|
||||
static let buf = path::buffer { ... };
|
||||
for (let directory .. bulb_path) {
|
||||
let location = path::set(&buf, directory, board)!;
|
||||
match (os::stat(location)) {
|
||||
case fs::filestat => return location;
|
||||
case => void;
|
||||
};
|
||||
};
|
||||
return no_such_board;
|
||||
};
|
||||
|
||||
export fn validate_board_name(board: str) (void | invalid_board) = {
|
||||
let iter = strings::iter(board);
|
||||
for (true) match (strings::next(&iter)) {
|
||||
case let run: rune =>
|
||||
if (
|
||||
run: int == '/' ||
|
||||
run: int == '.' ||
|
||||
run: int == '\\' ||
|
||||
run: int == ':') {
|
||||
return invalid_board;
|
||||
};
|
||||
case done => return void;
|
||||
};
|
||||
};
|
||||
|
||||
fn list_boards_in(directory: str) (void | fs::error) = {
|
||||
let entries = os::readdir(directory)?;
|
||||
defer fs::dirents_free(entries);
|
||||
for (let entry .. entries) {
|
||||
fmt::println(entry.name)!;
|
||||
};
|
||||
};
|
||||
|
||||
fn seek_to_nth_last_line(file: io::handle, number: int) (void | io::error) = {
|
||||
let buffer: [256]u8 = [0...];
|
||||
let end = io::seek(file, 0, io::whence::END)?;
|
||||
let step = len(buffer): io::off;
|
||||
let current = end;
|
||||
|
||||
for (true) {
|
||||
// read a chunk in reverse, or stop if there is nothing left
|
||||
let buffer = buffer[..];
|
||||
if (current - step > 0) {
|
||||
current -= step;
|
||||
} else {
|
||||
buffer = buffer[..current];
|
||||
current = 0;
|
||||
};
|
||||
io::seek(file, current, io::whence::SET)?;
|
||||
let length = match (io::read(file, buffer)?) {
|
||||
case let length: size => yield length;
|
||||
case io::EOF => break;
|
||||
};
|
||||
io::seek(file, current, io::whence::SET)?;
|
||||
|
||||
// look for line breaks in that chunk
|
||||
let buffer = buffer[..length];
|
||||
for (let index = len(buffer): int - 1; index >= 0; index -= 1) {
|
||||
if (buffer[index: size] == '\n') number -= 1;
|
||||
if (number <= 0) {
|
||||
io::seek(file, current + index: io::off, io::whence::SET)?;
|
||||
return void;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
return void;
|
||||
};
|
||||
104
cmd/bulb/main.ha
104
cmd/bulb/main.ha
@@ -1,8 +1,8 @@
|
||||
use bulb;
|
||||
use errors;
|
||||
use fmt;
|
||||
use fs;
|
||||
use getopt;
|
||||
use internal::bulb;
|
||||
use io;
|
||||
use os;
|
||||
use path;
|
||||
@@ -19,83 +19,67 @@ export fn main() void = {
|
||||
const cmd = getopt::parse(
|
||||
os::args,
|
||||
"bulletin board",
|
||||
("post", [
|
||||
('a', "post anonymously"),
|
||||
('b', "board", "which board to post on"),
|
||||
"message...",
|
||||
]): getopt::subcmd_help,
|
||||
("read", [
|
||||
('b', "board", "which board to read"),
|
||||
('n', "number", "display N most recent messages"),
|
||||
]): getopt::subcmd_help);
|
||||
('b', "board", "specify a board other than general"),
|
||||
('l', "list available boards and exit"),
|
||||
('n', "number", "display N most recent messages"),
|
||||
('p', "post a message (from stdin)"),
|
||||
('u', "operate undercover (anonymously)"));
|
||||
defer getopt::finish(&cmd);
|
||||
|
||||
match (cmd.subcmd) {
|
||||
case let subcmd: (str, *getopt::command) =>
|
||||
if (subcmd.0 == "post") {
|
||||
subcmd_post(subcmd.1);
|
||||
} else if (subcmd.0 == "read") {
|
||||
match (subcmd_read(subcmd.1)) {
|
||||
case errors::invalid =>
|
||||
getopt::printhelp(os::stdout, name, cmd.help)!;
|
||||
os::exit(os::status::FAILURE);
|
||||
case void => void;
|
||||
};
|
||||
};
|
||||
case void =>
|
||||
subcmd_ls(&cmd);
|
||||
};
|
||||
};
|
||||
|
||||
fn subcmd_post(cmd: *getopt::command) void = {
|
||||
let anonymous = false;
|
||||
let board = default_board;
|
||||
for (let opt .. cmd.opts) {
|
||||
switch (opt.0) {
|
||||
case 'a' => anonymous = true;
|
||||
case 'b' => board = opt.1;
|
||||
case => abort();
|
||||
};
|
||||
};
|
||||
|
||||
let user_name = if (anonymous) void else get_user_name();
|
||||
let message = strings::join(" ", cmd.args...);
|
||||
defer free(message);
|
||||
let list = false;
|
||||
let number = 8;
|
||||
let post = false;
|
||||
let anonymous = false;
|
||||
|
||||
match (bulb::post(board, user_name, message)) {
|
||||
case let err: bulb::error =>
|
||||
fmt::errorf("{}: Could not read {}: {}\n", name, board, bulb::strerror(err))!;
|
||||
case void => void;
|
||||
};
|
||||
};
|
||||
|
||||
fn subcmd_read(cmd: *getopt::command) (void | errors::invalid) = {
|
||||
let board = default_board;
|
||||
let number = 8;
|
||||
for (let opt .. cmd.opts) {
|
||||
switch (opt.0) {
|
||||
case 'b' =>
|
||||
board = opt.1;
|
||||
case 'l' =>
|
||||
list = true;
|
||||
case 'n' =>
|
||||
match (strconv::stoi(opt.1)) {
|
||||
case let num: int => number = num;
|
||||
case => return errors::invalid;
|
||||
case let num: int =>
|
||||
number = num;
|
||||
case =>
|
||||
fmt::errorf("{}: option -{} requires a number\n", name, opt.0)!;
|
||||
getopt::printusage(os::stdout, name, cmd.help)!;
|
||||
os::exit(os::status::FAILURE);
|
||||
};
|
||||
case 'p' =>
|
||||
post = true;
|
||||
case 'u' =>
|
||||
anonymous = true;
|
||||
case => abort();
|
||||
};
|
||||
};
|
||||
|
||||
match (bulb::read(board, number)) {
|
||||
case let err: bulb::error =>
|
||||
fmt::errorf("{}: Could not read {}: {}\n", name, board, bulb::strerror(err))!;
|
||||
case void => void;
|
||||
if (list) {
|
||||
// list boards and exit
|
||||
match (bulb::list(os::stdout)) {
|
||||
case let err: bulb::error =>
|
||||
fmt::errorf("{}: could not list boards: {}\n", name, bulb::strerror(err))!;
|
||||
os::exit(os::status::FAILURE);
|
||||
case void =>
|
||||
os::exit(os::status::SUCCESS);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
fn subcmd_ls(cmd: *getopt::command) void = {
|
||||
match (bulb::list()) {
|
||||
if (post) {
|
||||
// post a message (from stdin)
|
||||
let user_name = if (anonymous) void else get_user_name();
|
||||
match (bulb::post(os::stdin, board, user_name)) {
|
||||
case let err: bulb::error =>
|
||||
fmt::errorf("{}: could not post to {}: {}\n", name, board, bulb::strerror(err))!;
|
||||
case void => void;
|
||||
};
|
||||
};
|
||||
|
||||
// read latest posts on the board
|
||||
match (bulb::read(os::stdout, board, number)) {
|
||||
case let err: bulb::error =>
|
||||
fmt::errorf("{}: Could not list boards: {}\n", name, bulb::strerror(err))!;
|
||||
fmt::errorf("{}: could not read {}: {}\n", name, board, bulb::strerror(err))!;
|
||||
case void => void;
|
||||
};
|
||||
};
|
||||
|
||||
102
doc/bulb.1
Normal file
102
doc/bulb.1
Normal file
@@ -0,0 +1,102 @@
|
||||
.\" Copyright (c) 2024 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||
.\"
|
||||
.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license,
|
||||
.\" visit <http://creativecommons.org/licenses/by-sa/4.0/>.
|
||||
.\"
|
||||
.TH BULB 1 2024-10-11
|
||||
.SH NAME
|
||||
bulb \(en bulletin board
|
||||
.\"
|
||||
.SH SYNOPSIS
|
||||
|
||||
bulb
|
||||
.RB [ -hlpu ]
|
||||
.RB [ -b\ board ]
|
||||
.RB [ -n\ number ]
|
||||
.\"
|
||||
.SH DESCRIPTION
|
||||
|
||||
Read from and post to boards (text files) on the system. Posts are plain text,
|
||||
multiline, and each one is tagged with the user name of its author.
|
||||
.\"
|
||||
.SH OPTIONS
|
||||
|
||||
.IP \fB-h\fP
|
||||
Prints the help text and exits.
|
||||
.IP \fB-b\fP\ \fIboard\fP
|
||||
Specifies a board other than \(lqgeneral\(rq.
|
||||
.IP \fB-l\fP
|
||||
Lists available boards and exits.
|
||||
.IP \fB-n\fP\ \fInumber\fP
|
||||
Takes a numeric argument as the amount of recent posts to print out, the default
|
||||
being 8.
|
||||
.IP \fB-p\fP
|
||||
Posts a message read from the standard input. See the STANDARD INPUT section.
|
||||
.IP \fB-u\fP
|
||||
Operates undercover. All posts made will be under the name \(lqanonymous\(rq.
|
||||
.\"
|
||||
.SH ENVIRONMENT
|
||||
|
||||
The BULBPATH environment variable specifies a colon-separated list of
|
||||
directories (much like PATH) in which to search for boards. Directories at the
|
||||
start take precedence over directories at the end. It has a value of
|
||||
.I /var/bulb
|
||||
by default.
|
||||
.SH FILES
|
||||
.I /var/bulb/*
|
||||
.\"
|
||||
.SH STANDARD INPUT
|
||||
|
||||
When the
|
||||
.B -p
|
||||
option is supplied, the standard input will be read until EOF. The resulting
|
||||
text will be posted under the name of the current user in whichever board is
|
||||
selected.
|
||||
.\"
|
||||
.SH STANDARD OUTPUT
|
||||
|
||||
If the
|
||||
.B -l
|
||||
option is supplied, a list of available boards will be written to the standard
|
||||
output. If not, the last
|
||||
.B -n
|
||||
messages in the selected board will be written.
|
||||
.\"
|
||||
.SH DIAGNOSTICS
|
||||
|
||||
In the event of an I/O error, a debug message will be printed and the program
|
||||
will exit with an error code. In the event of a bad invocation, a debug message
|
||||
will be printed alongside usage text and the program will exit with an error
|
||||
code.
|
||||
.\"
|
||||
.SH RATIONALE
|
||||
|
||||
The commands
|
||||
.BR talk (1p),
|
||||
and
|
||||
.BR write (1p),
|
||||
and
|
||||
.BR wall (1p)
|
||||
can be used effectively for textual intra-system communication, but they require
|
||||
that the receiving user(s) be both logged in and monitoring their terminal for
|
||||
any messages to ever get through. The
|
||||
.BR bulb (1)
|
||||
command was written to support asynchronous, persistent conversations between
|
||||
users of the same system.
|
||||
.\"
|
||||
.SH AUTHOR
|
||||
|
||||
Written by Sasha Koshka
|
||||
.MT sashakoshka@tebibyte.media
|
||||
.ME .
|
||||
.\"
|
||||
.SH COPYRIGHT
|
||||
|
||||
Copyright \(co 2024 Sasha Koshka (pseudonymous). License GPLv3+: GNU GPL version
|
||||
3 or later <https://gnu.org/licenses/gpl.html>
|
||||
\."
|
||||
.SH SEE ALSO
|
||||
|
||||
.BR talk (1p),
|
||||
.BR write (1p),
|
||||
.BR wall (1p)
|
||||
196
internal/bulb/bulb.ha
Normal file
196
internal/bulb/bulb.ha
Normal file
@@ -0,0 +1,196 @@
|
||||
use ascii;
|
||||
use bufio;
|
||||
use fs;
|
||||
use fmt;
|
||||
use io;
|
||||
use os;
|
||||
use path;
|
||||
use strings;
|
||||
|
||||
// TODO
|
||||
// two issues:
|
||||
// A: posts are stored exactly as they are formatted
|
||||
// B: posts need to be sanitized from control chars going in and going out
|
||||
// C: specifying a number of posts to read only works with lines because it
|
||||
// cant separate out the individual posts
|
||||
//
|
||||
// all of these can be fixed by escaping line breaks as the post is written to
|
||||
// the board, and then rendering them once they get to the reading stage.
|
||||
// sanitize the text at both stages.
|
||||
|
||||
let bulb_path: []str = [];
|
||||
@init fn bulb_path() void = bulb_path = strings::split(os::tryenv("BULBPATH", "/var/bulb"), ":");
|
||||
@fini fn bulb_path() void = free(bulb_path);
|
||||
|
||||
export type no_such_board = !void;
|
||||
export type invalid_board = !void;
|
||||
export type error = !(io::error | fs::error | no_such_board | invalid_board);
|
||||
|
||||
export fn strerror(err: error) str = match(err) {
|
||||
case no_such_board => return "No such board";
|
||||
case invalid_board => return "Invalid board name";
|
||||
case let err: fs::error => return fs::strerror(err);
|
||||
case let err: io::error => return io::strerror(err);
|
||||
};
|
||||
|
||||
export fn read(output: io::handle, board: str, number: int) (void | error) = {
|
||||
validate_board_name(board)?;
|
||||
let location = look_up_board(board)?;
|
||||
let file = os::open(location)?;
|
||||
defer io::close(file)!;
|
||||
// we add 1 to account for the blank line that will always be at the
|
||||
// bottom
|
||||
seek_to_nth_last_line(file, number + 1)?;
|
||||
|
||||
static let output_wbuf: [os::BUFSZ]u8 = [0...];
|
||||
let output = bufio::init(output, [], output_wbuf);
|
||||
let file = bufio::newscanner(file);
|
||||
defer bufio::finish(&file);
|
||||
|
||||
let saw_escape = false;
|
||||
for (true) {
|
||||
let byte = match (bufio::scan_byte(&file)?) {
|
||||
case let byte: u8 => yield byte;
|
||||
case io::EOF => break;
|
||||
};
|
||||
|
||||
if (byte == '\x1b') {
|
||||
saw_escape = true;
|
||||
} else {
|
||||
if (byte == '\n') {
|
||||
if (saw_escape) {
|
||||
fmt::fprint(&output, "\n ")?;
|
||||
} else {
|
||||
fmt::fprint(&output, "\n")?;
|
||||
};
|
||||
} else if (!ascii::iscntrl(byte: rune)) {
|
||||
let fake_buffer: [1]u8 = [byte];
|
||||
io::write(&output, fake_buffer)?;
|
||||
};
|
||||
saw_escape = false;
|
||||
};
|
||||
};
|
||||
|
||||
bufio::flush(&output)?;
|
||||
};
|
||||
|
||||
export fn post(input: io::handle, board: str, user_name: (str | void)) (void | error) = {
|
||||
validate_board_name(board)?;
|
||||
let location = look_up_board(board)?;
|
||||
let file = os::open(location, fs::flag::APPEND | fs::flag::WRONLY)?;
|
||||
defer io::close(file)!;
|
||||
|
||||
let user = match(user_name) {
|
||||
case let user: str => yield(user);
|
||||
case void => yield "anonymous";
|
||||
};
|
||||
|
||||
static let file_wbuf: [os::BUFSZ]u8 = [0...];
|
||||
let file = bufio::init(file, [], file_wbuf);
|
||||
let input = bufio::newscanner(input);
|
||||
defer bufio::finish(&input);
|
||||
|
||||
let saw_break = false;
|
||||
fmt::fprintf(&file, "{}: ", user)?;
|
||||
for (true) {
|
||||
let byte = match (bufio::scan_byte(&input)?) {
|
||||
case let byte: u8 => yield byte;
|
||||
case io::EOF => break;
|
||||
};
|
||||
|
||||
if (saw_break) {
|
||||
fmt::fprint(&file, "\x1b\n")?;
|
||||
saw_break = false;
|
||||
};
|
||||
|
||||
if (byte == '\n') {
|
||||
saw_break = true;
|
||||
} else if (!ascii::iscntrl(byte: rune)) {
|
||||
let fake_buffer: [1]u8 = [byte];
|
||||
io::write(&file, fake_buffer)?;
|
||||
};
|
||||
};
|
||||
|
||||
fmt::fprint(&file, "\n")?;
|
||||
bufio::flush(&file)?;
|
||||
};
|
||||
|
||||
export fn list(output: io::handle) (void | error) = {
|
||||
for (let directory .. bulb_path) list_boards_in(output, directory)?;
|
||||
};
|
||||
|
||||
export fn look_up_board(board: str) (str | no_such_board | invalid_board) = {
|
||||
validate_board_name(board)?;
|
||||
static let buf = path::buffer { ... };
|
||||
for (let directory .. bulb_path) {
|
||||
let location = path::set(&buf, directory, board)!;
|
||||
match (os::stat(location)) {
|
||||
case fs::filestat => return location;
|
||||
case => void;
|
||||
};
|
||||
};
|
||||
return no_such_board;
|
||||
};
|
||||
|
||||
export fn validate_board_name(board: str) (void | invalid_board) = {
|
||||
let iter = strings::iter(board);
|
||||
for (true) match (strings::next(&iter)) {
|
||||
case let run: rune =>
|
||||
if (
|
||||
run: u32 == '/' ||
|
||||
run: u32 == '.' ||
|
||||
run: u32 == '\\' ||
|
||||
run: u32 == ':') {
|
||||
return invalid_board;
|
||||
};
|
||||
case done => return void;
|
||||
};
|
||||
};
|
||||
|
||||
fn list_boards_in(output: io:: handle, directory: str) (void | error) = {
|
||||
let entries = os::readdir(directory)?;
|
||||
defer fs::dirents_free(entries);
|
||||
for (let entry .. entries) {
|
||||
fmt::fprintln(output, entry.name)?;
|
||||
};
|
||||
};
|
||||
|
||||
fn seek_to_nth_last_line(file: io::handle, number: int) (void | io::error) = {
|
||||
let buffer: [256]u8 = [0...];
|
||||
let end = io::seek(file, 0, io::whence::END)?;
|
||||
let step = len(buffer): io::off;
|
||||
let current = end;
|
||||
|
||||
for (true) {
|
||||
// read a chunk in reverse, or stop if there is nothing left
|
||||
let buffer = buffer[..];
|
||||
if (current - step > 0) {
|
||||
current -= step;
|
||||
} else {
|
||||
buffer = buffer[..current];
|
||||
current = 0;
|
||||
};
|
||||
io::seek(file, current, io::whence::SET)?;
|
||||
let length = match (io::read(file, buffer)?) {
|
||||
case let length: size => yield length;
|
||||
case io::EOF => break;
|
||||
};
|
||||
|
||||
// look for line breaks in that chunk
|
||||
let buffer = buffer[..length];
|
||||
for (let index = len(buffer): int - 1; index >= 0; index -= 1) {
|
||||
if (buffer[index: size] == '\n') number -= 1;
|
||||
if (number <= 0) {
|
||||
current += index;
|
||||
io::seek(file, current: io::off, io::whence::SET)?;
|
||||
if (current != end) {
|
||||
current += 1;
|
||||
io::seek(file, current, io::whence::SET)?;
|
||||
};
|
||||
return void;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
return void;
|
||||
};
|
||||
Reference in New Issue
Block a user