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:
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*)