#stdout stdin in a struct

1 messages · Page 1 of 1 (latest)

burnt thunder
#

I'm trying to create a simple wrapper around console io, but having issues with determining how to create fields for the respective writers and readers of stdout and stdin. Maybe I'm going about this entirely wrong?

main.zig

const std = @import("std");
const Screen = @import("screen.zig");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const alloc = gpa.allocator();

    const screen: Screen = Screen.init(alloc);
    _ = screen;
}

screen.zig

const std = @import("std");
const Self = @This();

alloc: std.mem.Allocator,
stdout: std.fs.File,
buffWriter: std.io.bufferedWriter,
writer: std.io.Writer,
stdin: std.fs.File,
readBuffer: std.ArrayList(u8),
buffReader: std.io.BufferedReader,
reader: std.io.Reader,

pub fn init(self: *Self, alloc: std.mem.Allocator) !void {
    self.alloc = alloc;
    // Handle to the standard output
    self.stdout = std.io.getStdOut();
    self.buffWriter = std.io.bufferedWriter(self.sdout.writer());
    self.writer = self.buffWriter.writer();
    // Handle to the standard input
    self.stdin = std.io.getStdIn();
    self.readBuffer = std.ArrayList(u8).init(self.alloc, 4098);
    self.buffReader = std.io.bufferedReader(self.stdin.reader());
    self.reader = self.readBuffer.reader();
}

pub fn deinit(self: *Self) void {
    self.readBuffer.deinit();
}
gray yacht
#

std.io.Writer is not a type, it is a function which returns a type

#

simplest thing to do here would be to have writer be a function instead of a field, forwarding the result of self.bufWriter.writer()

#

and also, the type of buffWriter should be std.io.BufferedWriter(std.fs.File.Writer, n), where n is some value like 4096, for the buffer size

#

same deal with buffReader

#

and also turn reader into a function forwarding the result of buffReader.reader()

#

btw remember you'll need to buffWriter.flush() periodically and at the end of the lifetime of this thing in order to make sure everything you wrote is actually written

#

Here is how I would write this code (if I were writing it in this way, which in reality I probably wouldn't):

const Screen = @This();
allocator: std.mem.Allocator,
stdout: std.io.BufferedWriter(std.fs.File.Writer, 4096),
stdin: std.io.BufferedReader(std.fs.File.Reader, 4096),
read_buffer: std.ArrayListUnmanaged(u8),

pub fn init(
    allocator: std.mem.Allocator,
    stdout: std.fs.File,
    stdin: std.fs.File,
) Screen {
    return .{
        .allocator = allocator,
        .stdout = std.io.bufferedWriter(stdout.writer()),
        .stdin = std.io.bufferedReader(stdin.reader()),
        .read_buffer = .{},
    };
}
burnt thunder
#

The goal is to do most of the terminal work through two functions kinda like these for simplification:

pub fn write(self: *Self, text: []u8) !void {...}
pub fn read(self: *Self, text: []u8) ![]u8 {...}

So when these functions are invoked they'd do something like self.stdout.writer() each time? idk if that's smart, but then again I come from java and you said you wouldn't write it this way at all.

gray yacht
#

well, given the way buffering works, it's liable to give you some pretty unintiuitve behaviours if that's your only interface into it

#

you'll need to .stdout.flush() every now and then to make sure messages aren't too delayed or chopped up

#

Bit confused by how your proposed read function would behave

burnt thunder
#

read would take in a prompt message and then return a slice of whatever was entered by the user, which now that I think about it should probably be written to a variable passed in, maybe:

pub fn read(self: *Self, text: []u8, out: *ArrayList(u8)) !void {...}

For a bit of background, this is my first real project to get familiar with zig. A little text-based adventure.

gray yacht
#

So when these functions are invoked they'd do something like self.stdout.writer() each time?
also yes, all those functions do is return the writer/reader interface wrapping the implementation

#

well, the way I'd write this is without wrapping all of this up in Screen at all, just have them as variables and use them as needed. If there were repeated functionality for a few of them, I'd write a function that takes them as parameters and use that - which is essentially what implicitly ends up happening when you wrap it up in an object, with the difference being that you only parameterize the function with the exact data it needs, and not any uninvolved fields.
I understand that coming from Java you may be inclined to wrap things up in objects, however I suggest you give working with just plain ol data and variables a try

#

later on if you see a pattern emerge where a set of variables are often used together, with a set of behaviours, you could easily group those up in a struct, without having to untangle them from another object, incorrectly conceived at the beginning

#

I also say this because it's just going to be simpler in zig

burnt thunder
#

I see your point. There's another reason I was wanting to wrap it up: put the code in another file for readability / mental compartmentalization. Perhaps there's a smarter way to do that?

gray yacht
#

I mean, we are currently talking about about 5 variables at most

#

most of which you'll have to access directly anyway

#

you're just not really gaining that much by trying to hide away details

burnt thunder
#

true. I intend to expand with utils: text boxes around the msg, table generation, etc.

gray yacht
#

of course, an application will eventually scale

#

however, worse than even premature optimisation is premature abstraction

#

a good abstraction is based off of a real implementation, not the faulty imagination of a programmer unable to predict the future needs of the application

burnt thunder
#

I'll try your suggestion and hopefully keep it clean in the main.zig file. I'll paste here with whatever I come up with.

burnt thunder
#

@gray yacht I got stuck. How do I pass stdout and stdin to a function?

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    var allocator = gpa.allocator();

    var stdout = std.io.bufferedWriter(std.io.getStdOut().writer());
    var writer = stdout.writer();

    var stdin = std.io.bufferedReader(std.io.getStdIn().reader());
    var reader = stdin.reader();
    _ = reader;
    var read_buffer = std.ArrayList(u8).init(allocator);
    defer read_buffer.deinit();

    const first_name = GetString(stdout, stdin, read_buffer, "What is your first name?");
    const last_name = GetString(stdout, stdin, read_buffer, "What is your last name?");

    try writer.print("Your name is: {s} {s}", .{ first_name, last_name });
}

pub fn GetString(stdout: anytype, stdin: anytype, buffer: std.ArrayList(u8), msg: []const u8) !?[]const u8 {
    var writer = stdout.writer();
    var reader = stdin.reader();

    try writer.print(msg, .{});
    try stdout.flush();

    var try_count: usize = 1;
    const try_max: usize = 3;
    var input: []const u8 = undefined;
    var confirm: []const u8 = undefined;

    while (try_count <= try_max) : (try_count += 1) {
        try writer.print("{s}: ", .{msg});
        stdout.flush();
        try reader.streamUntilDelimiter(buffer.writer(), '\r', null);
        input = std.mem.trim(u8, buffer.items, "\r");
        try writer.print("Is this correct [yes|no]?", .{});
        stdout.flush();
        confirm = std.mem.trim(u8, buffer.items, "\r");

        if (std.mem.eql(u8, confirm, "yes") || std.mem.eql(u8, confirm, "Y")) {
            return input;
        }
    }

    try writer.print("Too many tries.", .{});
    stdout.flush();
    return null;
}
gray yacht
#

sorry for the late reply, got busied

burnt thunder
#

no worries

#

how would I flush() then?

gray yacht
#

well, looking at this, it seems like it'd be simpler to just pass the unbuffered writer directly

#

you're writing, and then immediately flushing in every instance that you use the writer in GetString, which means you're not gaining any performance by buffering here anyway

#

so instead of passing stdout.writer(), perhaps just pass stdout.unbuffered_writer (which is an std.fs.File.Writer in this case)

burnt thunder
#

out of time for the night. This doesn't work as expected, I'm but getting closer!

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    var allocator = gpa.allocator();

    var stdout = std.io.bufferedWriter(std.io.getStdOut().writer());
    var writer = stdout.writer();

    var stdin = std.io.bufferedReader(std.io.getStdIn().reader());
    var reader = stdin.reader();
    _ = reader;
    var read_buffer = std.ArrayList(u8).init(allocator);
    defer read_buffer.deinit();

    const first_name = try GetString(stdout.unbuffered_writer, stdin.unbuffered_reader, &read_buffer, "What is your first name?") orelse "NULL";
    const last_name = try GetString(stdout.unbuffered_writer, stdin.unbuffered_reader, &read_buffer, "What is your last name?") orelse "NULL";

    try writer.print("Your name is: {s} {s}", .{ first_name, last_name });
}

pub fn GetString(writer: std.fs.File.Writer, reader: std.fs.File.Reader, buffer: *std.ArrayList(u8), comptime msg: []const u8) !?[]const u8 {
    var try_count: usize = 1;
    const try_max: usize = 3;
    var string: []const u8 = "";
    var confirm: []const u8 = "";
    var isConfirmed: bool = false;

    while (isConfirmed == false or try_count <= try_max) : (try_count += 1) {
        try writer.print("{s}: ", .{msg});
        try reader.streamUntilDelimiter(buffer.writer(), '\n', null);
        string = std.mem.trim(u8, buffer.items, "\r");
        try buffer.resize(0);

        try writer.print("You said: {s}\nIs this correct [yes|no]?", .{string});
        try reader.streamUntilDelimiter(buffer.writer(), '\n', null);
        confirm = std.mem.trim(u8, buffer.items, "\r");
        try buffer.resize(0);

        if (std.mem.eql(u8, confirm, "yes") or std.mem.eql(u8, confirm, "Y")) {
            return string;
        }
    }

    return null;
}
gray yacht
#

looking good so far

burnt thunder
#

I keep coming back to this idea. Console.zig is incorrect, I think it's just the fields that are wrong. Maybe I'm just too green with how zig handles interfaces, but ngl I wish there was interface syntax that I could click through in my IDE and figure out what exactly I need for Console's field types. The way zig does it is hard to follow, or at least it is currently. Also I haven't quite figured out how the namespacing is yet, just guessed. Also also, I read all these files are considered structs, but maybe there is different syntax sugar for this approach?
main.zig

const std = @import("std");
const Console = @import("./Console.zig");

pub fn main() !void {
    var console = Console.init();
    try console.print("Hello {s}", .{"world"});
    try console.flush();
}

Console.zig

const Self = @This();
const std = @import("std");

bw: std.io.Writer,
stdout: std.io.Writer,

// allocator param for read() later on
pub fn init() Self {
    const stdout_file = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(stdout_file);
    const stdout = bw.writer();
    return .{ .bw = bw, .stdout = stdout };
}

pub fn print(self: *Self, str: []const u8, args: anytype) !void {
    try self.stdout.print(str, args);
}

pub fn flush(self: *Self) !void {
    try self.bw.flush();
}
// fns for reading user input, printing boxes, tables, handling ansi
fathom spruce
# burnt thunder I keep coming back to this idea. Console.zig is incorrect, I think it's just th...

std.io.Writer is a function that returns a type, struct fields need to be types. theres two approaches, either hardcode one:

bw: std.fs.File.Writer,
stdout: std.fs.File.Writer,

or extend the generic to your Console struct:

pub fn Console(comptime WriterType: type) type {
  return struct {
    bw: WriterType,
    stdout: WriterType,
    
    // …
  };
}

// …

pub fn main() !void {
  var console = Console(std.fs.File.Writer).init();
}
burnt thunder
#

i tried both your ways, also tried std.io.Writer and no dice:

main

const std = @import("std");
const Console = @import("./Console.zig");

pub fn main() !void {
    const console = Console(std.fs.File.Writer).init();
    try console.print("Hello {s}", .{"world"});
    try console.flush();
}

Console

const std = @import("std");

pub fn Console(comptime WriterType: type) type {
    return struct {
        bw: WriterType,
        stdout: WriterType,

        const Self = @This();

        pub fn init() Self {
            const stdout_file = std.io.getStdOut().writer();
            var bw = std.io.bufferedWriter(stdout_file);
            const stdout = bw.writer();
            return .{ .bw = bw, .stdout = stdout };
        }

        pub fn print(self: *Self, str: []const u8, args: anytype) !void {
            try self.stdout.print(str, args);
        }

        pub fn flush(self: *Self) !void {
            try self.bw.flush();
        }
    };
}

error
src\main.zig:5:21: error: type 'type' not a function const console = Console(std.fs.File.Writer).init();

fathom spruce
burnt thunder
#

Getting closer. last write isn't printing, I think it has to do with the trailing '\r'

const std = @import("std");
const Terminal = @import("terminal.zig").Terminal;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    var allocator = gpa.allocator();

    var terminal = Terminal.init(allocator);
    defer terminal.deinit();

    try terminal.write("What is your name? : ", .{});
    try terminal.read();
    try terminal.write("-----\n", .{});
    try terminal.write("you said: {s}", .{terminal.buffer.items});
}
const std = @import("std");

pub const Terminal = struct {
    alloc: std.mem.Allocator,
    buffer: std.ArrayList(u8),
    writer: std.fs.File.Writer,
    reader: std.fs.File.Reader,

    pub fn init(alloc: std.mem.Allocator) Terminal {
        return Terminal{ .alloc = alloc, .buffer = std.ArrayList(u8).init(alloc), .writer = std.io.getStdOut().writer(), .reader = std.io.getStdIn().reader() };
    }

    pub fn deinit(self: *Terminal) void {
        self.buffer.deinit();
    }

    pub fn write(self: *Terminal, comptime fmt: []const u8, args: anytype) !void {
        try std.fmt.format(self.writer, fmt, args);
    }

    pub fn read(self: *Terminal) !void {
        try self.reader.readUntilDelimiterArrayList(&self.buffer, '\n', 4096);
    }
};