├── .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 | demo 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 | --------------------------------------------------------------------------------