Changes
8 changed files (+405/-3)
-
-
@@ -58,6 +58,9 @@ - [niri](https://github.com/YaLTeR/niri)- [`pantheon-polkit-agent`](https://archlinux.org/packages/extra/x86_64/pantheon-polkit-agent/) ... The one installed installed using Nix cannot lookup `polkit-agent-helper-1`. - [swaylock](https://github.com/swaywm/swaylock) ... access to PAM required, which is not possible with regular user Nix installation. A custom Swaybar module assumes Markdown file placed at `$XDG_DATA_HOME/todo.md`. Create a symbolic link or normal file there (module tries to read in 10s interval if the file does not exist.) ## Programs ### `hm-clean`
-
-
-
@@ -14,8 +14,13 @@ ## SPDX-License-Identifier: 0BSD { config, lib, pkgs, ... }: { programs = lib.mkIf config.features.wayland-de.enable { config = lib.mkIf config.features.wayland-de.enable { home.packages = [ # /programs/waybar-text pkgs.my-waybar-text ]; programs = { waybar = { enable = true;
-
@@ -23,7 +28,7 @@ settings = {main = { layer = "top"; modules-left = [ ]; modules-left = [ "custom/todo" ]; modules-right = [ "clock" "pulseaudio" "tray" ]; clock = {
-
@@ -39,9 +44,16 @@ format-muted = "muted {format_source}";on-click = "pavucontrol"; }; "custom/todo" = { exec = "${pkgs.my-waybar-text}/bin/,waybar-text --trim-md-list ${config.xdg.dataHome}/todo.md"; restart-interval = 10; return-type = "json"; }; }; }; }; }; }; }
-
-
-
@@ -68,6 +68,7 @@ (import ./overlays/legit.nix)# Local packages (final: prev: { my-theme = prev.callPackage ./programs/theme { }; my-waybar-text = prev.callPackage ./programs/waybar-text { }; }) ]; };
-
-
-
@@ -0,0 +1,43 @@// Copyright 2025 Shota FUJI <pockawoooh@gmail.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ .name = ",waybar-text", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); b.installArtifact(exe); // zig build run { const step = b.step("run", "Compile and run program"); const run = b.addRunArtifact(exe); if (b.args) |args| { run.addArgs(args); } step.dependOn(&run.step); } }
-
-
-
@@ -0,0 +1,31 @@# Copyright 2025 Shota FUJI <pockawoooh@gmail.com> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 { stdenvNoCC, zig }: stdenvNoCC.mkDerivation rec { pname = "my-waybar-text"; version = "1.0.0"; nativeBuildInputs = [ zig.hook ]; src = ./.; meta = { mainProgram = ",waybar-text"; }; dontConfigure = true; }
-
-
-
@@ -0,0 +1,114 @@// Copyright 2025 Shota FUJI <pockawoooh@gmail.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); allocator: std.mem.Allocator, file_path: []const u8, class: ?[]const u8 = null, trim_markdown_list_prefix: bool = false, pub const InitError = error{ IncorrectUsage, ShowHelp, } || std.mem.Allocator.Error || std.fs.Dir.RealPathAllocError; const class_arg_prefix = "--class="; pub const helpText = \\,waybar-text - Watches text file and print its first line \\ \\[USAGE] \\,waybar-text [--class=string] <FILE> \\ \\[OPTIONS] \\--help Print this message to stdout and exits. \\--class=string Set CSS class for the output text. \\--trim-md-list Trim markdown list prefixes from displaying text. \\ ; pub fn init(allocator: std.mem.Allocator) InitError!@This() { var iter = try std.process.ArgIterator.initWithAllocator(allocator); defer iter.deinit(); // Skip program name. _ = iter.next(); var first_positional_arg: ?[]const u8 = null; var class: ?[]const u8 = null; errdefer if (class) |slice| allocator.free(slice); var trim_markdown_list_prefix: bool = false; while (iter.next()) |arg| { if (std.mem.eql(u8, arg, "--help")) { return InitError.ShowHelp; } if (std.mem.startsWith(u8, arg, class_arg_prefix)) { if (class) |_| { std.log.err("--class can't be used more than once", .{}); return InitError.IncorrectUsage; } class = try allocator.dupe(u8, arg[class_arg_prefix.len..]); if (class) |c| if (c.len == 0) { std.log.err("--class requires a value", .{}); return InitError.IncorrectUsage; }; continue; } if (std.mem.eql(u8, arg, "--trim-md-list")) { trim_markdown_list_prefix = true; continue; } if (std.mem.startsWith(u8, arg, "-")) { std.log.err("Unknown option: {s}", .{arg}); return InitError.IncorrectUsage; } if (first_positional_arg) |_| { std.log.err("FILE can't be used more than once", .{}); return InitError.IncorrectUsage; } first_positional_arg = arg; } const cwd = std.fs.cwd(); const file = try cwd.realpathAlloc(allocator, first_positional_arg orelse { std.log.err("FILE is required", .{}); return InitError.IncorrectUsage; }); return .{ .allocator = allocator, .file_path = file, .class = class, .trim_markdown_list_prefix = trim_markdown_list_prefix, }; } pub fn deinit(self: *const @This()) void { self.allocator.free(self.file_path); if (self.class) |class| self.allocator.free(class); }
-
-
-
@@ -0,0 +1,160 @@// Copyright 2025 Shota FUJI <pockawoooh@gmail.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 //! Watches specified file and print its first line in waybar output JSON. const std = @import("std"); const Options = @import("./Options.zig"); const waybar = @import("./waybar.zig"); const ExitCode = enum(u8) { ok = 0, unknown_error = 1, incorrect_usage = 2, read_permission_error = 3, file_watch_error = 4, out_of_memory = 5, file_not_found = 6, pub fn to_u8(self: @This()) u8 { return @intFromEnum(self); } }; pub fn main() !u8 { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const options = Options.init(allocator) catch |err| switch (err) { error.IncorrectUsage => { try std.io.getStdErr().writeAll(Options.helpText); return ExitCode.incorrect_usage.to_u8(); }, error.ShowHelp => { try std.io.getStdOut().writeAll(Options.helpText); return ExitCode.ok.to_u8(); }, error.OutOfMemory => { std.log.err("Out of memory", .{}); return ExitCode.out_of_memory.to_u8(); }, error.FileNotFound => { std.log.err("File not found", .{}); return ExitCode.file_not_found.to_u8(); }, else => { std.log.err("Unexpected error: {s}", .{@errorName(err)}); return ExitCode.unknown_error.to_u8(); }, }; defer options.deinit(); print(allocator, &options) catch |err| switch (err) { error.OutOfMemory => { std.log.err("Out of memory", .{}); return ExitCode.out_of_memory.to_u8(); }, else => { std.log.err("Unexpected error: {s}", .{@errorName(err)}); return ExitCode.unknown_error.to_u8(); }, }; watch(allocator, &options) catch |err| switch (err) { error.FileNotFound => { std.log.err("File not found", .{}); return ExitCode.file_not_found.to_u8(); }, else => { std.log.err("Unexpected error: {s}", .{@errorName(err)}); return ExitCode.unknown_error.to_u8(); }, }; return ExitCode.ok.to_u8(); } fn print(allocator: std.mem.Allocator, opts: *const Options) !void { const file = try std.fs.openFileAbsolute(opts.file_path, .{}); defer file.close(); const line = try file.reader().readUntilDelimiterAlloc(allocator, '\n', std.math.maxInt(usize)); defer allocator.free(line); const line_trimmed: []const u8 = if (opts.trim_markdown_list_prefix) line_start: { if (line.len < 2) { break :line_start line; } break :line_start switch (line[0]) { '*', '-', '+' => line[(std.mem.indexOfNonePos(u8, line, 1, " \t") orelse 0)..], else => line, }; } else line; const output = waybar.ModuleOutput{ .text = line_trimmed, .tooltip = line_trimmed, .class = opts.class, }; const buffer = try allocator.alloc(u8, output.getEncodedSize() + 1); defer allocator.free(buffer); var fbs = std.io.fixedBufferStream(buffer); const writer = fbs.writer(); try output.encode(writer); try writer.writeByte('\n'); try std.io.getStdOut().writeAll(buffer); } const watch_flags = std.os.linux.IN.MODIFY | std.os.linux.IN.DELETE_SELF; fn watch(allocator: std.mem.Allocator, opts: *const Options) !void { const fd = try std.posix.inotify_init1(0); defer std.posix.close(fd); var wd = try std.posix.inotify_add_watch(fd, opts.file_path, watch_flags); defer std.posix.inotify_rm_watch(fd, wd); var buffer: [64]std.os.linux.inotify_event = undefined; while (true) { const read_bytes = try std.posix.read(fd, std.mem.sliceAsBytes(&buffer)); var i: usize = 0; while (i < read_bytes) : (i += buffer[i].len + @sizeOf(std.os.linux.inotify_event)) { const event = buffer[i]; // Certain text editors (I mean, vim/neovim) creates a new file and remove the old one // instead of writing to the file. We have to watch the new file otherwise no events // will be fired for the path anymore. if (event.mask & std.os.linux.IN.DELETE_SELF != 0) { wd = try std.posix.inotify_add_watch(fd, opts.file_path, watch_flags); continue; } if (event.mask & std.os.linux.IN.MODIFY != 0) { print(allocator, opts) catch |err| { std.log.warn("Unable to print: {s}", .{@errorName(err)}); }; } } } }
-
-
-
@@ -0,0 +1,38 @@// Copyright 2025 Shota FUJI <pockawoooh@gmail.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); pub const ModuleOutput = struct { text: []const u8, tooltip: ?[]const u8 = null, class: ?[]const u8 = null, const jsonStringifyOptions = std.json.StringifyOptions{ .emit_null_optional_fields = false, .whitespace = .minified, }; pub fn getEncodedSize(self: *const @This()) usize { var cw = std.io.countingWriter(std.io.null_writer); std.json.stringify(self, jsonStringifyOptions, cw.writer()) catch return 0; return cw.bytes_written; } pub fn encode(self: *const @This(), writer: anytype) @TypeOf(writer).Error!void { try std.json.stringify(self, jsonStringifyOptions, writer); } };
-