├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── README.md
├── build.zig
├── build.zig.zon
├── demo.gif
├── flake.lock
├── flake.nix
└── src
├── command.zig
├── main.zig
├── options.zig
├── pausable_timer.zig
├── signalfd.zig
├── step.zig
└── terminal.zig
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 2
3 | updates:
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly"
8 |
9 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: "CI"
2 | on:
3 | pull_request:
4 | push:
5 | permissions:
6 | contents: write
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v4
13 | - name: Install nix
14 | uses: cachix/install-nix-action@v31
15 | with:
16 | github_access_token: ${{ secrets.GITHUB_TOKEN }}
17 | - name: Build
18 | run: nix build
19 | - name: Compress
20 | if: startsWith(github.ref, 'refs/tags/')
21 | run: tar -czf pomodozig.tar.gz -C result/bin pomodozig
22 | - name: Release
23 | uses: softprops/action-gh-release@v2
24 | if: startsWith(github.ref, 'refs/tags/')
25 | with:
26 | draft: true
27 | files: pomodozig.tar.gz
28 |
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.zig-cache/
2 | /result
3 | /scratch
4 | /zig-out/
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pomodozig
2 |
3 | [![CI][status-png]][status]
4 |
5 | `pomodozig` is a simple terminal based pomodoro timer written in `Zig`. It can
6 | be used in the terminal or embedded in a status bar such as `polybar` as shown
7 | below.
8 |
9 |
10 |

11 |
12 |
13 | ## Installation
14 |
15 | A static binary is available in the [releases][releases] page. It should work
16 | on any linux distribution. For nix users, an overlay is available in [the flake
17 | file](./flake.nix).
18 |
19 | ## Usage
20 |
21 | ```
22 | $ pomodozig -h
23 | Usage: pomodozig
24 | [-t ] Task length in minutes (default: 25 mins)
25 | [-sb ] Short break length in minutes (default: 5 mins)
26 | [-n ] Number of pomodoros before long break (default: 4)
27 | [-lb ] Long break length in minutes (default: 15 mins)
28 | [-s] Disable notifications
29 | [-h] Show this help message
30 | ```
31 |
32 | ## Use in Polybar
33 |
34 | Here is an example of how to embed `pomodozig` in `polybar`:
35 |
36 | ```ini
37 | [module/pomodozig]
38 | type = custom/script
39 | label = 🍅 %output%
40 | interval = 1
41 | exec = pomodozig
42 | tail = true
43 | ; pause/resume
44 | click-left = kill -USR1 %pid%
45 | ; reset
46 | click-right = kill -USR2 %pid%
47 | ```
48 |
49 | ## Controls
50 |
51 | `pomodozig` can be controlled with the following keys:
52 |
53 | - `q` Quit
54 | - `p` Pause/Resume
55 | - `r` Reset the current task/break. Hit twice to reset the whole session.
56 |
57 | or with signals:
58 |
59 | - `SIGUSR1` Pause/Resume
60 | - `SIGUSR2` Reset the current task/break. Send twice to reset the whole
61 | session.
62 |
63 | ## Notifications
64 |
65 | `pomodozig` notifies you when a task or break is over, using `notify-send` the
66 | program shipped with `libnotify`. It can be disabled with the `-s` flag.
67 |
68 | [nix]: https://nixos.org/
69 | [releases]: https://github.com/jecaro/pomodozig/releases
70 | [status-png]: https://github.com/jecaro/pomodozig/workflows/CI/badge.svg
71 | [status]: https://github.com/jecaro/pomodozig/actions
72 |
73 |
--------------------------------------------------------------------------------
/build.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | // Although this function looks imperative, note that its job is to
4 | // declaratively construct a build graph that will be executed by an external
5 | // runner.
6 | pub fn build(b: *std.Build) void {
7 | // Standard target options allows the person running `zig build` to choose
8 | // what target to build for. Here we do not override the defaults, which
9 | // means any target is allowed, and the default is native. Other options
10 | // for restricting supported target set are available.
11 | const target = b.standardTargetOptions(.{});
12 |
13 | // Standard optimization options allow the person running `zig build` to select
14 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
15 | // set a preferred release mode, allowing the user to decide how to optimize.
16 | const optimize = b.standardOptimizeOption(.{});
17 |
18 | const exe = b.addExecutable(.{
19 | .name = "pomodozig",
20 | .root_source_file = b.path("src/main.zig"),
21 | .target = target,
22 | .optimize = optimize,
23 | });
24 |
25 | // This declares intent for the executable to be installed into the
26 | // standard location when the user invokes the "install" step (the default
27 | // step when running `zig build`).
28 | b.installArtifact(exe);
29 |
30 | // This *creates* a Run step in the build graph, to be executed when another
31 | // step is evaluated that depends on it. The next line below will establish
32 | // such a dependency.
33 | const run_cmd = b.addRunArtifact(exe);
34 |
35 | // By making the run step depend on the install step, it will be run from the
36 | // installation directory rather than directly from within the cache directory.
37 | // This is not necessary, however, if the application depends on other installed
38 | // files, this ensures they will be present and in the expected location.
39 | run_cmd.step.dependOn(b.getInstallStep());
40 |
41 | // This allows the user to pass arguments to the application in the build
42 | // command itself, like this: `zig build run -- arg1 arg2 etc`
43 | if (b.args) |args| {
44 | run_cmd.addArgs(args);
45 | }
46 |
47 | // This creates a build step. It will be visible in the `zig build --help` menu,
48 | // and can be selected like this: `zig build run`
49 | // This will evaluate the `run` step rather than the default, which is "install".
50 | const run_step = b.step("run", "Run the app");
51 | run_step.dependOn(&run_cmd.step);
52 |
53 | const exe_unit_tests = b.addTest(.{
54 | .root_source_file = b.path("src/main.zig"),
55 | .target = target,
56 | .optimize = optimize,
57 | });
58 |
59 | const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
60 |
61 | // Similar to creating the run step earlier, this exposes a `test` step to
62 | // the `zig build --help` menu, providing a way for the user to request
63 | // running the unit tests.
64 | const test_step = b.step("test", "Run unit tests");
65 | test_step.dependOn(&run_exe_unit_tests.step);
66 | }
67 |
--------------------------------------------------------------------------------
/build.zig.zon:
--------------------------------------------------------------------------------
1 | .{
2 | // This is the default name used by packages depending on this one. For
3 | // example, when a user runs `zig fetch --save `, this field is used
4 | // as the key in the `dependencies` table. Although the user can choose a
5 | // different name, most users will stick with this provided value.
6 | //
7 | // It is redundant to include "zig" in this name because it is already
8 | // within the Zig package namespace.
9 | .name = "pomodozig",
10 |
11 | // This is a [Semantic Version](https://semver.org/).
12 | // In a future version of Zig it will be used for package deduplication.
13 | .version = "0.0.0",
14 |
15 | // This field is optional.
16 | // This is currently advisory only; Zig does not yet do anything
17 | // with this value.
18 | //.minimum_zig_version = "0.11.0",
19 |
20 | // This field is optional.
21 | // Each dependency must either provide a `url` and `hash`, or a `path`.
22 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
23 | // Once all dependencies are fetched, `zig build` no longer requires
24 | // internet connectivity.
25 | .dependencies = .{
26 | // See `zig fetch --save ` for a command-line interface for adding dependencies.
27 | //.example = .{
28 | // // When updating this field to a new URL, be sure to delete the corresponding
29 | // // `hash`, otherwise you are communicating that you expect to find the old hash at
30 | // // the new URL.
31 | // .url = "https://example.com/foo.tar.gz",
32 | //
33 | // // This is computed from the file contents of the directory of files that is
34 | // // obtained after fetching `url` and applying the inclusion rules given by
35 | // // `paths`.
36 | // //
37 | // // This field is the source of truth; packages do not come from a `url`; they
38 | // // come from a `hash`. `url` is just one of many possible mirrors for how to
39 | // // obtain a package matching this `hash`.
40 | // //
41 | // // Uses the [multihash](https://multiformats.io/multihash/) format.
42 | // .hash = "...",
43 | //
44 | // // When this is provided, the package is found in a directory relative to the
45 | // // build root. In this case the package's hash is irrelevant and therefore not
46 | // // computed. This field and `url` are mutually exclusive.
47 | // .path = "foo",
48 |
49 | // // When this is set to `true`, a package is declared to be lazily
50 | // // fetched. This makes the dependency only get fetched if it is
51 | // // actually used.
52 | // .lazy = false,
53 | //},
54 | },
55 |
56 | // Specifies the set of files and directories that are included in this package.
57 | // Only files and directories listed here are included in the `hash` that
58 | // is computed for this package. Only files listed here will remain on disk
59 | // when using the zig package manager. As a rule of thumb, one should list
60 | // files required for compilation plus any license(s).
61 | // Paths are relative to the build root. Use the empty string (`""`) to refer to
62 | // the build root itself.
63 | // A directory listed here means that all files within, recursively, are included.
64 | .paths = .{
65 | "build.zig",
66 | "build.zig.zon",
67 | "src",
68 | // For example...
69 | //"LICENSE",
70 | //"README.md",
71 | },
72 | }
73 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jecaro/pomodozig/7e18b5289aad9e8d5cb78ecf1eb4437d26557c17/demo.gif
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "nixpkgs": {
4 | "locked": {
5 | "lastModified": 1733808091,
6 | "narHash": "sha256-KWwINTQelKOoQgrXftxoqxmKFZb9pLVfnRvK270nkVk=",
7 | "owner": "nixos",
8 | "repo": "nixpkgs",
9 | "rev": "a0f3e10d94359665dba45b71b4227b0aeb851f8e",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "nixos",
14 | "ref": "nixos-24.11",
15 | "repo": "nixpkgs",
16 | "type": "github"
17 | }
18 | },
19 | "root": {
20 | "inputs": {
21 | "nixpkgs": "nixpkgs"
22 | }
23 | }
24 | },
25 | "root": "root",
26 | "version": 7
27 | }
28 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
3 | outputs = { self, nixpkgs }:
4 | let
5 | pkgs = nixpkgs.legacyPackages.x86_64-linux;
6 | supportedSystems = [ "x86_64-linux" "x86_64-darwin" ];
7 |
8 | forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
9 |
10 | nixpkgsFor = forAllSystems (system: import nixpkgs {
11 | inherit system;
12 | overlays = [ self.overlays.default ];
13 | });
14 | in
15 | {
16 | overlays.default = (final: prev: {
17 | pomodozig = prev.stdenv.mkDerivation {
18 | name = "pomodozig";
19 | src = self;
20 | nativeBuildInputs = [ pkgs.zig.hook ];
21 | };
22 | });
23 |
24 | packages = forAllSystems (system: {
25 | pomodozig = nixpkgsFor.${system}.pomodozig;
26 | default = self.packages.${system}.pomodozig;
27 | });
28 |
29 | devShells = forAllSystems (system:
30 | let pkgs = nixpkgsFor.${system};
31 | in
32 | {
33 | default = pkgs.mkShell {
34 | buildInputs = [
35 | pkgs.zig
36 | pkgs.zls
37 | ];
38 | };
39 | }
40 | );
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/src/command.zig:
--------------------------------------------------------------------------------
1 | const signalfd = @import("signalfd.zig");
2 | const std = @import("std");
3 | const terminal = @import("terminal.zig");
4 |
5 | pub const Command = enum {
6 | Pause,
7 | Quit,
8 | Reset,
9 | };
10 |
11 | const FdType = enum { stdin, signalfd };
12 |
13 | pub const Poller = struct {
14 | poller: std.io.Poller(FdType),
15 | signalfd: std.fs.File,
16 | /// Option set to the terminal, is null when running in non-interactive mode
17 | terminal: ?terminal.Terminal,
18 |
19 | pub fn init(allocator: std.mem.Allocator) !Poller {
20 | const opt_terminal = terminal.Terminal.init() catch null;
21 | errdefer {
22 | if (opt_terminal) |terminal_| {
23 | terminal_.deinit();
24 | }
25 | }
26 |
27 | const signalfd_ = try signalfd.open();
28 | errdefer signalfd_.close();
29 |
30 | return Poller{
31 | .terminal = opt_terminal,
32 | .signalfd = signalfd_,
33 | .poller = std.io.poll(
34 | allocator,
35 | FdType,
36 | .{
37 | .stdin = std.io.getStdIn(),
38 | .signalfd = signalfd_,
39 | },
40 | ),
41 | };
42 | }
43 |
44 | pub fn interactive(self: *Poller) bool {
45 | return self.terminal != null;
46 | }
47 |
48 | pub fn deinit(self: *Poller) void {
49 | self.poller.deinit();
50 | if (self.terminal) |terminal_| terminal_.deinit();
51 | self.signalfd.close();
52 | }
53 |
54 | pub fn pollTimeout(self: *Poller, nanoseconds: u64) !?Command {
55 | _ = try self.poller.pollTimeout(nanoseconds) or return error.Interrupted;
56 |
57 | if (self.poller.fifo(.stdin).readItem()) |char| {
58 | switch (char) {
59 | 'p' => return Command.Pause,
60 | 'q' => return Command.Quit,
61 | 'r' => return Command.Reset,
62 | else => {},
63 | }
64 | }
65 |
66 | if (try signalfd.read(self.poller.fifo(.signalfd))) |siginfo| {
67 | switch (siginfo.signo) {
68 | std.os.linux.SIG.USR1 => return Command.Pause,
69 | std.os.linux.SIG.USR2 => return Command.Reset,
70 | else => unreachable,
71 | }
72 | }
73 |
74 | return null;
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/src/main.zig:
--------------------------------------------------------------------------------
1 | const command = @import("command.zig");
2 | const options = @import("options.zig");
3 | const pausable_timer = @import("pausable_timer.zig");
4 | const signalfd = @import("signalfd.zig");
5 | const std = @import("std");
6 | const step = @import("step.zig");
7 | const terminal = @import("terminal.zig");
8 |
9 | pub fn main() !void {
10 | var gpa = std.heap.GeneralPurposeAllocator(.{}){};
11 |
12 | const allocator = gpa.allocator();
13 | defer std.debug.assert(gpa.deinit() == .ok);
14 |
15 | const program_and_args = try std.process.argsAlloc(allocator);
16 | defer std.process.argsFree(allocator, program_and_args);
17 |
18 | const program = program_and_args[0];
19 | const args = program_and_args[1..];
20 |
21 | const stdout = std.io.getStdOut().writer();
22 |
23 | const opts = options.parseArgs(args) catch {
24 | try printUsage(allocator, stdout, program);
25 | std.process.exit(1);
26 | };
27 |
28 | switch (opts) {
29 | .help => {
30 | try printUsage(allocator, stdout, program);
31 | },
32 | .settings => |settings| {
33 | try run(allocator, settings);
34 | },
35 | }
36 | }
37 |
38 | fn run(allocator: std.mem.Allocator, settings: options.Settings) !void {
39 | const stdout = std.io.getStdOut().writer();
40 |
41 | var poller = try command.Poller.init(allocator);
42 | defer poller.deinit();
43 |
44 | // Print a placeholder to be cleared by the first status
45 | if (poller.interactive()) {
46 | try stdout.print("\n", .{});
47 | }
48 |
49 | var current = step.Step{};
50 | while (true) : ({
51 | current = current.next(settings.num_pomodoros);
52 | if (settings.notifications) {
53 | notify(allocator, current.step_type.message());
54 | }
55 | }) {
56 | var timer = try pausable_timer.PausableTimer.init();
57 |
58 | var remaining = current.length(settings);
59 |
60 | while (remaining > 0) : ({
61 | remaining = current.length(settings) -| timer.read();
62 | }) {
63 | // Clear the last status
64 | if (poller.interactive()) {
65 | try stdout.print("\x1b[F\x1b[2K\r", .{});
66 | }
67 | try printStatus(
68 | allocator,
69 | stdout,
70 | current,
71 | settings.num_pomodoros,
72 | remaining,
73 | );
74 |
75 | if (try poller.pollTimeout(std.time.ns_per_s)) |command_| {
76 | switch (command_) {
77 | command.Command.Quit => {
78 | try stdout.print("\n", .{});
79 | return;
80 | },
81 | command.Command.Pause => {
82 | try timer.togglePause();
83 | },
84 | command.Command.Reset => {
85 | // if the step is not started yet
86 | if (timer.paused and remaining == current.length(settings)) {
87 | current = step.Step{};
88 | }
89 |
90 | try timer.reset();
91 | },
92 | }
93 | }
94 | }
95 | }
96 | }
97 |
98 | fn notify(allocator: std.mem.Allocator, message: []const u8) void {
99 | const argv = [_][]const u8{ "notify-send", "pomodozig", message };
100 | var proc = std.process.Child.init(&argv, allocator);
101 |
102 | proc.spawn() catch {
103 | return;
104 | };
105 |
106 | _ = proc.wait() catch {};
107 | }
108 |
109 | fn printStatus(
110 | allocator: std.mem.Allocator,
111 | out: std.fs.File.Writer,
112 | current: step.Step,
113 | num_pomodoros: u32,
114 | remaining: u64,
115 | ) !void {
116 | const msg = try current.render(allocator, num_pomodoros, remaining);
117 | defer allocator.free(msg);
118 |
119 | try out.print("{s}\n", .{msg});
120 | }
121 |
122 | fn printUsage(
123 | allocator: std.mem.Allocator,
124 | out: std.fs.File.Writer,
125 | program: []const u8,
126 | ) !void {
127 | const usage = try options.usage(allocator, program);
128 | defer allocator.free(usage);
129 |
130 | try out.print("{s}", .{usage});
131 | }
132 |
133 | test "main.zig" {
134 | std.testing.refAllDecls(@This());
135 | }
136 |
--------------------------------------------------------------------------------
/src/options.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | pub const OptionsTag = enum { settings, help };
4 |
5 | pub const Options = union(OptionsTag) {
6 | settings: Settings,
7 | help: void,
8 | };
9 |
10 | pub const Settings = struct {
11 | /// Length of a task in minutes
12 | task_length: u32 = 25,
13 | /// Length of a short break in minutes
14 | short_break: u32 = 5,
15 | /// Number of pomodoros before a long break
16 | num_pomodoros: u32 = 4,
17 | /// Length of a long break in minutes
18 | long_break: u32 = 15,
19 | /// Enable notifications
20 | notifications: bool = true,
21 | };
22 |
23 | pub fn parseArgs(args: []const []const u8) !Options {
24 | var settings = Settings{};
25 |
26 | var i: u32 = 0;
27 | while (i < args.len) : (i += 1) {
28 | const arg = args[i];
29 | // settings options
30 | if (i + 1 < args.len and std.mem.eql(u8, arg, "-t")) {
31 | i += 1;
32 | settings.task_length = try std.fmt.parseInt(u32, args[i], 10);
33 | } else if (i + 1 < args.len and std.mem.eql(u8, arg, "-sb")) {
34 | i += 1;
35 | settings.short_break = try std.fmt.parseInt(u32, args[i], 10);
36 | } else if (i + 1 < args.len and std.mem.eql(u8, arg, "-n")) {
37 | i += 1;
38 | settings.num_pomodoros = try std.fmt.parseInt(u32, args[i], 10);
39 | } else if (i + 1 < args.len and std.mem.eql(u8, arg, "-lb")) {
40 | i += 1;
41 | settings.long_break = try std.fmt.parseInt(u32, args[i], 10);
42 | } else if (std.mem.eql(u8, arg, "-s")) {
43 | settings.notifications = false;
44 | }
45 | // help
46 | else if (std.mem.eql(u8, arg, "-h")) {
47 | return .{ .help = void{} };
48 | }
49 | // error
50 | else {
51 | return error.InvalidArgument;
52 | }
53 | }
54 |
55 | return .{ .settings = settings };
56 | }
57 |
58 | pub fn usage(allocator: std.mem.Allocator, program: []const u8) ![]u8 {
59 | const result = try std.fmt.allocPrint(allocator,
60 | \\Usage: {s}
61 | \\[-t ] Task length in minutes (default: 25 mins)
62 | \\[-sb ] Short break length in minutes (default: 5 mins)
63 | \\[-n ] Number of pomodoros before long break (default: 4)
64 | \\[-lb ] Long break length in minutes (default: 15 mins)
65 | \\[-s] Disable notifications
66 | \\[-h] Show this help message
67 | \\
68 | , .{program});
69 |
70 | return result;
71 | }
72 |
73 | test "parseArgs options" {
74 | const args = [_][]const u8{
75 | "-t", "30",
76 | "-sb", "10",
77 | "-n", "5",
78 | "-lb", "20",
79 | "-s",
80 | };
81 |
82 | const options = try parseArgs(&args);
83 | try std.testing.expectEqual(options.settings.task_length, 30);
84 | try std.testing.expectEqual(options.settings.short_break, 10);
85 | try std.testing.expectEqual(options.settings.num_pomodoros, 5);
86 | try std.testing.expectEqual(options.settings.long_break, 20);
87 | try std.testing.expectEqual(options.settings.notifications, false);
88 | }
89 |
90 | test "parseArgs help" {
91 | const args = [_][]const u8{"-h"};
92 |
93 | const options = try parseArgs(&args);
94 | try std.testing.expectEqual(options, OptionsTag.help);
95 | }
96 |
97 | test "parseArgs nok" {
98 | const args = [_][]const u8{ "some", "junk", "arguments" };
99 |
100 | const result = parseArgs(&args);
101 | try std.testing.expectError(error.InvalidArgument, result);
102 | }
103 |
--------------------------------------------------------------------------------
/src/pausable_timer.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | pub const PausableTimer = struct {
4 | timer: std.time.Timer,
5 | paused: bool,
6 |
7 | pub fn init() !PausableTimer {
8 | return PausableTimer{
9 | .timer = try std.time.Timer.start(),
10 | .paused = true,
11 | };
12 | }
13 |
14 | pub fn togglePause(self: *PausableTimer) !void {
15 | // when restarting we offset the started field of the time of the time
16 | // that has passed since the pause started
17 | if (self.paused) {
18 | // running paused
19 | // |-------------|----------------|
20 | // started previous now
21 | const now = try std.time.Instant.now();
22 | const since_paused_nsec: u64 = now.since(self.timer.previous);
23 | self.timer.started = add_ns(
24 | self.timer.started,
25 | since_paused_nsec,
26 | );
27 | }
28 | self.paused = !self.paused;
29 | }
30 |
31 | pub fn reset(self: *PausableTimer) !void {
32 | if (!self.paused) {
33 | try self.togglePause();
34 | }
35 | self.timer.reset();
36 | }
37 |
38 | pub fn read(self: *PausableTimer) u64 {
39 | if (self.paused) {
40 | // make as if the timer was paused
41 | return self.timer.previous.since(self.timer.started);
42 | } else {
43 | return self.timer.read();
44 | }
45 | }
46 | };
47 |
48 | pub fn add_ns(instant: std.time.Instant, offset_nsec: u64) std.time.Instant {
49 | // timestamp is expressed in seconds and nanoseconds. we add the offset to
50 | // the nanoseconds part
51 | const instant_tv_nsec: u64 = @intCast(instant.timestamp.tv_nsec);
52 | const total_tv_nsec: u64 = instant_tv_nsec + offset_nsec;
53 |
54 | // then we split the result in the new nanosecond part
55 | const new_ns: u64 = total_tv_nsec % std.time.ns_per_s;
56 | // and the number of seconds to add to the second part
57 | const elapsed_sec: u64 = total_tv_nsec / std.time.ns_per_s;
58 |
59 | return .{ .timestamp = .{
60 | .tv_sec = instant.timestamp.tv_sec + @as(isize, @intCast(elapsed_sec)),
61 | .tv_nsec = @intCast(new_ns),
62 | } };
63 | }
64 |
--------------------------------------------------------------------------------
/src/signalfd.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | pub fn read(fifo: *std.io.PollFifo) !?std.os.linux.signalfd_siginfo {
4 | if (fifo.readableLength() < @sizeOf(std.os.linux.signalfd_siginfo)) {
5 | return null;
6 | }
7 |
8 | var siginfo: std.os.linux.signalfd_siginfo = undefined;
9 | const num_bytes = fifo.read(std.mem.asBytes(&siginfo));
10 |
11 | if (num_bytes < @sizeOf(std.os.linux.signalfd_siginfo)) {
12 | return error.ShortRead;
13 | }
14 |
15 | return siginfo;
16 | }
17 |
18 | pub fn open() !std.fs.File {
19 | var mask = std.posix.empty_sigset;
20 | std.os.linux.sigaddset(&mask, std.os.linux.SIG.USR1);
21 | std.os.linux.sigaddset(&mask, std.os.linux.SIG.USR2);
22 |
23 | _ = std.os.linux.sigprocmask(std.os.linux.SIG.BLOCK, &mask, null);
24 | return .{
25 | .handle = @intCast(
26 | std.os.linux.signalfd(
27 | -1,
28 | &mask,
29 | std.os.linux.SFD.NONBLOCK,
30 | ),
31 | ),
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/src/step.zig:
--------------------------------------------------------------------------------
1 | const options = @import("options.zig");
2 | const std = @import("std");
3 |
4 | pub const StepType = enum(u8) {
5 | task = 'T',
6 | short_break = 'b',
7 | long_break = 'B',
8 |
9 | pub fn message(self: StepType) []const u8 {
10 | return switch (self) {
11 | StepType.task => "Time to focus!",
12 | StepType.short_break => "Time for a break!",
13 | StepType.long_break => "Time for a long break!",
14 | };
15 | }
16 | };
17 |
18 | pub const Step = struct {
19 | task: u32 = 1,
20 | step_type: StepType = StepType.task,
21 |
22 | pub fn next(self: Step, num_pomodoros: u32) Step {
23 | return switch (self.step_type) {
24 | StepType.task => Step{
25 | .task = self.task,
26 | .step_type = if (self.task % num_pomodoros == 0)
27 | StepType.long_break
28 | else
29 | StepType.short_break,
30 | },
31 | StepType.short_break, StepType.long_break => Step{
32 | .task = self.task + 1,
33 | .step_type = StepType.task,
34 | },
35 | };
36 | }
37 |
38 | pub fn length(self: Step, settings: options.Settings) u64 {
39 | const minutes: u64 = switch (self.step_type) {
40 | StepType.task => settings.task_length,
41 | StepType.short_break => settings.short_break,
42 | StepType.long_break => settings.long_break,
43 | };
44 |
45 | return minutes * std.time.ns_per_min;
46 | }
47 |
48 | pub fn render(
49 | self: Step,
50 | allocator: std.mem.Allocator,
51 | num_pomodoros: u32,
52 | countdown: u64,
53 | ) ![]u8 {
54 | const total_secs = countdown / std.time.ns_per_s;
55 | const mins = total_secs / std.time.s_per_min;
56 | const secs = total_secs % std.time.s_per_min;
57 | return try std.fmt.allocPrint(
58 | allocator,
59 | "{c}-{}/{}-{:0>2}:{:0>2}",
60 | .{
61 | @intFromEnum(self.step_type),
62 | self.task,
63 | num_pomodoros,
64 | mins,
65 | secs,
66 | },
67 | );
68 | }
69 | };
70 |
--------------------------------------------------------------------------------
/src/terminal.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | /// Turn on these two options on stdin:
4 | /// - ICANON: disable line buffering
5 | /// - ECHO: disable echo
6 | ///
7 | /// Used to be able to read a single character from stdin. Only works when
8 | /// running in interactive mode
9 | pub const Terminal = struct {
10 | termios: std.posix.termios,
11 |
12 | pub fn init() !Terminal {
13 | const old = try std.posix.tcgetattr(std.posix.STDIN_FILENO);
14 | var termios = old;
15 | // unbuffered input
16 | termios.lflag.ICANON = false;
17 | // no echo
18 | termios.lflag.ECHO = false;
19 | try std.posix.tcsetattr(std.posix.STDIN_FILENO, .NOW, termios);
20 | return .{ .termios = old };
21 | }
22 |
23 | pub fn deinit(self: Terminal) void {
24 | std.posix.tcsetattr(std.posix.STDIN_FILENO, .NOW, self.termios) catch unreachable;
25 | }
26 | };
27 |
--------------------------------------------------------------------------------