tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

commit cd022b8cee1542ce9e9244b7adada46b150b2d94
parent c7247ea20010644f0283207537b886a9a9898028
Author: mtmn <miro@haravara.org>
Date:   Thu,  9 Apr 2026 15:09:06 +0200

feat: remove khal_notifier

Former-commit-id: 37ca20e3ef47c80803639ace67dbe45eacd234ca
Former-commit-id: 6c840229e7b370751e74aa12fca8d83b49afa7aa
Former-commit-id: c885e27a510bad87ce72a7f3d5b8c443682545c9
Diffstat:
Dkhal_notifier/.formatter.exs | 4----
Dkhal_notifier/.gitignore | 4----
Dkhal_notifier/AGENTS.md | 154-------------------------------------------------------------------------------
Dkhal_notifier/BUILD | 13-------------
Dkhal_notifier/Makefile | 24------------------------
Dkhal_notifier/README.md | 21---------------------
Dkhal_notifier/build.exs | 30------------------------------
Dkhal_notifier/flake.lock | 61-------------------------------------------------------------
Dkhal_notifier/flake.nix | 83-------------------------------------------------------------------------------
Dkhal_notifier/lib/BUILD | 5-----
Dkhal_notifier/lib/khal_notifier.ex | 253-------------------------------------------------------------------------------
Dkhal_notifier/mix.exs | 41-----------------------------------------
Dkhal_notifier/mix.lock | 18------------------
Dkhal_notifier/rewrite_in_bb/example.bb | 111-------------------------------------------------------------------------------
Dkhal_notifier/rewrite_in_bb/khal_notifier.bb | 238-------------------------------------------------------------------------------
15 files changed, 0 insertions(+), 1060 deletions(-)

diff --git a/khal_notifier/.formatter.exs b/khal_notifier/.formatter.exs @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/khal_notifier/.gitignore b/khal_notifier/.gitignore @@ -1,4 +0,0 @@ -/.expert/ -/_build/ -/deps/ -khal_notifier diff --git a/khal_notifier/AGENTS.md b/khal_notifier/AGENTS.md @@ -1,153 +0,0 @@ -# Khal Notifier - Agent Documentation - -## Project Overview - -Khal Notifier is an Elixir application that syncs calendars with vdirsyncer and sends desktop notifications for upcoming khal calendar events. It's designed as a command-line tool that runs periodically to alert users about imminent calendar events. - -## Architecture - -The application is built as a single Elixir module that: -1. Optionally syncs calendars using `vdirsyncer` -2. Queries upcoming events from `khal` (a calendar application) -3. Filters events to exclude those that have already started -4. Sends desktop notifications using `notify-send` - -## Essential Commands - -### Building -The project provides automatic build selection based on environment: - -```bash -# Automatically chooses best build method -make build - -# Force specific build methods -make build-burrito # For static binaries (non-Nix) -make build-escript # For Nix compatibility - -# Or manually: -mix deps.get -mix escript.build # Works in Nix -# OR -mix release # Works outside Nix -``` - -### Development -```bash -# Format code -make format - -# Lint code (strict mode) -make lint - -# Type checking -make check - -# Clean build artifacts -make clean -``` - -### Running -After building, the binary will be located at: -``` -_build/dev/rel/khal_notifier/burrito_out/khal_notifier -``` - -Or for production: -``` -_build/prod/rel/khal_notifier/burrito_out/khal_notifier -``` - -## Command Line Interface - -The application accepts these command-line arguments: - -- `--lookahead TIME` - How far ahead to check for events (default: "15m") -- `--urgency LEVEL` - Notification urgency level (default: "critical") -- `--skip-sync` - Skip the vdirsyncer sync step -- `--clean` - Clean operation (see source for details) -- `--refresh SECONDS` - Run continuously, re-checking every N seconds (internal cron mode) - -Example: -```bash -# Run once -./khal_notifier --lookahead "30m" --urgency "normal" --skip-sync - -# Run continuously, checking every 5 minutes -./khal_notifier --refresh 300 - -# Run continuously, checking every 30 seconds -./khal_notifier --refresh 30 - -# Run continuously with custom lookahead -./khal_notifier --refresh 60 --lookahead "30m" -``` - -## Dependencies and External Tools - -The application depends on these external tools being installed and available on PATH: -- `vdirsyncer` - for calendar synchronization (optional if --skip-sync used) -- `khal` - for calendar event queries -- `notify-send` - for desktop notifications (libnotify) - -## Project Structure - -- `lib/khal_notifier.ex` - Main application module with all logic -- `mix.exs` - Mix project configuration (uses Burrito for static binaries) -- `Makefile` - Build automation -- `.formatter.exs` - Elixir code formatting rules -- `khal_notifier.service` - Systemd service file -- `khal_notifier.timer` - Systemd timer file - -## Build Configuration - -The project uses **Burrito** instead of escript to create static binaries. This allows: -- Single binary distribution without Elixir runtime dependencies -- Cross-platform compilation (Linux, macOS, Windows targets configured) -- Easier deployment and distribution - -## Testing Approach - -Currently no automated tests are set up. Manual testing involves: -1. Building the binary -2. Running with various command-line options -3. Verifying notifications appear correctly -4. Checking error handling when external tools are missing - -## Important Gotchas - -1. **Nix Environment**: The project automatically detects Nix environments and falls back to escript builds. Burrito builds may fail in Nix due to permission restrictions on ERTS files. - -2. **Build Output**: - - Burrito build: `_build/dev/rel/khal_notifier/burrito_out/khal_notifier` - - Escript build: `./khal_notifier` (in project root) - -3. **External Dependencies**: The application will fail silently if `khal` or `notify-send` are not available. Check logs for error messages. - -2. **Calendar Format**: The application expects `khal list` to output events in a specific format: `{start-time}|{end-time}|{title}|{organizer}`. This format is hardcoded. - -3. **Time Parsing**: Time comparison uses simple string comparison (HH:MM format), which works for same-day events but may not handle all edge cases. - -4. **Error Handling**: External command failures are logged but don't stop execution - the application continues with empty results. - -5. **Permissions**: For systemd integration (using the provided .service and .timer files), appropriate permissions are needed to access calendar files and send notifications. - -## Development Conventions - -- Use `mix format` before committing -- Run `make lint` to check code style with Credo (strict mode) -- Run `make check` for type analysis with Dialyxir -- Keep the main module focused - this is a simple utility tool -- Error handling should be robust but non-blocking - -## Deployment - -The project includes systemd service and timer files for automated execution: -- `khal_notifier.service` - Defines how to run the application -- `khal_notifier.timer` - Defines when to run the application (typically every few minutes) - -These files need to be installed in `/etc/systemd/system/` and enabled: -```bash -sudo cp khal_notifier.* /etc/systemd/system/ -sudo systemctl enable --now khal_notifier.timer -``` -\ No newline at end of file diff --git a/khal_notifier/BUILD b/khal_notifier/BUILD @@ -1,13 +0,0 @@ -load("//bazel:local-deploy.bzl", "local_deploy") - -# you have to `mix escript.build` first and then copy the binary to /opt/tools -filegroup( - name = "binary", - srcs = ["khal_notifier"], - visibility = ["//visibility:public"], -) - -local_deploy( - name = "deploy", - srcs = [":binary"], -) diff --git a/khal_notifier/Makefile b/khal_notifier/Makefile @@ -1,24 +0,0 @@ -.PHONY: all build-burrito build-escript format lint check clean - -all: build-escript - -build-burrito: - mix deps.get - mix release - -build-escript: - elixir build.exs - -format: - mix format - -lint: - mix credo --strict - -check: - mix dialyzer - -clean: - mix clean - rm -rf _build - rm -f khal_notifier diff --git a/khal_notifier/README.md b/khal_notifier/README.md @@ -1,21 +0,0 @@ -# KhalNotifier - -**TODO: Add description** - -## Installation - -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `khal_notifier` to your list of dependencies in `mix.exs`: - -```elixir -def deps do - [ - {:khal_libnotify, "~> 0.1.0"} - ] -end -``` - -Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at <https://hexdocs.pm/khal_libnotify>. - diff --git a/khal_notifier/build.exs b/khal_notifier/build.exs @@ -1,29 +0,0 @@ -#!/usr/bin/env elixir - -defmodule Build do - def main(_) do - # Ensure dependencies are installed - System.cmd("mix", ["deps.get"], cd: File.cwd!()) - - # Create a simple escript build as fallback for Nix - IO.puts("Building escript for Nix environment...") - - {output, 0} = System.cmd("mix", ["escript.build"], cd: File.cwd!) - IO.puts(output) - - # Check if binary was created - if File.exists?("khal_notifier") do - IO.puts("✅ Successfully built khal_notifier escript") - - # Make it executable - System.cmd("chmod", ["+x", "khal_notifier"]) - - IO.puts("Binary location: #{Path.absname("khal_notifier")}") - else - IO.puts("❌ Build failed - binary not found") - System.halt(1) - end - end -end - -Build.main(System.argv()) -\ No newline at end of file diff --git a/khal_notifier/flake.lock b/khal_notifier/flake.lock @@ -1,61 +0,0 @@ -{ - "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": 1775525641, - "narHash": "sha256-rM50ZJFuc9ePeWX7mgkX3jyqIlJoAdhyGsz5d2xhaTk=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "dc72a9d2282085d97d5a2cc8e7103a68ed4de188", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-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/khal_notifier/flake.nix b/khal_notifier/flake.nix @@ -1,83 +0,0 @@ -{ - description = "Khal Notifier - Calendar notification system"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - - # Elixir and Erlang versions - beamPackages = pkgs.beam.packages.erlang_27; - elixir = beamPackages.elixir_1_19; - - pname = "khal_notifier"; - version = "0.1.0"; - - # Build dependencies (mix deps) - mixDeps = beamPackages.fetchMixDeps { - pname = "${pname}-deps"; - inherit version; - src = pkgs.lib.cleanSource ./.; - # You will need to update this hash when dependencies change - # Run `nix build` and use the hash from the error message - hash = "sha256-hpbDcEHtQKBPinqR+e+jjsnOcd5PC8WN16ecLyoLDio="; - }; - - khal_notifier = (beamPackages.buildMix.override { inherit elixir; }) { - name = pname; - inherit pname version mixDeps; - src = pkgs.lib.cleanSource ./.; - - nativeBuildInputs = [ pkgs.git pkgs.makeWrapper pkgs.cacert ]; - - buildPhase = '' - runHook preBuild - export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt - export SSL_CERT_DIR=${pkgs.cacert}/etc/ssl/certs - export MIX_HOME=$TMPDIR/.mix - - mkdir -p deps - cp -r ${mixDeps}/* deps/ || true - chmod -R +w deps/ || true - - mix compile --no-deps-check --no-archives-check - mix escript.build - runHook postBuild - ''; - - installPhase = '' - mkdir -p $out/bin - cp khal_notifier $out/bin/ - # Patch shebangs to use the nix-provided erlang/elixir - patchShebangs $out/bin/khal_notifier - ''; - }; - in - { - packages.default = khal_notifier; - - devShells.default = pkgs.mkShell { - buildInputs = [ - elixir - beamPackages.erlang - beamPackages.hex - beamPackages.rebar3 - pkgs.git - ]; - - shellHook = '' - echo "🗓️ Khal Notifier Development Environment" - echo "Elixir ${elixir.version} with Erlang ${beamPackages.erlang.version}" - echo "" - echo "Build the application with: nix escript.build" - echo "Run with: ./khal_notifier" - echo "Or build with Nix: nix build" - ''; - }; - }); -} diff --git a/khal_notifier/lib/BUILD b/khal_notifier/lib/BUILD @@ -1,5 +0,0 @@ -filegroup( - name = "all_srcs", - srcs = glob(["**/*.ex"]), - visibility = ["//khal_notifier:__pkg__"], -) diff --git a/khal_notifier/lib/khal_notifier.ex b/khal_notifier/lib/khal_notifier.ex @@ -1,253 +0,0 @@ -defmodule KhalNotifier do - @moduledoc """ - Sync calendars with vdirsyncer, then send notify-send - alerts for upcoming khal events. - """ - require Logger - - def main(args) do - {opts, _, _} = - OptionParser.parse(args, - switches: [ - lookahead: :string, - urgency: :string, - skip_sync: :boolean, - clean: :boolean, - refresh: :integer - ], - aliases: [] - ) - - lookahead = Keyword.get(opts, :lookahead, "15m") - urgency = Keyword.get(opts, :urgency, "critical") - skip_sync = Keyword.get(opts, :skip_sync, false) - refresh_seconds = Keyword.get(opts, :refresh, 300) - - run_with_refresh(refresh_seconds, lookahead, urgency, skip_sync) - end - - defp run_with_refresh(seconds, lookahead, urgency, skip_sync) do - Logger.info("Running in refresh mode: checking every #{seconds} second(s)") - loop_with_refresh(0, seconds, lookahead, urgency, skip_sync) - end - - defp loop_with_refresh(iteration, seconds, lookahead, urgency, skip_sync) do - if iteration > 0 do - Logger.info("Waiting #{seconds} second(s) before next refresh") - :timer.sleep(seconds * 1000) - end - - Logger.info("Run #{iteration + 1}") - run_once(lookahead, urgency, skip_sync) - - # Tail recursion ensures previous iteration's memory is freed - loop_with_refresh(iteration + 1, seconds, lookahead, urgency, skip_sync) - end - - defp run_once(lookahead, urgency, skip_sync) do - unless skip_sync do - unless sync_vdirsyncer() do - Logger.warning("Sync failed — continuing with local calendar data") - end - end - - events = get_upcoming_events(lookahead) - - events = filter_events(events) - - if Enum.empty?(events) do - Logger.info("No upcoming events") - else - Enum.each(events, fn event -> - summary = event.title - body = format_event(event) - Logger.info("Notifying: #{summary} — #{body}") - send_notification(summary, body, urgency) - end) - end - end - - defp filter_events(events) do - {now_time, 0} = System.cmd("date", ["+%H:%M"]) - now_time = String.trim(now_time) - - filtered = - Enum.reject(events, fn event -> - event.start_time != "" and event.start_time <= now_time - end) - - filtered - end - - defp run_cmd(cmd, args, timeout \\ 30_000) do - Logger.debug("Running: #{cmd} #{Enum.join(args, " ")}") - - case System.find_executable(cmd) do - nil -> - {:error, :enoent} - - path -> - try do - task = Task.async(fn -> System.cmd(path, args, stderr_to_stdout: true) end) - - case Task.yield(task, timeout) || Task.shutdown(task) do - {:ok, {output, exit_code}} -> - {:ok, output, exit_code} - - nil -> - {:error, :timeout} - end - rescue - e -> {:error, e} - end - end - end - - defp sync_vdirsyncer do - Logger.info("Syncing calendars with vdirsyncer") - - case run_cmd("vdirsyncer", ["sync"], 60_000) do - {:ok, _output, 0} -> - Logger.info("vdirsyncer sync complete") - true - - {:ok, output, code} -> - Logger.warning("vdirsyncer exited with code #{code}: #{String.trim(output)}") - false - - {:error, :enoent} -> - Logger.error("vdirsyncer not found") - false - - {:error, :timeout} -> - Logger.error("vdirsyncer timed out after 60 seconds") - false - - {:error, e} -> - Logger.error("vdirsyncer failed: #{inspect(e)}") - false - end - end - - defp get_upcoming_events(lookahead) do - Logger.info("Checking khal for events in the next #{lookahead}...") - - case run_cmd("khal", [ - "list", - "now", - lookahead, - "--format", - "{start-time}|{end-time}|{title}|{organizer}" - ]) do - {:ok, output, 0} -> - parse_events(output) - - {:ok, output, code} -> - Logger.warning("khal exited with code #{code}: #{String.trim(output)}") - [] - - {:error, :enoent} -> - Logger.error("khal not found. Is it installed and on PATH?") - [] - - {:error, :timeout} -> - Logger.error("khal timed out.") - [] - - {:error, _e} -> - Logger.error("khal execution failed.") - [] - end - end - - defp parse_events(output) do - events = - output - |> String.split("\n", trim: true) - |> Enum.map(&String.trim/1) - |> Enum.reject(&(&1 == "")) - |> Enum.filter(&String.contains?(&1, "|")) - |> Enum.map(fn line -> - case String.split(line, "|", parts: 4) do - [start_time, end_time, title, organizer] -> - %{ - start_time: String.trim(start_time), - end_time: String.trim(end_time), - title: String.trim(title), - organizer: String.trim(organizer) - } - - [start_time, end_time, title] -> - %{ - start_time: String.trim(start_time), - end_time: String.trim(end_time), - title: String.trim(title), - organizer: "" - } - - [start_time, title] -> - %{ - start_time: String.trim(start_time), - end_time: "", - title: String.trim(title), - organizer: "" - } - - [title] -> - %{ - start_time: "", - end_time: "", - title: String.trim(title), - organizer: "" - } - end - end) - |> Enum.reject(&(&1.title == "")) - - Logger.info("Found #{length(events)} upcoming event(s).") - events - end - - defp format_event(event) do - time_str = - case {event.start_time, event.end_time} do - {"", ""} -> "All day" - {start, ""} -> start - {start, end_t} -> "#{start} - #{end_t}" - end - - if Map.has_key?(event, :organizer) and event.organizer != "" do - "#{time_str}\nOrganizer: #{event.organizer}" - else - time_str - end - end - - defp send_notification(summary, body, urgency) do - args = [ - "--app-name", - "khal_notifier", - "--urgency", - urgency, - summary, - body - ] - - case run_cmd("notify-send", args) do - {:ok, _output, 0} -> - true - - {:ok, output, code} -> - Logger.error("notify-send failed (code #{code}): #{String.trim(output)}") - false - - {:error, :enoent} -> - Logger.error("notify-send not found") - false - - {:error, _} -> - Logger.error("notify-send failed") - false - end - end -end diff --git a/khal_notifier/mix.exs b/khal_notifier/mix.exs @@ -1,41 +0,0 @@ -defmodule KhalNotifier.MixProject do - use Mix.Project - - def project do - [ - app: :khal_notifier, - version: "0.1.0", - elixir: "~> 1.19", - start_permanent: Mix.env() == :prod, - escript: [main_module: KhalNotifier], - releases: [ - khal_notifier: [ - steps: [:assemble, &Burrito.wrap/1], - burrito: [ - targets: [ - linux: [os: :linux, cpu: :x86_64] - ], - quiet: false - ] - ] - ], - deps: deps() - ] - end - - # Run "mix help compile.app" to learn about applications. - def application do - [ - extra_applications: [:logger] - ] - end - - # Run "mix help deps" to learn about dependencies. - defp deps do - [ - {:burrito, "~> 1.0"}, - {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, - {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false} - ] - end -end diff --git a/khal_notifier/mix.lock b/khal_notifier/mix.lock @@ -1,18 +0,0 @@ -%{ - "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "burrito": {:hex, :burrito, "1.5.0", "d68ec01df2871f1d5bc603b883a78546c75761ac73c1bec1b7ae2cc74790fcd1", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "3861abda7bffa733862b48da3e03df0b4cd41abf6fd24b91745f5c16d971e5fa"}, - "credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"}, - "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, - "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, - "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, - "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, - "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, - "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, - "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, - "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, - "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, -} diff --git a/khal_notifier/rewrite_in_bb/example.bb b/khal_notifier/rewrite_in_bb/example.bb @@ -1,111 +0,0 @@ -#!/usr/bin/env bb - -(ns habitual - (:require [clojure.string :as str] - [clojure.tools.cli :refer [parse-opts]] - [babashka.process :refer [shell]])) - -(defn parse-cron-field [field lo hi] - (let [all (set (range lo (inc hi)))] - (reduce - (fn [acc part] - (cond - (= part "*") - (into acc all) - - (re-matches #"\*/\d+" part) - (let [step (parse-long (subs part 2))] - (into acc (range lo (inc hi) step))) - - (re-matches #"\d+-\d+/\d+" part) - (let [[range-part step-part] (str/split part #"/") - [a b] (map parse-long (str/split range-part #"-")) - step (parse-long step-part)] - (into acc (range a (inc b) step))) - - (re-matches #"\d+-\d+" part) - (let [[a b] (map parse-long (str/split part #"-"))] - (into acc (range a (inc b)))) - - :else - (conj acc (parse-long part)))) - #{} - (str/split field #",")))) - -(defn parse-cron [expr] - (let [[minutes hours days months weekdays] (str/split expr #"\s+")] - {:minutes (parse-cron-field minutes 0 59) - :hours (parse-cron-field hours 0 23) - :days (parse-cron-field days 1 31) - :months (parse-cron-field months 1 12) - :weekdays (parse-cron-field weekdays 0 6)})) - -(defn next-cron-ms - "Returns milliseconds until the next matching habit" - [cron] - (let [now (System/currentTimeMillis) - start (-> now (quot 60000) inc (* 60000))] - (loop [t start] - (if (> t (+ start (* 4 366 24 60 60 1000))) - (throw (ex-info "No cron match found within 4 years" {:cron cron})) - (let [ldt (java.time.LocalDateTime/ofInstant - (java.time.Instant/ofEpochMilli t) - (java.time.ZoneId/systemDefault)) - month (.getMonthValue ldt) ; 1-12 - mday (.getDayOfMonth ldt) ; 1-31 - hour (.getHour ldt) ; 0-23 - minute (.getMinute ldt) ; 0-59 - weekday (-> ldt .getDayOfWeek .getValue (mod 7))] ; 0=Sun (ISO Mon=1, so Sun=7 mod 7=0) - (if (and ((:months cron) month) - ((:days cron) mday) - ((:weekdays cron) weekday) - ((:hours cron) hour) - ((:minutes cron) minute)) - (- t now) - (recur (+ t 60000)))))))) - -(defn run-job! [name cron prog] - (future - (loop [] - (let [delay-ms (next-cron-ms cron)] - (println (format "[INFO] Scheduling next run for '%s' in %d seconds" - name (quot delay-ms 1000))) - (Thread/sleep delay-ms) - (println (format "[INFO] Running notifier for '%s'" name)) - (let [result (shell {:continue true} prog name)] - (when (not= 0 (:exit result)) - (println (format "[WARN] Command returned %d: %s %s" - (:exit result) prog name)))) - (recur))))) - -(def cli-options - [["-p" "--prog PROG" "Program to send notifications to" - :default "notify-send"] - ["-h" "--help"]]) - -(defn -main [& args] - (let [{:keys [options summary errors]} (parse-opts args cli-options)] - (when errors - (println (str/join "\n" errors)) - (System/exit 1)) - (when (:help options) - (println "Usage: habitual [options]") - (println summary) - (System/exit 0)) - - (let [config-file (str (System/getProperty "user.home") - "/.config/habitual/habitual") - lines (-> config-file slurp str/split-lines) - prog (:prog options) - jobs (for [line lines - :let [line (str/trim line)] - :when (and (seq line) (not (str/starts-with? line "#"))) - :let [[schedule name] (str/split line #"\|" 2) - cron (parse-cron (str/trim schedule))]] - (run-job! (str/trim name) cron prog))] - (println (format "[INFO] Loaded %d job(s), running..." (count (doall jobs)))) - ;; Block forever — futures run the jobs - @(promise)))) - -(apply -main *command-line-args*) - diff --git a/khal_notifier/rewrite_in_bb/khal_notifier.bb b/khal_notifier/rewrite_in_bb/khal_notifier.bb @@ -1,238 +0,0 @@ -#!/usr/bin/env bb - -(ns khal-notifier - (:require [clojure.string :as str] - [babashka.process :refer [shell]] - [babashka.fs :as fs])) - -(defn parse-duration [s] - "Parse duration string like '15m', '1h', '30s' into seconds." - (let [m (re-matches #"(\d+)([smh])" s)] - (if m - (let [value (parse-long (second m)) - unit (nth m 2)] - (case unit - "s" value - "m" (* value 60) - "h" (* value 3600))) - (throw (ex-info (str "Invalid duration format: " s) {:input s}))))) - -(defn run-cmd [cmd args & {:keys [timeout] :or {timeout 30000}}] - "Run a command with timeout. Returns {:status :ok, :out s, :code n} or {:status :error, :reason r}." - (try - (if-not (fs/which cmd) - {:status :error :reason :enoent} - (let [result (apply shell {:out :string :err :string :continue true :timeout timeout} - cmd args)] - {:status :ok :out (:out result) :code (:exit result)})) - (catch java.util.concurrent.TimeoutException _ - {:status :error :reason :timeout}) - (catch Exception e - {:status :error :reason e}))) - -(defn sync-vdirsyncer [] - "Sync calendars with vdirsyncer. Returns true on success." - (println "[INFO] Syncing calendars with vdirsyncer") - (let [result (run-cmd "vdirsyncer" ["sync"] :timeout 60000)] - (cond - (and (= :ok (:status result)) - (zero? (:code result))) - (do (println "[INFO] vdirsyncer sync complete") - true) - - (= :ok (:status result)) - (do (println (format "[WARN] vdirsyncer exited with code %d: %s" - (:code result) - (str/trim (or (:out result) "")))) - false) - - (= :enoent (:reason result)) - (do (println "[ERROR] vdirsyncer not found") - false) - - (= :timeout (:reason result)) - (do (println "[ERROR] vdirsyncer timed out after 60 seconds") - false) - - :else - (do (println (format "[ERROR] vdirsyncer failed: %s" - (ex-message (:reason result)))) - false)))) - -(defn get-upcoming-events [lookahead] - "Query khal for upcoming events. Returns vector of event maps." - (println (format "[INFO] Checking khal for events in the next %s..." lookahead)) - (let [result (run-cmd "khal" ["list" "now" lookahead "--format" "{start-time}|{end-time}|{title}|{organizer}"])] - (cond - (and (= :ok (:status result)) - (zero? (:code result))) - (let [lines (->> (:out result) - str/split-lines - (map str/trim) - (remove str/blank?) - (filter #(str/includes? % "|"))) - events (mapv (fn [line] - (let [parts (str/split line #"\|" 4) - cnt (count parts)] - (cond - (= cnt 4) - {:start-time (str/trim (nth parts 0)) - :end-time (str/trim (nth parts 1)) - :title (str/trim (nth parts 2)) - :organizer (str/trim (nth parts 3))} - - (= cnt 3) - {:start-time (str/trim (nth parts 0)) - :end-time (str/trim (nth parts 1)) - :title (str/trim (nth parts 2)) - :organizer ""} - - (= cnt 2) - {:start-time (str/trim (nth parts 0)) - :end-time "" - :title (str/trim (nth parts 1)) - :organizer ""} - - :else - {:start-time "" - :end-time "" - :title (str/trim (nth parts 0)) - :organizer ""}))) - lines) - events (remove #(str/blank? (:title %)) events)] - (println (format "[INFO] Found %d upcoming event(s)." (count events))) - events) - - (= :ok (:status result)) - (do (println (format "[WARN] khal exited with code %d: %s" - (:code result) - (str/trim (or (:out result) "")))) - []) - - (= :enoent (:reason result)) - (do (println "[ERROR] khal not found. Is it installed and on PATH?") - []) - - (= :timeout (:reason result)) - (do (println "[ERROR] khal timed out.") - []) - - :else - (do (println "[ERROR] khal execution failed.") - [])))) - -(defn now-time [] - "Get current time as HH:MM string." - (-> (shell {:out :string :continue true} "date" "+%H:%M") - :out - str/trim)) - -(defn filter-events [events] - "Filter out events that have already started." - (let [now (now-time)] - (remove #(and (not (str/blank? (:start-time %))) - (<= (compare (:start-time %) now) 0)) - events))) - -(defn format-event [event] - "Format an event for notification body." - (let [time-str (let [{:keys [start-time end-time]} event] - (cond - (and (str/blank? start-time) (str/blank? end-time)) - "All day" - (str/blank? end-time) - start-time - :else - (str start-time " - " end-time)))] - (if (and (contains? event :organizer) - (not (str/blank? (:organizer event)))) - (str time-str "\nOrganizer: " (:organizer event)) - time-str))) - -(defn send-notification [summary body urgency] - "Send a desktop notification using notify-send." - (println (format "[INFO] Notifying: %s — %s" summary body)) - (let [result (run-cmd "notify-send" - ["--app-name" "khal_notifier" - "--urgency" urgency - summary - body])] - (cond - (and (= :ok (:status result)) - (zero? (:code result))) - true - - (= :ok (:status result)) - (do (println (format "[ERROR] notify-send failed (code %d): %s" - (:code result) - (str/trim (or (:out result) "")))) - false) - - (= :enoent (:reason result)) - (do (println "[ERROR] notify-send not found") - false) - - :else - (do (println "[ERROR] notify-send failed") - false)))) - -(defn run-once [lookahead urgency skip-sync] - "Run a single check cycle." - (when-not skip-sync - (when-not (sync-vdirsyncer) - (println "[WARN] Sync failed — continuing with local calendar data"))) - - (let [events (get-upcoming-events lookahead) - filtered (filter-events events)] - (if (empty? filtered) - (println "[INFO] No upcoming events") - (doseq [event filtered] - (let [summary (:title event) - body (format-event event)] - (send-notification summary body urgency)))))) - -(defn run-with-refresh [seconds lookahead urgency skip-sync] - "Run continuously, checking every N seconds." - (println (format "[INFO] Running in refresh mode: checking every %d second(s)" seconds)) - (loop [iteration 0] - (when (pos? iteration) - (println (format "[INFO] Waiting %d second(s) before next refresh" seconds)) - (Thread/sleep (* seconds 1000))) - (println (format "[INFO] Run %d" (inc iteration))) - (run-once lookahead urgency skip-sync) - (recur (inc iteration)))) - -(defn print-usage [] - (println "Usage: khal_notifier [options]") - (println) - (println "Options:") - (println " --lookahead TIME How far ahead to check (default: 15m)") - (println " --urgency LEVEL Notification urgency (default: critical)") - (println " --skip-sync Skip vdirsyncer sync") - (println " --refresh SECONDS Run continuously, re-checking every N seconds (default: 300)") - (println " --help Show this help")) - -(defn parse-args [args] - (loop [pairs (seq args) - acc {}] - (if (empty? pairs) - acc - (let [k (first pairs) - v (second pairs)] - (recur (nnext pairs) - (if (nil? v) - (assoc acc k true) - (assoc acc k v))))))) - -(defn -main [& args] - (let [opts (parse-args args)] - (if (get opts "--help") - (do (print-usage) - (System/exit 0)) - (let [lookahead (get opts "--lookahead" "15m") - urgency (get opts "--urgency" "critical") - skip-sync (contains? opts "--skip-sync") - refresh (parse-long (get opts "--refresh" "300"))] - (run-with-refresh refresh lookahead urgency skip-sync))))) - -(apply -main *command-line-args*)