Yobihomehttps://yobibyte.github.io/Make old internet great again.en-usNewsboat hackhttps://yobibyte.github.io/newsboat_hack.htmlhttps://yobibyte.github.io/newsboat_hack.htmlFri, 12 Jan 2024 21:18:27 -0000Homehttps://yobibyte.github.io/https://yobibyte.github.io/Thu, 11 Jan 2024 19:33:27 -0000John Blow on how to Cope with Mental Issueshttps://yobibyte.github.io/john_blow_advice.htmlhttps://yobibyte.github.io/john_blow_advice.htmlMon, 15 Jan 2024 22:20:10 -0000hard tech is the way to gohttps://yobibyte.github.io/hard_tech.htmlhttps://yobibyte.github.io/hard_tech.htmlWed, 17 Jan 2024 20:30:10 -0000notebooks are McDonalds of codehttps://yobibyte.github.io/notebooks.htmlhttps://yobibyte.github.io/notebooks.htmlThu, 30 May 2024 13:31:45 -0000My Neovim Setuphttps://yobibyte.github.io/neovim_setup.htmlhttps://yobibyte.github.io/neovim_setup.htmlSat, 05 Oct 2024 06:18:17 -0000Framework, Arch and Kernel Updathttps://yobibyte.github.io/kernel_update.htmlhttps://yobibyte.github.io/kernel_update.htmlSat, 07 Dec 2024 06:18:17 -0000titlehttps://yobibyte.github.io/../yobibyte.github.io/safetensors.htmlhttps://yobibyte.github.io/../yobibyte.github.io/safetensors.htmlSun, 9 Feb 2025 18:07:29 +0000reading safetensors in zig

I got curious about Zig recently and decided to learn it. After finishing ziglings (that are great, btw!), I wanted to do a small, self-contained project. I decided that reading safetensors would be a nice idea I can code in a day or so. Why safetensors? Because we can!

What are safetensors? It is a binary format to store neural network weights. They are pretty straightforward. Here is my amazing ASCII art on them.

HEADER SIZE (N)   | HEADER JSON | NUMBERS
^^^ 8 bytes (u64)      N bytes  ^
                                |
                                  ____ Offset 0 is here.

Sounds easy enough! Let's get started! I assume you alreadey have a Zig project set up, and can build and run it. If you don't, check out the zig init command. I won't be using any external dependencies (at the time I didn't even know how to add one), and I will be writing all my code in main.zig file. YOLO!

Here is the plan. We'll first read the header size, to determine how many bytes to read to get the header JSON. Then we'll convert the binary into JSON, parse the metadata and get the info on how where to find the weights themselves. We will also need to think about format, for example, if the weights are in bf16, we'll convert those in f32.

First, let's grab a model! I picked Qwen2.5-coder-0.5B-Instruct. It has only one safetensors file and is small enough to speed up the development.

The high-level code is pretty simple and follows our plan above.

var layers_info = std.ArrayList(*LayerMetadata()).init(allocator);
defer {
  for (layers_info.items) |*item| {
      item.*.deinit();
  }
  layers_info.deinit();
}

// At this point, we will know all the layers names, their types, shapes, and offsets.
const header_size = try get_safetensors_content(safetensors_path, allocator, &layers_info);

var layer_metadata: *LayerMetadata() = undefined;
for (layers_info.items) |layer_spec| {
  layer_spec.print();
  if (std.mem.eql(u8, layer_spec.name, layer_name)) {
      layer_metadata = layer_spec;
  }
}
layer_metadata.print();

// We can extract the weights now.
var weights = load_weights(header_size, layer_metadata, safetensors_path, allocator);
We will store all the metadata in the ArrayList as we do not know the size in advance. I will not put all the code for the metadata structs, but here are the fields:
pub fn LayerMetadata() type {
    return struct {
        allocator: std.mem.Allocator,
        name: []u8,
        shape: []u64,
        dtype: []u8,
        offset_start: u64,
        offset_end: u64,
Sending allocators around is very common in Zig. We include an allocator in our structs to allocate the memory during construction, and free it after the struct is no longer needed. I am using GeneralPurposeAllocator that helps detect memory leaks and use-after-free errors. This is very nice, and I literally found a bug with it right before I started writing this post.

get_safetensors_content is the main parsing bit and was the hardest to write/debug: Opening the file is the easiest part xD, the main bit is not to forget to close it after, let's put a defer.
    var file = try std.fs.openFileAbsolute(fpath, .{});
    defer file.close();
To parse the whole thing, we need the header json, to know how much bytes to read to get the header, we need to read the first 8 bytes to get the header size (see my ASCII art above). Let's get the header size then:
var header_size_buf: [HEADER_SIZE_BUFF_SIZE]u8 = undefined;
_ = try file.read(header_size_buf[0..]);
// Another thing to mind is whether to interpret this as a litle/big endian.
// I could not quickly find what it was, and just tried both.
// The big endian did not make any sense to me, and I later check the parity with Python anyways.
const header_size = std.mem.readInt(u64, &header_size_buf, std.builtin.Endian.little);
Now, we can read the header!
// This is where knowing the header size comes in handy.
const header_buf = try allocator.alloc(u8, header_size);
defer allocator.free(header_buf);
_ = try file.read(header_buf[0..]);
Parsing the header was the hardest part, or, let's say, iterating over it, and putting the correct data to our structs. My solution is a bit hacky and might break with new data types, but it was good enough for a working prototype.
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, header_buf, .{});
defer parsed.deinit();
var iter = parsed.value.object.iterator();
while (iter.next()) |entry| {
    const key = entry.key_ptr;
    // The JSON has some other stuff in it.
    // Skip metadata, we need only layers.
    if (std.mem.eql(u8, key.*, "__metadata__")) {
        continue;
    }

    const val = entry.value_ptr;
    // grab info about the weights dtype
    const dtype = val.object.get("dtype").?.string;

    const unk_shape = val.object.get("shape").?.array;
    const shape = try allocator.alloc(u64, unk_shape.items.len);
    // I forgot to add the next line in the first version of this code, and GeneralPurposeAllocator
    // helped me to find it.
    defer allocator.free(shape);
    // Shape is an array of integers, as we do not know the amount of items,
    // we need to allocate it explicitly on the heap.
    // We can probably have a max number and store the shape dims as well, but I took this path.
    for (unk_shape.items, 0..) |el, idx| {
        switch (el) {
            .integer => |num| {
                const signless: u64 = @as(u64, @intCast(num));
                shape[idx] = @intCast(signless);
            },
            else => {},
        }
    }

    // With offsets, we actually know that there is only start and end.
    // Let's exploit this and store those as separate fields in the struct.
    // const offset_start: u64 = -1;
    // const offset_end: u64 = -1;
    const unk_offsets = val.object.get("data_offsets").?.array;
    const offset_start: u64 = @as(u64, @intCast(unk_offsets.items[0].integer));
    const offset_end: u64 = @as(u64, @intCast(unk_offsets.items[1].integer));
    const cur_layer = try LayerMetadata().init(
        allocator,
        key.*[0..key.*.len],
        shape,
        dtype,
        offset_start,
        offset_end,
    );
    try layers_info.append(cur_layer);
}
That's pretty much it, we have everything to load the weights themselves.
const metadata_bytesize = HEADER_SIZE_BUFF_SIZE + header_size;
const read_len = layer_metadata.offset_end - layer_metadata.offset_start;

// Weight offsets are starting with 0 meaning the first byte *after* the header.
try file.seekTo(layer_metadata.offset_start + metadata_bytesize);
const wbuf = try allocator.alloc(u8, read_len);
const bytes_read = try file.read(wbuf);
if (bytes_read != read_len) {
    std.debug.print("Something is wrong! Expected bytes to read: {}. Actual bytes read:{}.]\n", .{ read_len, bytes_read });
    std.process.exit(1);
}
defer allocator.free(wbuf);

// Here I hardcode that we are in bf16. 
// This is not true for all, and has to be generalised.
const bf16_count: usize = read_len / 2;

const rows = layer_metadata.shape[0];
const cols = layer_metadata.shape[1];
// Again, here we assume that we have a two dimensional array.
// Check that the shape corresponds to amount of bytes we read.
std.debug.assert(rows * cols == bf16_count);

// Original weights are in bf16, let's get fp32 from those.
var f32_values = try allocator.alloc(f32, bf16_count);
defer allocator.free(f32_values);
batch_bf16bytes_to_fp32(wbuf, bf16_count, f32_values);

// Let's get the 2D array printed to compare to what we see in Python (run test.py to compare).
var weights = try NDArray(f32).init(allocator, rows, cols);
weights.copy_from(&f32_values);
The only missing bit here, before we wrap up is how we convert bf16 to fp32. We are actually pretty lucky here! Both have the same sign bit and the exponent length! We can just pad the missing half with zeros from the right:
pub fn batch_bf16bytes_to_fp32(bf16_buf: []u8, bf16_count: usize, fp32_buf: []f32) void {
    const bf16_ptr = @as([*]u16, @ptrCast(@alignCast(bf16_buf.ptr)));
    const bf16_slice = bf16_ptr[0..bf16_count];
    const shift_width: u32 = 16;
    for (bf16_slice, 0..) |bf, i| {
        // Showing my incredible bit arithmetics skills here.
        const bits: u32 = @as(u32, bf) << shift_width;
        fp32_buf[i] = @bitCast(bits);
    }
}

And that's it! A self-contained Zig program to parse safetensors. There's definitely room for improvement, but it works! I'll probably make it a bit more general in the future, or you can send me a PR! You can find the code here.


]]>
reading safetensors in zighttps://yobibyte.github.io/safetensors.htmlhttps://yobibyte.github.io/safetensors.htmlSun, 9 Feb 2025 22:55:02 +0000reading safetensors in zig

I got curious about Zig recently and decided to learn it. After finishing ziglings (that are great, btw!), I wanted to do a small, self-contained project. I decided that reading safetensors would be a nice idea I can code in a day or so. Why safetensors? Because we can!

What are safetensors? It is a binary format to store neural network weights. They are pretty straightforward. Here is my amazing ASCII art on them.

HEADER SIZE (N)   | HEADER JSON | NUMBERS
^^^ 8 bytes (u64)      N bytes  ^
                                |
                                  ____ Offset 0 is here.

Sounds easy enough! Let's get started! I assume you alreadey have a Zig project set up, and can build and run it. If you don't, check out the zig init command. I won't be using any external dependencies (at the time I didn't even know how to add one), and I will be writing all my code in main.zig file. YOLO!

Here is the plan. We'll first read the header size, to determine how many bytes to read to get the header JSON. Then we'll convert the binary into JSON, parse the metadata and get the info on how where to find the weights themselves. We will also need to think about format, for example, if the weights are in bf16, we'll convert those in f32.

First, let's grab a model! I picked Qwen2.5-coder-0.5B-Instruct. It has only one safetensors file and is small enough to speed up the development.

The high-level code is pretty simple and follows our plan above.

var layers_info = std.ArrayList(*LayerMetadata()).init(allocator);
defer {
  for (layers_info.items) |*item| {
      item.*.deinit();
  }
  layers_info.deinit();
}

// At this point, we will know all the layers names, their types, shapes, and offsets.
const header_size = try get_safetensors_content(safetensors_path, allocator, &layers_info);

var layer_metadata: *LayerMetadata() = undefined;
for (layers_info.items) |layer_spec| {
  layer_spec.print();
  if (std.mem.eql(u8, layer_spec.name, layer_name)) {
      layer_metadata = layer_spec;
  }
}
layer_metadata.print();

// We can extract the weights now.
var weights = load_weights(header_size, layer_metadata, safetensors_path, allocator);
We will store all the metadata in the ArrayList as we do not know the size in advance. I will not put all the code for the metadata structs, but here are the fields:
pub fn LayerMetadata() type {
    return struct {
        allocator: std.mem.Allocator,
        name: []u8,
        shape: []u64,
        dtype: []u8,
        offset_start: u64,
        offset_end: u64,
Sending allocators around is very common in Zig. We include an allocator in our structs to allocate the memory during construction, and free it after the struct is no longer needed. I am using GeneralPurposeAllocator that helps detect memory leaks and use-after-free errors. This is very nice, and I literally found a bug with it right before I started writing this post.

get_safetensors_content is the main parsing bit and was the hardest to write/debug: Opening the file is the easiest part xD, the main bit is not to forget to close it after, let's put a defer.
    var file = try std.fs.openFileAbsolute(fpath, .{});
    defer file.close();
To parse the whole thing, we need the header json, to know how much bytes to read to get the header, we need to read the first 8 bytes to get the header size (see my ASCII art above). Let's get the header size then:
var header_size_buf: [HEADER_SIZE_BUFF_SIZE]u8 = undefined;
_ = try file.read(header_size_buf[0..]);
// Another thing to mind is whether to interpret this as a litle/big endian.
// I could not quickly find what it was, and just tried both.
// The big endian did not make any sense to me, and I later check the parity with Python anyways.
const header_size = std.mem.readInt(u64, &header_size_buf, std.builtin.Endian.little);
Now, we can read the header!
// This is where knowing the header size comes in handy.
const header_buf = try allocator.alloc(u8, header_size);
defer allocator.free(header_buf);
_ = try file.read(header_buf[0..]);
Parsing the header was the hardest part, or, let's say, iterating over it, and putting the correct data to our structs. My solution is a bit hacky and might break with new data types, but it was good enough for a working prototype.
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, header_buf, .{});
defer parsed.deinit();
var iter = parsed.value.object.iterator();
while (iter.next()) |entry| {
    const key = entry.key_ptr;
    // The JSON has some other stuff in it.
    // Skip metadata, we need only layers.
    if (std.mem.eql(u8, key.*, "__metadata__")) {
        continue;
    }

    const val = entry.value_ptr;
    // grab info about the weights dtype
    const dtype = val.object.get("dtype").?.string;

    const unk_shape = val.object.get("shape").?.array;
    const shape = try allocator.alloc(u64, unk_shape.items.len);
    // I forgot to add the next line in the first version of this code, and GeneralPurposeAllocator
    // helped me to find it.
    defer allocator.free(shape);
    // Shape is an array of integers, as we do not know the amount of items,
    // we need to allocate it explicitly on the heap.
    // We can probably have a max number and store the shape dims as well, but I took this path.
    for (unk_shape.items, 0..) |el, idx| {
        switch (el) {
            .integer => |num| {
                const signless: u64 = @as(u64, @intCast(num));
                shape[idx] = @intCast(signless);
            },
            else => {},
        }
    }

    // With offsets, we actually know that there is only start and end.
    // Let's exploit this and store those as separate fields in the struct.
    // const offset_start: u64 = -1;
    // const offset_end: u64 = -1;
    const unk_offsets = val.object.get("data_offsets").?.array;
    const offset_start: u64 = @as(u64, @intCast(unk_offsets.items[0].integer));
    const offset_end: u64 = @as(u64, @intCast(unk_offsets.items[1].integer));
    const cur_layer = try LayerMetadata().init(
        allocator,
        key.*[0..key.*.len],
        shape,
        dtype,
        offset_start,
        offset_end,
    );
    try layers_info.append(cur_layer);
}
That's pretty much it, we have everything to load the weights themselves.
const metadata_bytesize = HEADER_SIZE_BUFF_SIZE + header_size;
const read_len = layer_metadata.offset_end - layer_metadata.offset_start;

// Weight offsets are starting with 0 meaning the first byte *after* the header.
try file.seekTo(layer_metadata.offset_start + metadata_bytesize);
const wbuf = try allocator.alloc(u8, read_len);
const bytes_read = try file.read(wbuf);
if (bytes_read != read_len) {
    std.debug.print("Something is wrong! Expected bytes to read: {}. Actual bytes read:{}.]\n", .{ read_len, bytes_read });
    std.process.exit(1);
}
defer allocator.free(wbuf);

// Here I hardcode that we are in bf16. 
// This is not true for all, and has to be generalised.
const bf16_count: usize = read_len / 2;

const rows = layer_metadata.shape[0];
const cols = layer_metadata.shape[1];
// Again, here we assume that we have a two dimensional array.
// Check that the shape corresponds to amount of bytes we read.
std.debug.assert(rows * cols == bf16_count);

// Original weights are in bf16, let's get fp32 from those.
var f32_values = try allocator.alloc(f32, bf16_count);
defer allocator.free(f32_values);
batch_bf16bytes_to_fp32(wbuf, bf16_count, f32_values);

// Let's get the 2D array printed to compare to what we see in Python (run test.py to compare).
var weights = try NDArray(f32).init(allocator, rows, cols);
weights.copy_from(&f32_values);
The only missing bit here, before we wrap up is how we convert bf16 to fp32. We are actually pretty lucky here! Both have the same sign bit and the exponent length! We can just pad the missing half with zeros from the right:
pub fn batch_bf16bytes_to_fp32(bf16_buf: []u8, bf16_count: usize, fp32_buf: []f32) void {
    const bf16_ptr = @as([*]u16, @ptrCast(@alignCast(bf16_buf.ptr)));
    const bf16_slice = bf16_ptr[0..bf16_count];
    const shift_width: u32 = 16;
    for (bf16_slice, 0..) |bf, i| {
        // Showing my incredible bit arithmetics skills here.
        const bits: u32 = @as(u32, bf) << shift_width;
        fp32_buf[i] = @bitCast(bits);
    }
}

And that's it! A self-contained Zig program to parse safetensors. There's definitely room for improvement, but it works! I'll probably make it a bit more general in the future, or you can send me a PR! You can find the code here.


]]>
Welcome to yobihomehttps://yobibyte.github.io/vibecoding.htmlhttps://yobibyte.github.io/vibecoding.htmlFri, 25 Apr 2025 09:34:47 +0000thoughts on vibe coding

Vibe coding took our industry by storm. Some people I know and respect went completely nuts about it and switched from designing software by the means of reasoning to begging their computer to do what they want. Some of them still check what LLMs outputs, but others just blindly accept the code, and only skim through it when it does not run or outputs completely wrong results. For me, this sounds exactly like the Tesla autopilot: you are expected to keep your hands on the wheel and pay attention, but you have few incentives to do that, and many people just do not and stare into their phones.

Most of the people who preach vibe coding think that the sole output of programming is the running code. That is a big mistake. When you program in the usual, non-perverse, way you learn a lot. Occasionally, you jump to the libraries' source code, you read documentation, you lurk on forums and encounter insightful responses, you learn a lot! You also hit you head against the wall multiple times, and you come out stronger from it, you learn to persevere and have to build a mental model of the reality to solve a complex problem. Vibe coding is like cheating on an exam. You solved the problem, but you have not learnt a thing.

Normal-way programming is sometimes similar to solving puzzles: you collect evidence, you have your suspects, and you design experiments to confirm your hypotheses. You feel great when you finally solve your mystery, you have achieved something. With vibe coding, I do not have this feeling, you just open the 'Answers' section of the book (which is full of typos).

With vibe coding, you also bang your head against the wall trying to convince an LLM to fix your bug, but in the end, you come out of this without learning much, you have some cargo-cult thoughts about better prompting, but they are a sandcastle. Instead of learning new things, you start forgetting old things: language syntax. Without knowing syntax, your flow is constantly interrupted as you need to google more, or you are even more dependent on your LLM. Try coding on a plane without the Internet now, I am pretty sure you will suffer a lot.

The models will definitely improve, but right now I do not think that the models are good enough to solve the problems I care about. I tried to vibe code some proc macros in Rust, and it was terrible. I tried do some basic operations with Polars in Rust, and it was incredibly dumb, less than useless. Even in-context learning which is considered the modern-day ML miracle did not work that well for me. I was trying to give the model the library API changes and it could not get it. It kept spitting out the old library version's code without paying any attention (pun intented) on what I was telling it.

I enjoy writing code, I enjoy building something that I understand and that is of high quality. With vibe coding, part of the activity I enjoy goes away. You end up doing two things: convincing a machine to do what you want in natural language, and then cleaning up after it. You turn into an unlucky manager who gives tasks to a junior developer, and then cleans up the mess he gets. Your technical skills deteriorate, you depend more and more on the machine, you start hating yourself.

Hey, aren't you a luddite, you might ask? Yeah, I kinda am: I don't use jupyter notebooks, I am horrified by the quality of the modern software, and generally think that our field has gone the wrong way at some point. I even stopped using a smartphone a couple of months ago. But I don't think I am completely off my onion. I want to preserve my intellectual capabilites and use computers to help me create stuff by empowering me rather than turning me into a clip buffer between my browser and the rest of my computer.

I train LLMs and use them daily, but mostly for explaining stuff I do not understand. I do not know Rust that well and often I ask LLMs what a couple of lines actually mean or what the syntax for a particular idiomatic expression is. I often copypaste scientific papers' content to ChatGPT and go over the derivation I do not get. With this approach, LLMs empower me, they do not make me dumber.


]]>
Vim Setuphttps://yobibyte.github.io/vim.htmlhttps://yobibyte.github.io/vim.htmlSat, 5 Jul 2025 10:30:43 +0000why I got rid of all my neovim plugins

Let's jump straight into it. This is the neovim config I write all my code with. It is just eleven lines, two key bindings, and it does not use any plugins. In this post, I will shortly describe my vim journey, and explain why I ended up with this config.


vim.o.undofile      = true
vim.o.clipboard     = "unnamedplus"
vim.o.laststatus    = 0
vim.opt.expandtab   = true
vim.opt.shiftwidth  = 4
vim.opt.softtabstop = -1
vim.cmd("syntax off | colorscheme retrobox | highlight Normal guifg=#ffaf00 guibg=#282828")
vim.keymap.set('n', '<space>y', function() vim.fn.setreg('+', vim.fn.expand('%:p')) end)
vim.keymap.set("n", "<space>c", function() vim.ui.input({}, function(c) if c and c~="" then 
  vim.cmd("noswapfile vnew") vim.bo.buftype = "nofile" vim.bo.bufhidden = "wipe"
  vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.fn.systemlist(c)) end end) end)

Me and vim

I first started using vim in high school because I could not quit it. It might have even been vi, and I opened it by accident. I started using it to edit my configuration files or write simple python scripts. For everything else I used Eclipse, and, then, JetBrains IDEs.

A couple of years ago I learnt about neovim and kickstart.nvim, and was amazed by what you can turn vim into. I loved kickstart because it gave you a fully working config that you could poke into, study, and learn more. I loved that everything was in a single file, and it was simple. I did learn a lot, and I also found out that there is stuff in there I do not need. A thousand lines of configuration was too much, even though most of the lines were comments. I started removing comments to trim the config down, I started removing plugins I did not use.

A couple of months ago, I watched Prime's interview with Ginger Bill who said he does not use LSPs, and this helps him build a better mental model of the code he works with. This idea resonated with me, and I decided to give it ago. Moreover, LSPs were the most brittle part of my neovim setup, they often crashed, and I get angry when the software I use does not work. I want to trust my software.

Getting rid of LSPs significantly simplified my config. I started replacing the plugins I used with self-written custom functions, e.g. using grep/find to replace telescope, going over the first 100 lines to figure out the shiftwidth etc. Eventually, I was left only with a couple of plugins, and, I thought that I do not need the plugin manager now, I can just check out the repos myself from init.lua. And then I realised that neovim had commenting out of the box, and I need neither Comment.nvim nor my own custom function to comment out stuff. That was it, I got rid of all my plugins, and I got about 200 lines of custom functions that made my life easier.

Slowly, I started realising, that I can live without much of the functionality of my init.lua. Moreover, similarly to LSPs, that functionality made me less efficient. It prevented me from getting better. And this is how I ended up with 11 lines of configuration which I can even retype if I ever need to work on a machine with vim but without access to the Internet. In hindsight, I found the three ideas that brought me to these 11 lines, and I will elaborate on those below.

Simplicity

I love simplicity. I have a fucking PhD thesis in simplicity. The fewer lines of config you have, the less you should worry about. The simpler your config is, the less it will break, and, usually, the faster it will be. The more plugins you use, the more lines of code you download from the Internet, and every line of code in a random plugin can fuck your computer up. The simpler your system is, the bigger part of it you understand. The simpler your setup is, the easier it is for you to work with the stock tools.

I do not think there is much more to it.

Magic

A lot of people are trying to turn vim into an IDE. I do the opposite. I think, text editor should help you edit text, it should not replace your terminal, web browser, and a gaming console, sorry Emacs users. What I think a good editor should do is to provide a seamless integration with the terminal so that you could grep/find/build your project and direct the output to your editor without any hassle.

I have this seamless integration by having a simple custom binding that creates a scratch buffer, runs a command, and sends the output to that scratch buffer:


vim.keymap.set("n", "<space>c", function()
  vim.ui.input({}, function(c) 
      if c and c~="" then 
        vim.cmd("noswapfile vnew") 
        vim.bo.buftype = "nofile"
        vim.bo.bufhidden = "wipe"
        vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.fn.systemlist(c))
      end 
  end) 
end)
I can even get rid of this binding and just use ":vnew | read !cmd", but I do not want to manually close that buffer.

Why do I not want my editor to be an IDE? Relying on magic makes me a worse programmer, it makes me lazy and forgetful. It makes me less resilient and increases my dependencies on others. I will give a couple examples.

I was a happy user of the fugitive plugin. It makes it so easy to stage/reset chunks you want, you can see all the changes easily, and you can do walk over commits, see the history, check the blame without any problems. However, you have no idea how it works. I use git cli interface now, and I have learnt so many new commands recently! I know a command to track the evolution of a single line in the repo: git log -L 42,42:file.rs. If I get a merge conflict, I load all the conflicting files into a buffer by git diff --name-only --diff-filter=U. I could use a binding for that, but I want to memorize the flags. Does this make me a bit slower? I think so. But it also makes me learn more when I work, and producing more lines of code is not my goal when programming.

But, Vitaly, you might say, how the fuck do you navigate around the codebase, how do you search, how do you live without Harpoon??? Simple, I replaced Telescope with just grepping stuff, or ripgrepping, if a system I work in has ripgrep. I run <space>c, and I type rg --vimgrep TODO to get a buffer filled in by the todos. I can type :cbufer to convert this to a quickfix list for easier navigation with :cn and :cp. I open my files by typing :e for the files I edit often. This makes me more aware of the project structure. If I want to search for a file, I just run rg --files | rg filename to get it to the buffer. Then it is a one-line yank and :e PASTE. I use bookmarks instead of Harpoon, or I type :b file to move. If the buffer list is not polluted :b file (you do not have to type the full path or full name), this can be extremely efficient.

Going to definition of external dependencies was the hardest to figure out. I ended up having a binding that gives me a filepath where python or rust keep the dependencies and grepping there. Now I remember the paths (or the way python/cargo make this path), and just cd there, fire vim or just grep directly in the terminal. This is definitely slower than going to definition with one key bind, but this makes me aware of the structure of external dependencies, every time I grep, I see the context around, I can read a couple of lines up or down, I might run tree inside. Again, not magically teleporting to the place I need, makes me learn more. I trade immediate efficiency for learning more in the long-term. I am an anti-vibe coder.

Distractions

The last thing I love my setup for is lack of distrations. When I see people coding in IDE, I feel like staring through a gunhole into their code. They buy the latest macbook pro with 24k resolution to see 3 lines and 5 columns of code surrounded by hundreds of buttons, 25 error messages, and popups with the latest update on the weather around them. I personally cannot deal with it, I get overwhelmed and angry by all these distractions. I want a clean canvas as static as possible, I want it to calm me down, not shout in my face.

This was the first thing I loved about not having LSPs: no autocomplete popups, no diagnostic messages saying the variable is not defined (of course is not, I haven't finished typing the name yet), no messages about rust analyzer crashing. This felt like a sea breeze and waves crashing onto the rocks you are standing on. This was freedom. And this was coherent with the rest of my setup: i3 with zero animations, no window bars, maximum vertical space, and zero notifications.

I do not have line numbers on, I do not need to see them to go on a line that compiler tells me about. I do not have a statusline on, I can press Ctrl-g to get the filename. I do not have the syntax highlighting, this goes to the same bucket. Not doing fuzzy find with Telescope frees me from seeing hundreds of lines changing while I still type. I can finish typing the command and get a static list of results aftewards.


I understand, that my setup is not for everyone. I am not trying to convince anyone that my setup is better. I just want to demonstrate how powerful tools can make your setup work for you, and encourage you question things that are considered to be ultimate truth in the community.


]]>
Welcome to yobihomehttps://yobibyte.github.io/kitty.htmlhttps://yobibyte.github.io/kitty.htmlSun, 24 Aug 2025 09:40:10 +0000kitty productivity setup

I have used tmux for ages. Even though I am using i3, I still use tmux locally, because I want to copy text from my terminal without using my mouse.

How do you copy text in tmux? You first go to the select mode with ctrl+B,[, press space to start selection and select with vim motions (if you have the vi bindings on). It improves my life, but it has always felt very clunky and slow to me. But now I found a better option!

Kitty terminal emulator has a ctrl+shift+g shortcut to show the output of the last command. By default, it uses less to show it, but you can override the pager! I am overriding mine to open a neovim window with it, btw. I also added another shortcut for ctrl+shift+s to copy the output of the last command to my clipboard. This is useful when I want to get a full path of some dataset, and add it to a script or invoke another command with it.

If you want the niceties I have just decsribed, just add these two lines to your ~/.config/kitty/kitty.conf


map ctrl+shift+g launch --stdin-source=@last_cmd_output --type=overlay nvim
map ctrl+shift+s launch --stdin-source=@last_cmd_output --type=clipboard
You are welcome!
]]>
self-reliant programmer manifestohttps://yobibyte.github.io/self_reliant_programmer.htmlhttps://yobibyte.github.io/self_reliant_programmer.htmlSat, 20 Sep 2025 19:30:58 +0000self-reliant programmer manifesto

Most of the modern software is a disgrace to its users. Most of the modern software is getting worse every day. Relying on this software is like relying on a cheeto to keep your house safe from burglars instead of using a proper lock. Fuck this. Enough! It is time to become more self-reliant. A self-reliant programmer beleives that simple is good, minimises their dependencies, and writes their own tools.

Most of the modern software has more features than anybody uses, you only need a subset of those. You can often implement a subset of features for a program you need. You do not need fourty-two layers of abstractions to implement something simple. You can just implement it.

Everything complex is a sum of simple stuff. You just need to get started. Curl started as 300 lines of C. Reimplementing just the part you need, without catering to others, makes it simple and possible to implement yourself. You can be a lot more self-reliant.

A simpler program has fewer dependencies, less code, fewer bugs and smaller attack surface. Fewer dependencies means being less vulnerable to supply-chain attack on package managers. Simpler code means better understanding what you actually use, and how it works. Implementing something yourself makes you understand it even better. Understanding how stuff works helps you build better mental models of the reality and you become more capable. Being more capable means being more self-reliant, and being more free.

Simpler tools mean that you can work alone, making your software even simpler. You do not depend on bloated CI, Docker, Kubernetes, a countless web-based tools that break half of the time. You can focus on programming and solving your actual problems. You do not need to argue with others about decisions and use the workflows you do not like. You just sit down and solve your problem. You become even more self-reliant.

Being self-reliant makes you more responsible for your own destiny, you do not wait for others to magically do things for you, you have to do this yourself. There is no backup plan, you have to become better or use shitty software. But you won't be able to blame anyone, you are to blame in this case. Being capable and self-reliant and using simpler tools means you do not need anyone's blessing to do whaterver you need. You can just sit down and code stuff without asking ChatGPT to do this for you or fix something when it does not work. You do not need the Internet to remind you how to make a for loop in your favourite language or to download a hundred dependencies for your project. You have more agency to change environment around you. You can just do stuff.


]]>
Writing RSS reader in 80 lines of bashhttps://yobibyte.github.io/yr.htmlhttps://yobibyte.github.io/yr.htmlWed, 4 Feb 2026 09:44:56 +0000Writing an RSS reader in 80 lines of bash

I consume most of my information through RSS. RSS is amazing. Up until yesterday, I used Newsboat to go through my feed, and had a ,a macro that appended a link from the item to a text file with links. Every morning, I would go through my whole feed, and get a bunch of links appended to my links.md file. After that, I start reading them by copying one line at a time, and running the script that dumps the content of the link via w3m and opens a vim buffer with it. If it is a large read, I would save it as a txt file and send it to my e-reader.

Recently, I started reducing my dependencies on external software. Yesterday, I thought, why not write an RSS reader myself. It will not be as powerful and robust as my current one, but it will be simpler, more hackable, and mine, i.e. I will not be dependent on a stranger's commit that might break my system accidentally or on purpose.

First, I wanted to write it in Zig, as this is the language I have been recently playing with and want to get better in. But then I thought that omnipresent Unix utils like curl, grep, awk, sed will be enough to quickly hack a simple script. I had the following idea. Write a bash script that will curl a given feed, extract the title/link/description from each item, apply filters on each of the fields above (e.g. I do not want to get hackernews posts on TypeScript), and append the results to a file. Later, I will run this script with xargs in parallel to quickly get all the feeds downloaded.

This is, basically, how it works:


cat $HOME/notes/urls.txt | xargs --verbose -P $THREADS -I {} bash -c '~/dev/yr/yr_feed "{}" | flock -x $HOME/dev/yr/new.txt -c "cat >> $HOME/dev/yr/new.txt"'
I added flock on the file append at the end to make sure that the writes from multiple threads do not get interleaved.

Let's see what do I do with each of the feeds now!


url="$1"
mapfile -t items < <(curl -s "$url" | awk 'BEGIN{RS=""; ORS=""} NR>0 {
    gsub(/\n/," ");       # replace newlines with spaces
    gsub(/[[:space:]]+/," "); # collapse multiple spaces
    print $0 RS "\n"
}' | head -n $MAX_ITEMS)
Here, we download the feed, and split the received xml by item/entry tags using awk. We limit the number of items to process (often the feed items are just appended to the file, and we do not want to go over a 1000-item list every time). We also remove newline symbols within each item as I will use grep to parse elements of items later.

We are now ready for a for loop over the items array, where we parse the content and decide whether to append it to our reading list.


title=$(echo "$item" | grep -oP ']*>\K.*?(?=)')
hash=$(echo -n "${link1}${link2}${title}" | md5sum | awk '{print $1}')
We first find the two main elements of the feed item: its title and its url. Others use <link href=""> style, also potentially with attributes. Title is more or less standard. I will probably benefit from reading the RSS format specs, but I did not yet have time to look those up.

After that, I use those to compute a hash of an item. We need this hash to understand whether we have already downloaded the item or not. The most important thing here is not to use content to compute the hash. Some of the websites, e.g. hackernews, put the number of comments on a post into the content. This makes the same thing appear in my reading list multiple times. We do not want that. This potentially misses updates for pages that add content gradually. I will need to deal with this somehow in the future iteration of the script.


if ! grep -Fxq "$hash" "$DOWNLOADED_PATH"; then
    content1=$(echo "$item" | grep -oP ']*>\K.*?(?=)' | cut -c1-$MAX_CHARS | w3m -T text/html -dump)
    content2=$(echo "$item" | grep -oP ']*>\K.*?(?=)' | cut -c1-$MAX_CHARS | w3m -T text/html -dump)
    [[ -n $link1 ]] && [[ $(match_against_filters "$link1" "url") -eq 1 ]] && continue
    [[ -n $link2 ]] && [[ $(match_against_filters "$link2" "url") -eq 1 ]] && continue
    [[ -n $title ]] && [[ $(match_against_filters "$title" "title") -eq 1 ]] && continue
    [[ -n $content1 ]] && [[ $(match_against_filters "$content1" "content") -eq 1 ]] && continue
    [[ -n $content2 ]] && [[ $(match_against_filters "$content2" "content") -eq 1 ]] && continue

    echo "--- $1"
    [[ -n $link1 ]] && echo "$link1"
    [[ -n $link2 ]] && echo "$link2"
    [[ -n $title ]] && echo "$title"
    [[ -n $content1 ]] && echo "$content1"
    [[ -n $content2 ]] && echo "$content2"
    echo

    new_hashes+=$'\n'"$hash"

fi;
We are now ready to parse the content and apply our filters! I have been a heavy user of filters in Newsboat, carefully building my own information bubble protecting myself from the information overdose. Again, content can be a description/summary tag, and we check both. After that, we check if any of the fields match the list of filters we have in a separate file. If they do, we go to the next item on the list, otherwise, we print out the item title/url/content and append the item to the list of hashes we do not want to download again.


match_against_filters() {
    local link="$1"
    local type="$2"

    while IFS=',' read -r feed type regex; do
    if ! echo "$link" | grep -Eq "$regex"; then
        return 1
    fi
    done < <(grep "^\*,${type}" "$FILTERS_PATH")

    while IFS=',' read -r feed type regex; do
    if ! echo "$link" | grep -Eq "$regex"; then
        return 1
    fi
    done < <(grep "${url},${type}" "$FILTERS_PATH")

    return 0 
}
The matching is pretty simple. We can have three fields to match (url, title, and content), and two rules to define whether we want to match a line or not. The * symbol means that a filter should be applied to every item. E.g, I have one that filters out all medium/substack links *,url,"medium|substack". Another one blocks youtube shorts videos: *,url,".*youtube.com/.*shorts" If we use a feed url instead of the star symbol, the filter will only be applied to a particular feed. I use this primarily to filter out a list of arxiv.org submissions I go over every morning.

Short disclaimer before we continue: I haven't thorougly tested the filter system yet, there are possible bugs in there. Work in progress, tbd.

Finally, when we have processed all the items, we append the list of downloaded hashes to the downloaded.txt file. Again, there is a file lock on the append operation as this script will be used in parallel for multiple feeds.


{
  flock 9
  printf "%s" "$new_hashes" >&9
} 9>>"$DOWNLOADED_PATH"

That's it, really! You can have a simple RSS reader only depending on core Unix utils that are everywhere. Sometimes, the output will be ugly, it will not work for some of the feeds due to naive parsing. But it can be improved, it was fun to build, and it is pretty usable. Hope it was fun. Here is the full code in case you want to play with it.


#!/bin/bash

MAX_ITEMS=150
MAX_CHARS=1000
DOWNLOADED_PATH="$HOME/dev/yr/downloaded.txt"
FILTERS_PATH="$HOME/dev/yr/filters.txt"
touch "$DOWNLOADED_PATH"

# filters syntax: URL,type,REGEX
# if * is used for URL, then it is applied to all urls.
# types: title/url/content
# grep will do negative match! E.g. if I hit a find, I will skip an item.

new_hashes=""

match_against_filters() {
    local link="$1"
    local type="$2"

    while IFS=',' read -r feed type regex; do
    if ! echo "$link" | grep -Eq "$regex"; then
        return 1
    fi
    done < <(grep "^\*,${type}" "$FILTERS_PATH")

    while IFS=',' read -r feed type regex; do
    if ! echo "$link" | grep -Eq "$regex"; then
        return 1
    fi
    done < <(grep "${url},${type}" "$FILTERS_PATH")

    return 0 
}

url="$1"
mapfile -t items < <(curl -s "$url" | awk 'BEGIN{RS=""; ORS=""} NR>0 {
    gsub(/\n/," ");       # replace newlines with spaces
    gsub(/[[:space:]]+/," "); # collapse multiple spaces
    print $0 RS "\n"
}' | head -n $MAX_ITEMS)

for item in "${items[@]}"; do
    title=$(echo "$item" | grep -oP ']*>\K.*?(?=)')

    hash=$(echo -n "${link1}${link2}${title}" | md5sum | awk '{print $1}')
    if ! grep -Fxq "$hash" "$DOWNLOADED_PATH"; then

        content1=$(echo "$item" | grep -oP ']*>\K.*?(?=)' | cut -c1-$MAX_CHARS | w3m -T text/html -dump)
        content2=$(echo "$item" | grep -oP ']*>\K.*?(?=)' | cut -c1-$MAX_CHARS | w3m -T text/html -dump)
        [[ -n $link1 ]] && [[ $(match_against_filters "$link1" "url") -eq 1 ]] && continue
        [[ -n $link2 ]] && [[ $(match_against_filters "$link2" "url") -eq 1 ]] && continue
        [[ -n $title ]] && [[ $(match_against_filters "$title" "title") -eq 1 ]] && continue
        [[ -n $content1 ]] && [[ $(match_against_filters "$content1" "content") -eq 1 ]] && continue
        [[ -n $content2 ]] && [[ $(match_against_filters "$content2" "content") -eq 1 ]] && continue

        echo "--- $1"
        [[ -n $link1 ]] && echo "$link1"
        [[ -n $link2 ]] && echo "$link2"
        [[ -n $title ]] && echo "$title"
        [[ -n $content1 ]] && echo "$content1"
        [[ -n $content2 ]] && echo "$content2"
        echo

        new_hashes+=$'\n'"$hash"

    fi;
done

# TODO think on how to clean that file later
{
  flock 9
  printf "%s" "$new_hashes" >&9
} 9>>"$DOWNLOADED_PATH"


]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.htmlWed, 25 Feb 2026 16:19:12 +0000What's up?

Wed Feb 25 16:15:22 GMT 2026

Started a new workflow experiment. By default, I work for TTY without X11. If I need a browser or pdf reader, I press ctrl+shift+2 to get X11 and switch back after I'm done.


]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.htmlWed, 25 Feb 2026 18:31:47 +0000What's up?

Wed Feb 25 18:30:40 GMT 2026

I managed to make fbpdf work from a TTY. No need in X to read pdfs anymore.

Wed Feb 25 16:15:22 GMT 2026

Started a new workflow experiment. By default, I work for TTY without X11. If I need a browser or pdf reader, I press ctrl+shift+2 to get X11 and switch back after I'm done.


]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Wed, 25 Feb 2026 18:57:59 +0000 What's up?

Wed Feb 25 18:57:59 GMT 2026

Testing the tweet script to send RSS updates with a single-item only.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Wed, 25 Feb 2026 20:23:59 +0000 What's up?

Wed Feb 25 20:23:59 GMT 2026

Amazingly written! Slow death of the power user.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Wed, 25 Feb 2026 21:24:57 +0000 What's up?

Wed Feb 25 21:24:57 GMT 2026

This webpage is powered with a single bash script that takes the text from a tweet.txt file, and updates html/feed.xml files with its content/timestamp.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Thu, 26 Feb 2026 09:41:54 +0000 What's up?

Thu Feb 26 09:41:54 GMT 2026

With high DPI screens, TTY fontsize is usually tiny. A simple way to increase your fontsize is to use -d flag with setfont.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Fri, 27 Feb 2026 21:51:08 +0000 What's up?

Fri Feb 27 21:51:08 GMT 2026

A veritasium video on the XZ backdoor.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Tue, 03 Mar 2026 19:59:38 +0000 What's up?

Tue Mar 3 19:59:38 GMT 2026

Github is down so often, it's prob more stable to self-host a git server now.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Sun, 08 Mar 2026 10:37:55 +0000 What's up?

Sun Mar 8 10:37:55 GMT 2026

I installed OpenBSD on my old laptop to use it is a dev machine that does not have a display server, and is not connected to the internet. You do all the development in a no-distraction environment as if you are on a flight, and at the end of the day, you connect to the internet, upload the code to a git server and you are done for the day.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Sun, 08 Mar 2026 12:34:02 +0000 What's up?

Sun Mar 8 12:34:02 GMT 2026

This post is done from OpenBSD. Hello world.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Sun, 08 Mar 2026 17:54:19 +0000 What's up?

Sun Mar 8 17:54:19 GMT 2026

Added a page describing setting up OpenBSD: link.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Tue, 10 Mar 2026 11:12:09 +0000 What's up?

Tue Mar 10 11:12:09 GMT 2026

Registered at SourceHut. Will be moving most of my repos over there soon.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Tue, 10 Mar 2026 22:35:50 +0000 What's up?

Tue Mar 10 22:35:50 GMT 2026

I'm amazed by how great and fast sourcehut's interface is. And it is surprisingly usable via w3m too!

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Sat, 14 Mar 2026 11:43:08 +0000 What's up?

Sat Mar 14 11:43:08 GMT 2026

Fun fact:

You know how Ken Thompson and Dennis Ritchie created Unix on a PDP-7 in 1969?
Well around 1971 they upgraded to a PDP-11 with a pair of RK05 disk packs (1.5
megabytes each) for storage.

When the operating system grew too big to fit on the first RK05 disk pack (their
root filesystem) they let it leak into the second one, which is where all the
user home directories lived (which is why the mount was called /usr).  They
replicated all the OS directories under there (/bin, /sbin, /lib, /tmp...) and
wrote files to those new directories because their original disk was out of
space.  When they got a third disk, they mounted it on /home and relocated all
the user directories to there so the OS could consume all the space on both
disks and grow to THREE WHOLE MEGABYTES (ooooh!).

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Mon, 16 Mar 2026 13:00:23 +0000 What's up?

Mon Mar 16 13:00:23 GMT 2026

Working on a laptop without internet connection and X11 will make you a 10xer. Wrote four pages of the DPhil Grind yesterday.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Wed, 18 Mar 2026 16:07:08 +0000 What's up?

Wed Mar 18 16:07:08 GMT 2026

For some reason, mbsync + hydroxide for protonmail is extremely slow on OpenBSD. It takes minutes to get the Archive dir to sync. If I could make this work, I can daily-drive OpenBSD.

]]>
What's up?https://yobibyte.github.io/whatsup.htmlhttps://yobibyte.github.io/whatsup.html Wed, 18 Mar 2026 23:00:55 +0000 What's up?

Wed Mar 18 23:00:55 GMT 2026

Implemented a simple clone of tree just for fun. 96 lines of C, with 0 dependencies. That was quite rewarding.

]]>