timetracker
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
  119. 119
  120. 120
  121. 121
  122. 122
  123. 123
// Copyright 2026 Shota FUJI
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// SPDX-License-Identifier: MPL-2.0

const std = @import("std");
const uuid = @import("uuid");

const Options = @import("../Options.zig");
const errors = @import("../errors.zig");
const events = @import("../events.zig");

pub const Error = errors.CommandLineError ||
    events.Iterator.InitError ||
    std.Io.Reader.LimitedAllocError ||
    events.Envelope.AppendError ||
    events.Manifest.SaveError ||
    std.mem.Allocator.Error;

fn printUsage(io: std.Io, file: std.Io.File) !void {
    var buf: [512]u8 = undefined;
    var writer = file.writer(io, &buf);

    try writer.interface.writeAll(
        \\timetracker start - Start a new timer
        \\
        \\[USAGE]
        \\timetracker start [OPTIONS] <TITLE>
        \\
        \\[PARAMETERS]
        \\TITLE   Title text of the new timer.
        \\
        \\[OPTIONS]
        \\-h, --help   Print this message to stdout and exits.
        \\
        \\
    );

    try writer.flush();
}

pub fn run(args: *std.process.Args.Iterator, gpa: std.mem.Allocator, io: std.Io, opts: Options) Error!void {
    var maybe_title: ?[]const u8 = null;
    defer if (maybe_title) |slice| gpa.free(slice);

    while (args.next()) |arg| {
        if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
            printUsage(io, std.Io.File.stdout()) catch |err| {
                std.log.err("Failed to print usage to stdout: {t}", .{err});
                return Error.WriteToStdoutFailed;
            };
            return;
        }

        if (maybe_title != null) {
            std.log.err("You cannot set title more than once", .{});
            printUsage(io, std.Io.File.stderr()) catch return Error.WriteToStderrFailed;
            return Error.InvalidUsage;
        }

        maybe_title = try gpa.dupe(u8, arg);
    }

    // Using `maybe_title` rather than putting this logic inside the `orelse` below
    // makes freeing the slice simpler.
    if (maybe_title == null) {
        const stdin = std.Io.File.stdin();
        if (!(try stdin.isTty(io))) {
            var read_buf: [128]u8 = undefined;
            var reader = stdin.reader(io, &read_buf);

            maybe_title = reader.interface.allocRemaining(gpa, .limited(256)) catch |err| {
                std.log.err("Unable to read from stdin: {t}", .{err});
                return err;
            };
        }
    }

    const title = if (maybe_title) |title| std.mem.trim(u8, title, " \n") else {
        std.log.err("Title is required", .{});
        printUsage(io, std.Io.File.stderr()) catch return Error.WriteToStderrFailed;
        return Error.InvalidUsage;
    };

    var manifest = try events.Manifest.open(gpa, io, &opts.app_dir);
    defer manifest.deinit(gpa);

    const event_id = uuid.v4.new(io);

    const envelope: events.Envelope = .{
        .parent = manifest.tail,
        .id = event_id,
        .event = .{ .timer_started = .{
            .event_id = event_id,
            .timer_id = uuid.v4.new(io),
            .created_at = std.Io.Clock.real.now(io).toSeconds(),
            .title = title,
        } },
    };

    envelope.append(gpa, io, &manifest) catch |err| {
        std.log.err("Failed to save event file: {t}", .{err});
        return err;
    };

    manifest.tail = event_id;
    manifest.save(io) catch |err| {
        std.log.err("Failed to update manifest file: {t}", .{err});
        return err;
    };

    var stderr = std.Io.File.stderr();
    defer stderr.close(io);

    var write_buf: [1024]u8 = undefined;
    var writer = stderr.writer(io, &write_buf);

    try writer.interface.print("Created \"{s}\"\n", .{title});
    try writer.interface.flush();
}