From 90164173e30e6dd1f5f581233d232a3803de61e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Dessureault?= <6100619+JeffDess@users.noreply.github.com> Date: Thu, 29 Jan 2026 23:18:52 -0500 Subject: [PATCH 1/3] Add Nix flake, package & modules --- docs/installation/linux_installation.md | 74 ++++++++++++++++++++- docs/installation/macos_installation.md | 4 ++ flake.lock | 61 ++++++++++++++++++ flake.nix | 63 ++++++++++++++++++ nix/modules/home-manager.nix | 86 +++++++++++++++++++++++++ nix/modules/nixos.nix | 38 +++++++++++ nix/package.nix | 38 +++++++++++ 7 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/modules/home-manager.nix create mode 100644 nix/modules/nixos.nix create mode 100644 nix/package.nix diff --git a/docs/installation/linux_installation.md b/docs/installation/linux_installation.md index 19b00eb..aa032b2 100644 --- a/docs/installation/linux_installation.md +++ b/docs/installation/linux_installation.md @@ -16,7 +16,7 @@ This guide will walk you through installing tmpo on Linux. - **x86_64 (64-bit)**: `tmpo_X.X.X_Linux_x86_64.tar.gz` - **ARM64**: `tmpo_X.X.X_Linux_arm64.tar.gz` -> [!NOTE] +> [!NOTE] > Replace `X.X.X` with the latest version number, e.g., `0.1.0` You can also download using curl or wget: @@ -138,6 +138,78 @@ chmod +x ~/bin/tmpo tmpo --version ``` +## Method 3: Nix (flake) + +Run tmpo without installing: + +```bash +# Pass any tmpo command after "--" +nix run github:DylanDevelops/tmpo -- start +``` + +Or add `tmpo` to your flake inputs and your favorite install method in the outputs: + +```nix +{ + inputs = { + tmpo = { + url = "github:DylanDevelops/tmpo"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { nixpkgs, ... }@inputs: + { + nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { + modules = [ + # Package only + ({ pkgs, ... }: { + environment.systemPackages = [ inputs.tmpo.packages.${pkgs.system}.default ]; + }) + # OR NixOS Module + inputs.tmpo.nixosModules.tmpo + ]; + }; + + # OR Home Manager module + homeConfigurations."me" = home-manager.lib.homeManagerConfiguration { + modules = [ + inputs.tmpo.homeManagerModules.tmpo + ]; + }; + }; + }; +} +``` + +### NixOS module + +```nix +{ + programs.tmpo = { + enable = true; + devMode = false; + }; +} +``` + +### Home Manager module + +```nix +{ + programs.tmpo = { + enable = true; + settings = { + currency = "USD"; + dateFormat = "MM/DD/YYYY"; # `MM/DD/YYYY`, `DD/MM/YYYY`, `YYYY-MM-DD` + timeFormat = "24-hour"; # `24-hour`, `12-hour (AM/PM)` + timezone = "America/New_York"; + exportPath = "~/Documents/timesheets"; + }; + }; +} +``` + ## Determining Your Architecture If you're not sure which binary to download: diff --git a/docs/installation/macos_installation.md b/docs/installation/macos_installation.md index cf9b181..a3ac676 100644 --- a/docs/installation/macos_installation.md +++ b/docs/installation/macos_installation.md @@ -109,6 +109,10 @@ sudo chmod +x /usr/local/bin/tmpo tmpo --version ``` +## Method 4: Nix (flake) + +Nix installation is configured in a similar fashion to Linux. Follow the same steps from [Method 3: Nix (flake)](./linux_installation.md#method-3-nix-flake) and use nix-darwin/Home Manager where applicable. + ## Alternative Installation Locations If you prefer not to use `/usr/local/bin/`, you can install to your home directory: diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6bae7c7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1769461804, + "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..66c3317 --- /dev/null +++ b/flake.nix @@ -0,0 +1,63 @@ +{ + description = "tmpo CLI time tracker"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + ... + }: + let + lib = nixpkgs.lib; + sourceInfo = self.sourceInfo or { }; + ref = sourceInfo.ref or ""; + version = if lib.hasPrefix "v" ref then lib.removePrefix "v" ref else "unstable"; + in + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + goPkg = pkgs.go_1_25 or pkgs.go; + rev = self.rev or (self.dirtyRev or "dirty"); + lastModified = sourceInfo.lastModifiedDate or self.lastModifiedDate or "19700101000000"; + date = + "${builtins.substring 0 4 lastModified}-" + + "${builtins.substring 4 2 lastModified}-" + + "${builtins.substring 6 2 lastModified}T" + + "${builtins.substring 8 2 lastModified}:" + + "${builtins.substring 10 2 lastModified}:" + + "${builtins.substring 12 2 lastModified}Z"; + package = pkgs.callPackage ./nix/package.nix { + inherit version; + commit = rev; + inherit date; + go = goPkg; + srcPath = ./.; + }; + in + { + packages = { + default = package; + tmpo = package; + }; + apps.default = flake-utils.lib.mkApp { drv = package; }; + devShells.default = pkgs.mkShell { + packages = [ + goPkg + pkgs.gopls + pkgs.gotools + ]; + }; + } + ) + // { + nixosModules.tmpo = import ./nix/modules/nixos.nix { inherit self; }; + homeManagerModules.tmpo = import ./nix/modules/home-manager.nix { inherit self; }; + }; +} diff --git a/nix/modules/home-manager.nix b/nix/modules/home-manager.nix new file mode 100644 index 0000000..4aa5f2b --- /dev/null +++ b/nix/modules/home-manager.nix @@ -0,0 +1,86 @@ +{ + self ? null, +}: +{ + lib, + pkgs, + config, + ... +}: +let + cfg = config.programs.tmpo; + fallbackPackage = pkgs.callPackage ../package.nix { srcPath = ../../.; }; + defaultPackage = if self == null then fallbackPackage else self.packages.${pkgs.system}.default; + settingsFile = lib.filterAttrs (_: value: value != null) { + inherit (cfg.settings) currency timezone; + date_format = cfg.settings.dateFormat; + time_format = cfg.settings.timeFormat; + export_path = cfg.settings.exportPath; + }; + hasSettings = builtins.length (builtins.attrNames settingsFile) > 0; + yamlSettings = lib.generators.toKeyValue { + mkKeyValue = lib.generators.mkKeyValueDefault { } ": "; + } settingsFile; +in +{ + options.programs.tmpo = { + enable = lib.mkEnableOption "tmpo CLI time tracker"; + + package = lib.mkOption { + type = lib.types.package; + default = defaultPackage; + description = "tmpo package to install."; + }; + + devMode = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Use ~/.tmpo-dev by setting TMPO_DEV=1."; + }; + + settings = { + currency = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "ISO 4217 currency code (ex: USD)."; + }; + + dateFormat = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Date format (MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD)."; + }; + + timeFormat = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Time format (24-hour, 12-hour (AM/PM))."; + }; + + timezone = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "IANA timezone (ex: America/New_York)."; + }; + + exportPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Default export path for csv/json exports."; + }; + }; + }; + + config = lib.mkIf cfg.enable { + home = { + packages = [ cfg.package ]; + sessionVariables = lib.mkIf cfg.devMode { + TMPO_DEV = "1"; + }; + + file = lib.mkIf hasSettings { + ".tmpo/config.yaml".text = yamlSettings; + }; + }; + }; +} diff --git a/nix/modules/nixos.nix b/nix/modules/nixos.nix new file mode 100644 index 0000000..2c1c9f3 --- /dev/null +++ b/nix/modules/nixos.nix @@ -0,0 +1,38 @@ +{ + self ? null, +}: +{ + lib, + pkgs, + config, + ... +}: +let + cfg = config.programs.tmpo; + fallbackPackage = pkgs.callPackage ../package.nix { srcPath = ../../.; }; + defaultPackage = if self == null then fallbackPackage else self.packages.${pkgs.system}.default; +in +{ + options.programs.tmpo = { + enable = lib.mkEnableOption "tmpo CLI time tracker"; + + package = lib.mkOption { + type = lib.types.package; + default = defaultPackage; + description = "tmpo package to install."; + }; + + devMode = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Use ~/.tmpo-dev by setting TMPO_DEV=1."; + }; + }; + + config = lib.mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + environment.variables = lib.mkIf cfg.devMode { + TMPO_DEV = "1"; + }; + }; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..66ef1ac --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,38 @@ +{ + lib, + buildGoModule, + go, + srcPath ? lib.cleanSource ../., + version ? "0.4.5", + commit ? "dirty", + date ? "1970-01-01T00:00:00Z", +}: +buildGoModule { + pname = "tmpo"; + inherit version go; + src = srcPath; + + vendorHash = "sha256-4S5ESwFJ8JYKugNJgTkxUywZM7dTqDlDIvXcNRot5b4="; + subPackages = [ "." ]; + + ldflags = [ + "-s" + "-w" + "-X github.com/DylanDevelops/tmpo/cmd/utilities.Version=${version}" + "-X github.com/DylanDevelops/tmpo/cmd/utilities.Commit=${commit}" + "-X github.com/DylanDevelops/tmpo/cmd/utilities.Date=${date}" + ]; + + env = { + CGO_ENABLED = "0"; + }; + + doCheck = false; + + meta = with lib; { + description = "Minimal CLI time tracker for developers"; + homepage = "https://github.com/DylanDevelops/tmpo"; + license = licenses.mit; + mainProgram = "tmpo"; + }; +} From e40b11752ffeca148cf7302cd548c2d2f025d85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Dessureault?= <6100619+JeffDess@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:15:29 -0500 Subject: [PATCH 2/3] Add Nix development setup --- .gitignore | 3 ++- CONTRIBUTING.md | 20 ++++++++++++++++++++ flake.nix | 3 +++ nix/package.nix | 2 +- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b84fba6..b73d64f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # Binaries for programs and plugins tmpo dist/ +result/ *.exe *.exe~ *.dll @@ -47,4 +48,4 @@ node_modules/ .tmporc # AI Tools -CLAUDE.md \ No newline at end of file +CLAUDE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a81e71..e9bca1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,16 @@ Thank you for your interest in contributing to tmpo! This document provides guid git remote add upstream https://github.com/DylanDevelops/tmpo.git ``` +### Using Nix (optional) + +If you'd rather use Nix, you can enter a development shell that provides every required dependencies: + +```bash +nix develop +``` + +Once inside the shell, `TMPO_DEV=1` is automatically enabled and you run the same commands as in any other environment. + ## Development Workflow ### Building @@ -37,6 +47,13 @@ go build -o tmpo . ./tmpo --help ``` +Nix also allows to run these without entering a devshell: + +```bash +nix build +nix run . -- --help +``` + ### Development Mode To prevent corrupting your real tmpo data during development, use the `TMPO_DEV` environment variable: @@ -77,6 +94,9 @@ echo 'export TMPO_DEV=1' >> ~/.bashrc Then restart your terminal or run `source ~/.zshrc` (or `source ~/.bashrc`). +> [!TIP] +> In NixOS, dev move persistence can also be configured in [modules](docs/installation/linux_installation.md#nixos-module). + **Benefits of development mode:** - Your real time tracking data and settings stay safe diff --git a/flake.nix b/flake.nix index 66c3317..98b2119 100644 --- a/flake.nix +++ b/flake.nix @@ -48,10 +48,13 @@ }; apps.default = flake-utils.lib.mkApp { drv = package; }; devShells.default = pkgs.mkShell { + TMPO_DEV = "1"; packages = [ goPkg pkgs.gopls pkgs.gotools + pkgs.goreleaser + pkgs.sqlite ]; }; } diff --git a/nix/package.nix b/nix/package.nix index 66ef1ac..2150ac5 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -27,7 +27,7 @@ buildGoModule { CGO_ENABLED = "0"; }; - doCheck = false; + doCheck = true; meta = with lib; { description = "Minimal CLI time tracker for developers"; From 036f7d15c962938952a6c69884aecf547d0dce83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Dessureault?= <6100619+JeffDess@users.noreply.github.com> Date: Mon, 2 Feb 2026 06:55:02 -0500 Subject: [PATCH 3/3] Check for version update only if current version is semver Previously only checked for "dev" or empty, but Nix flake will show "unstable" as it run latest commit instead of a version. --- cmd/utilities/version.go | 13 +++++++++---- cmd/utilities/version_test.go | 5 +++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cmd/utilities/version.go b/cmd/utilities/version.go index 16a1b0c..3c8920e 100644 --- a/cmd/utilities/version.go +++ b/cmd/utilities/version.go @@ -17,6 +17,8 @@ var ( Date = "unknown" ) +var releaseVersionRegex = regexp.MustCompile(`^v?\d+\.\d+\.\d+(-[\w.]+)?$`) + func VersionCmd() *cobra.Command { cmd := &cobra.Command{ Use: "version", @@ -54,8 +56,7 @@ func GetFormattedDate(inputDate string) string { func GetChangelogUrl(version string) string { path := "https://github.com/DylanDevelops/tmpo" - r := regexp.MustCompile(`^v?\d+\.\d+\.\d+(-[\w.]+)?$`) - if !r.MatchString(version) { + if !isReleaseVersion(version) { return fmt.Sprintf("%s/releases/latest", path) } @@ -63,8 +64,8 @@ func GetChangelogUrl(version string) string { } func checkForUpdates() { - // Only check if we have a valid version (not "dev" or empty) - if Version == "" || Version == "dev" { + // Only check for released semantic versions. + if !isReleaseVersion(Version) { return } @@ -79,3 +80,7 @@ func checkForUpdates() { fmt.Printf("%s\n\n", ui.Muted(updateInfo.UpdateURL)) } } + +func isReleaseVersion(version string) bool { + return releaseVersionRegex.MatchString(version) +} diff --git a/cmd/utilities/version_test.go b/cmd/utilities/version_test.go index b340e32..6759cf6 100644 --- a/cmd/utilities/version_test.go +++ b/cmd/utilities/version_test.go @@ -78,6 +78,11 @@ func TestGetChangelogUrl(t *testing.T) { version: "dev", expected: "https://github.com/DylanDevelops/tmpo/releases/latest", }, + { + name: "unstable version returns latest", + version: "unstable", + expected: "https://github.com/DylanDevelops/tmpo/releases/latest", + }, { name: "empty version returns latest", version: "",