diff --git a/.envrc.example b/.envrc.example new file mode 100644 index 000000000..cc5c18b36 --- /dev/null +++ b/.envrc.example @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +# `use devenv` supports the same options as the `devenv shell` command. +# +# To silence all output, use `--quiet`. +# +# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true +use devenv diff --git a/.gitignore b/.gitignore index 64495397c..4a6782147 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,20 @@ doc/tags # Lock file .org-ts-lock.json + +# Nvim files (for project scoped sessions / scratches etc --- used by `np`) +.nvim/ + +# Personal orgfiles (for project scoped tasks etc --- used by `np`) +orgfiles/ + +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml +.pre-commit-config.yaml + +# direnv +.direnv +.envrc* +!.envrc.example diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 000000000..77fd2b2f3 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,730 @@ +{ + "nodes": { + "alejandra": { + "inputs": { + "fenix": "fenix", + "flakeCompat": "flakeCompat", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1775937104, + "owner": "kamadorueda", + "repo": "alejandra", + "rev": "6d808e1c36b430b1e4762085912db3ccf5b2efdf", + "type": "github" + }, + "original": { + "owner": "kamadorueda", + "repo": "alejandra", + "type": "github" + } + }, + "build-systems": { + "inputs": { + "nixpkgs": [ + "np", + "proselint", + "nixpkgs" + ], + "pyproject-nix": [ + "np", + "proselint", + "pyproject" + ], + "uv2nix": [ + "np", + "proselint", + "uv" + ] + }, + "locked": { + "lastModified": 1773870109, + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "b6e74f433b02fa4b8a7965ee24680f4867e2926f", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1776080969, + "owner": "cachix", + "repo": "devenv", + "rev": "8d558a84fa38242a7f13781670fee1a6a8902b48", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "fenix": { + "inputs": { + "nixpkgs": [ + "np", + "alejandra", + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1776068450, + "owner": "nix-community", + "repo": "fenix", + "rev": "a8c773e277756e481d946637e6fac860bb429bc8", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixvim", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775087534, + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ + "np", + "nixvim", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775087534, + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_3" + }, + "locked": { + "lastModified": 1731533236, + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "inputs": { + "systems": "systems_4" + }, + "locked": { + "lastModified": 1731533236, + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flakeCompat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775585728, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "580633fa3fe5fc0379905986543fd7495481913d", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1762808025, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gitignore_2": { + "inputs": { + "nixpkgs": [ + "np", + "proselint", + "hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1762808025, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "hooks": { + "inputs": { + "flake-compat": "flake-compat_2", + "gitignore": "gitignore_2", + "nixpkgs": [ + "np", + "proselint", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775585728, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "580633fa3fe5fc0379905986543fd7495481913d", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "ixx": { + "inputs": { + "flake-utils": [ + "nixvim", + "nuschtosSearch", + "flake-utils" + ], + "nixpkgs": [ + "nixvim", + "nuschtosSearch", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1754860581, + "owner": "NuschtOS", + "repo": "ixx", + "rev": "babfe85a876162c4acc9ab6fb4483df88fa1f281", + "type": "github" + }, + "original": { + "owner": "NuschtOS", + "ref": "v0.1.1", + "repo": "ixx", + "type": "github" + } + }, + "ixx_2": { + "inputs": { + "flake-utils": [ + "np", + "nixvim", + "nuschtosSearch", + "flake-utils" + ], + "nixpkgs": [ + "np", + "nixvim", + "nuschtosSearch", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1754860581, + "owner": "NuschtOS", + "repo": "ixx", + "rev": "babfe85a876162c4acc9ab6fb4483df88fa1f281", + "type": "github" + }, + "original": { + "owner": "NuschtOS", + "ref": "v0.1.1", + "repo": "ixx", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1776067740, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7e495b747b51f95ae15e74377c5ce1fe69c1765f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1776133771, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ea642f52356449ad5d5767ad0f2e39c54a2f9471", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable_2": { + "locked": { + "lastModified": 1776134871, + "owner": "nixos", + "repo": "nixpkgs", + "rev": "4fb204eee7ee0a00ce3e967df5ebff5f6333dad6", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1776030597, + "owner": "nixos", + "repo": "nixpkgs", + "rev": "c88e63f4caf12c731f61ce71f300680ce73c180e", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable-small", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1776067740, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7e495b747b51f95ae15e74377c5ce1fe69c1765f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixvim": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": [ + "nixpkgs" + ], + "nuschtosSearch": "nuschtosSearch", + "systems": "systems_2" + }, + "locked": { + "lastModified": 1769049374, + "owner": "nix-community", + "repo": "nixvim", + "rev": "b8f76bf5751835647538ef8784e4e6ee8deb8f95", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "nixos-25.11", + "repo": "nixvim", + "type": "github" + } + }, + "nixvim_2": { + "inputs": { + "flake-parts": "flake-parts_2", + "nixpkgs": [ + "np", + "nixpkgs" + ], + "nuschtosSearch": "nuschtosSearch_2", + "systems": "systems_5" + }, + "locked": { + "lastModified": 1769049374, + "owner": "nix-community", + "repo": "nixvim", + "rev": "b8f76bf5751835647538ef8784e4e6ee8deb8f95", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "nixos-25.11", + "repo": "nixvim", + "type": "github" + } + }, + "np": { + "inputs": { + "alejandra": "alejandra", + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-unstable": "nixpkgs-unstable_2", + "nixvim": "nixvim_2", + "proselint": "proselint" + }, + "locked": { + "lastModified": 1776074002, + "owner": "ar-at-localhost", + "repo": "np", + "rev": "b813abb9a129616a2f56f3459dc8de07cc740699", + "type": "github" + }, + "original": { + "owner": "ar-at-localhost", + "ref": "nixos-25.11", + "repo": "np", + "type": "github" + } + }, + "nuschtosSearch": { + "inputs": { + "flake-utils": "flake-utils", + "ixx": "ixx", + "nixpkgs": [ + "nixvim", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1776097945, + "owner": "NuschtOS", + "repo": "search", + "rev": "d15c05d20b434704c3e84f9dea161b8184b6643d", + "type": "github" + }, + "original": { + "owner": "NuschtOS", + "repo": "search", + "type": "github" + } + }, + "nuschtosSearch_2": { + "inputs": { + "flake-utils": "flake-utils_3", + "ixx": "ixx_2", + "nixpkgs": [ + "np", + "nixvim", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1776097945, + "owner": "NuschtOS", + "repo": "search", + "rev": "d15c05d20b434704c3e84f9dea161b8184b6643d", + "type": "github" + }, + "original": { + "owner": "NuschtOS", + "repo": "search", + "type": "github" + } + }, + "proselint": { + "inputs": { + "build-systems": "build-systems", + "hooks": "hooks", + "nixpkgs": "nixpkgs_3", + "pyproject": "pyproject", + "uv": "uv" + }, + "locked": { + "lastModified": 1769449807, + "owner": "amperser", + "repo": "proselint", + "rev": "79b33e728a385d6244a993c5e7f2f94c98cc881d", + "type": "github" + }, + "original": { + "owner": "amperser", + "repo": "proselint", + "type": "github" + } + }, + "pyproject": { + "inputs": { + "nixpkgs": [ + "np", + "proselint", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1776120154, + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "29dc4e9960d2b7f122b52b155e0e8f87cd5c5c08", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable", + "nixvim": "nixvim", + "np": "np", + "pre-commit-hooks": [ + "git-hooks" + ] + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1775979430, + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "7b6e1249b7320e16792e31ce67bb2e5f4acd6a8b", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_4": { + "locked": { + "lastModified": 1681028828, + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_5": { + "locked": { + "lastModified": 1681028828, + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "uv": { + "inputs": { + "nixpkgs": [ + "np", + "proselint", + "nixpkgs" + ], + "pyproject-nix": [ + "np", + "proselint", + "pyproject" + ] + }, + "locked": { + "lastModified": 1776114780, + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "73ff87a3e489b07b9cf842f917963a9e40d49225", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 000000000..a18abc4a9 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,53 @@ +{ + pkgs, + lib, + inputs, + config, + ... +}: { + # To enable: set `use-nvim = true` in `devenv.local.nix` + options.use-nvim = lib.mkOption { + type = lib.types.bool; + description = "Configure an in-repo Nvim instance"; + default = false; + }; + + config = let + inherit (pkgs.stdenv.hostPlatform) system; + use-nvim = config.use-nvim or false; + in { + packages = with pkgs; + [gnumake lemmy-help stylua alejandra] + ++ ( + if use-nvim + then [ + (import ./nix/nixvim.nix { + inherit system pkgs; + inherit (inputs) nixvim np; + }) + ] + else [neovim] + ); + + git-hooks = { + hooks = { + alejandra.enable = true; + stylua.enable = true; + }; + }; + + enterShell = '' + echo " Nvim Orgmode development environment" + echo " $(nvim --version | head -n1)" + ''; + + enterTest = '' + export LUA_PATH="$(pwd)/lua/?.lua;$(pwd)/lua/?/init.lua;$(pwd)/tests/?.lua;$(pwd)/tests/?/init.lua;;" + make test + ''; + + tasks."orgmode:gen-api-docs" = { + exec = "make api_docs"; + }; + }; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 000000000..4724aaef4 --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,15 @@ +inputs: + nixpkgs: + url: github:NixOS/nixpkgs/nixos-25.11 + nixpkgs-unstable: + url: github:NixOS/nixpkgs + nixvim: + url: github:nix-community/nixvim/nixos-25.11 + inputs: + nixpkgs: + follows: nixpkgs + np: + url: github:ar-at-localhost/np/nixos-25.11 + inputs: + nixpkgs: + follows: nixpkgs diff --git a/doc/orgmode_api.txt b/doc/orgmode_api.txt index 870f60117..d12a6496b 100644 --- a/doc/orgmode_api.txt +++ b/doc/orgmode_api.txt @@ -90,6 +90,12 @@ OrgFile:get_link() *OrgFile:get_link* @return string +OrgFile:insert_headline() *OrgFile:insert_headline* + Insert a headline to the file + @param headline OrgApiHeadline | OrgHeadline | { level: integer, keyword?: string, text: string } Headline to be inserted + @param after? OrgApiHeadline | OrgHeadline Optional headline to put after + + OrgApiHeadline *OrgApiHeadline* Fields: ~ @@ -211,6 +217,39 @@ OrgHeadline:get_link() *OrgHeadline:get_link* @return string +OrgHeadline:set_todo() *OrgHeadline:set_todo* + Set todo keyword + + +OrgHeadline:clock_in({opts?}) *OrgHeadline:clock_in* + Clock-in the headline + + Parameters: ~ + {opts?} (OrgHeadlineClockInOpts) Clock in options + + +OrgHeadline:clock_out({opts?}) *OrgHeadline:clock_out* + Clock-out in the headline + + Parameters: ~ + {opts?} (OrgHeadlineClockOutOpts) Clock out options + + +OrgHeadline:is_clocked_in() *OrgHeadline:is_clocked_in* + Check if the headline is currently clocked in or not + + +OrgHeadline:toggle_clock({opts?}) *OrgHeadline:toggle_clock* + Toggle clock the headline (Clock in if currently not, clock out otherwise) + + Parameters: ~ + {opts?} (OrgHeadlineClockOpts) Clock out options + + +OrgHeadline:cancel_active_clock() *OrgHeadline:cancel_active_clock* + Cancel active clock on the headline (if any) + + OrgApiAgenda *OrgApiAgenda* diff --git a/lua/orgmode/api/file.lua b/lua/orgmode/api/file.lua index a3a322aef..5044f0495 100644 --- a/lua/orgmode/api/file.lua +++ b/lua/orgmode/api/file.lua @@ -128,4 +128,57 @@ function OrgFile:get_link() return org.links:get_link_to_file(self._file) end +--- Insert a headline to the file +--- @param headline OrgApiHeadline | OrgHeadline | { level: integer, keyword?: string, text: string } Headline to be inserted +--- @param after? OrgApiHeadline | OrgHeadline Optional headline to put after +function OrgFile:insert_headline(headline, after) + local bufnr = vim.fn.bufnr(self.filename) + local created = bufnr == -1 + + if created then + bufnr = vim.fn.bufadd(self.filename) + vim.bo[bufnr].swapfile = false + vim.fn.bufload(bufnr) + end + + local insert_at + + if after then + insert_at = after.position.end_line + else + local last = self.headlines[#self.headlines] + if last then + insert_at = last.position.end_line + else + insert_at = 0 + end + end + + local lines + if headline.node then + local text = vim.treesitter.get_node_text(headline:node(), bufnr) + lines = vim.split(text, '\n', { plain = true }) + else + local stars = string.rep('*', headline.level or 1) + local keyword = headline.keyword and (' ' .. headline.keyword) or '' + lines = { stars .. keyword .. ' ' .. headline.text } + end + + vim.api.nvim_buf_set_lines(bufnr, insert_at, insert_at, false, lines) + + if created then + vim.api.nvim_buf_call(bufnr, function() + if vim.bo.modified then + vim.cmd('noautocmd silent! write') + end + end) + vim.api.nvim_buf_delete(bufnr, { unload = true }) + end + + local reloaded = self:reload() + local hl = reloaded:get_headline_on_line(insert_at + 1) + + return hl or reloaded.headlines[#reloaded.headlines] +end + return OrgFile diff --git a/lua/orgmode/api/headline.lua b/lua/orgmode/api/headline.lua index 698660ad7..86cf9c92c 100644 --- a/lua/orgmode/api/headline.lua +++ b/lua/orgmode/api/headline.lua @@ -291,4 +291,76 @@ function OrgHeadline:get_link() return org.links:get_link_to_headline(self._section) end +---Set todo keyword +function OrgHeadline:set_todo(keyword) + self:_do_action(function() + local headline = org.files:get_closest_headline() + headline:set_todo(keyword) + end) +end + +---Clock-in the headline +---@param opts? OrgHeadlineClockInOpts Clock in options +function OrgHeadline:clock_in(opts) + return self:_do_action(function() + local headline = org.files:get_closest_headline() + + if headline:is_clocked_in() then + return + end + + headline:clock_in(opts) + end) +end + +---Clock-out in the headline +---@param opts? OrgHeadlineClockOutOpts Clock out options +function OrgHeadline:clock_out(opts) + ---@diagnostic disable-next-line: invisible + return self:_do_action(function() + local headline = org.files:get_closest_headline() + + if not headline:is_clocked_in() then + return + end + + headline:clock_out(opts) + end) +end + +---Check if the headline is currently clocked in or not +function OrgHeadline:is_clocked_in() + local clock_in + + return self + :_do_action(function() + local headline = org.files:get_closest_headline() + clock_in = headline:is_clocked_in() + end) + :next(function() + return clock_in + end) +end + +---Toggle clock the headline (Clock in if currently not, clock out otherwise) +---@param opts? OrgHeadlineClockOpts Clock out options +function OrgHeadline:toggle_clock(opts) + return self:_do_action(function() + local headline = org.files:get_closest_headline() + return headline:is_clocked_in() + ---@cast opts OrgHeadlineClockOutOpts + and headline:clock_out(opts) + ---@cast opts OrgHeadlineClockInOpts + or headline:clock_in(opts) + end) +end + +---Cancel active clock on the headline (if any) +function OrgHeadline:cancel_active_clock() + self:_do_action(function() + local headline = org.files:get_closest_headline() + return headline:is_clocked_in() and headline:cancel_active_clock() + end) +end + return OrgHeadline diff --git a/lua/orgmode/files/headline.lua b/lua/orgmode/files/headline.lua index 712cab68b..df5d8bb1b 100644 --- a/lua/orgmode/files/headline.lua +++ b/lua/orgmode/files/headline.lua @@ -150,21 +150,32 @@ function Headline:is_clocked_in() return logbook and logbook:is_active() or false end -function Headline:clock_in() +---@class OrgHeadlineClockOpts +---@field silent? boolean Omit clock-in notification, by setting to truthy value +---@class OrgHeadlineClockInOpts:OrgHeadlineClockOpts +---@class OrgHeadlineClockOutOpts:OrgHeadlineClockOpts + +---@param opts? OrgHeadlineClockInOpts clock-in options +function Headline:clock_in(opts) local logbook = self:get_logbook() if not logbook then logbook = Logbook.new_from_headline(self) end logbook:add_clock_in() - EventManager.dispatch(events.ClockedIn:new(self)) + if not opts or not opts.silent then + EventManager.dispatch(events.ClockedIn:new(self)) + end return self:refresh() end -function Headline:clock_out() +---@param opts? OrgHeadlineClockOutOpts clock-out options +function Headline:clock_out(opts) local logbook = self:get_logbook() if logbook then logbook:clock_out() - EventManager.dispatch(events.ClockedOut:new(self)) + if not opts or not opts.silent then + EventManager.dispatch(events.ClockedOut:new(self)) + end end return self:refresh() end diff --git a/nix/nixvim.nix b/nix/nixvim.nix new file mode 100644 index 000000000..da1352b54 --- /dev/null +++ b/nix/nixvim.nix @@ -0,0 +1,21 @@ +{ + system, + pkgs, + nixvim, + np, + ... +}: (nixvim.legacyPackages.${system}.makeNixvimWithModule { + inherit pkgs; + + module = { + imports = [ + np.nixvimModules.base + np.nixvimModules.xtras.orgmode + ]; + }; + + extraSpecialArgs = { + inherit (pkgs) stdenv; + inherit np; + }; +}) diff --git a/tests/plenary/api/api_spec.lua b/tests/plenary/api/api_spec.lua index 8c0748086..1e9b4be99 100644 --- a/tests/plenary/api/api_spec.lua +++ b/tests/plenary/api/api_spec.lua @@ -439,6 +439,84 @@ describe('Api', function() assert.is.Nil(closest_headline) end) + describe('insert_headline', function() + it('should insert headline from plain object', function() + local file = helpers.create_agenda_file({ + '* TODO Existing headline', + }) + + local inserted = api.load(file.filename):insert_headline({ + level = 1, + keyword = 'TODO', + text = 'New inserted task', + }) + + assert.is_not_nil(inserted) + assert.are.same('New inserted task', inserted.title) + assert.are.same('TODO', inserted.todo_value) + + local reloaded = api.load(file.filename) + assert.are.same(2, #reloaded.headlines) + assert.are.same('New inserted task', reloaded.headlines[2].title) + end) + + it('should insert headline with level and text only', function() + local file = helpers.create_agenda_file({ + '* TODO Existing headline', + }) + + local inserted = api.load(file.filename):insert_headline({ + level = 2, + text = 'Level 2 task', + }) + + assert.is_not_nil(inserted) + assert.are.same(2, inserted.level) + assert.is_nil(inserted.todo_value) + + local reloaded = api.load(file.filename) + assert.are.same('** Level 2 task', reloaded.headlines[2].line) + end) + + it('should insert headline after another headline', function() + local file = helpers.create_agenda_file({ + '* TODO First headline', + '* TODO Third headline', + }) + + local reloaded = api.load(file.filename) + local inserted = api.load(file.filename):insert_headline({ + level = 1, + text = 'Second headline', + }, reloaded.headlines[1]) + + assert.is_not_nil(inserted) + assert.are.same('Second headline', inserted.title) + + local final = api.load(file.filename) + assert.are.same(3, #final.headlines) + assert.are.same('First headline', final.headlines[1].title) + assert.are.same('Second headline', final.headlines[2].title) + assert.are.same('Third headline', final.headlines[3].title) + end) + + it('should insert at end when no after param', function() + local file = helpers.create_agenda_file({ + '* TODO First headline', + '* TODO Second headline', + }) + + api.load(file.filename):insert_headline({ + level = 1, + text = 'Appended headline', + }) + + local reloaded = api.load(file.filename) + assert.are.same(3, #reloaded.headlines) + assert.are.same('Appended headline', reloaded.headlines[3].title) + end) + end) + describe('Refile', function() describe('from org file', function() it('should refile a headline to another file', function() diff --git a/tests/plenary/files/headline_spec.lua b/tests/plenary/files/headline_spec.lua index 9bc1ea06c..b3964ddf8 100644 --- a/tests/plenary/files/headline_spec.lua +++ b/tests/plenary/files/headline_spec.lua @@ -287,4 +287,86 @@ describe('Headline', function() end) end) end) + + describe('clock', function() + it('should cancel active clock and remove the clock entry from logbook', function() + local file = helpers.create_file({ + '* TODO Test clock cancel', + ' :LOGBOOK:', + ' CLOCK: [2024-05-22 Wed 05:15]', + ' :END:', + }, 'clock_cancel.org') + + local headline = file:get_headlines()[1] + assert.is_true(headline:is_clocked_in()) + + headline:cancel_active_clock() + + local lines = file:reload_sync().lines + assert.are.same({ + '* TODO Test clock cancel', + }, lines) + end) + + it('should clock in with silent option and not dispatch event', function() + local file = helpers.create_file({ + '* TODO Test silent clock in', + }, 'silent_clock_in.org') + + local EventManager = require('orgmode.events') + local events = EventManager.event + + local received_event = nil + local listener = function(event) + received_event = event + end + EventManager.listen(events.ClockedIn, listener) + + local headline = file:get_headlines()[1] + headline:clock_in({ silent = true }) + + assert.is_nil(received_event) + + -- cleanup listener + local listeners = EventManager._listeners[events.ClockedIn.type] + for i, l in ipairs(listeners) do + if l == listener then + table.remove(listeners, i) + break + end + end + end) + + it('should clock out with silent option and not dispatch event', function() + local file = helpers.create_file({ + '* TODO Test silent clock out', + ' :LOGBOOK:', + ' CLOCK: [2024-05-22 Wed 05:15]', + ' :END:', + }, 'silent_clock_out.org') + + local EventManager = require('orgmode.events') + local events = EventManager.event + + local received_event = nil + local listener = function(event) + received_event = event + end + EventManager.listen(events.ClockedOut, listener) + + local headline = file:get_headlines()[1] + headline:clock_out({ silent = true }) + + assert.is_nil(received_event) + + -- cleanup listener + local listeners = EventManager._listeners[events.ClockedOut.type] + for i, l in ipairs(listeners) do + if l == listener then + table.remove(listeners, i) + break + end + end + end) + end) end) diff --git a/tests/plenary/ui/clock_spec.lua b/tests/plenary/ui/clock_spec.lua index 385c6f5df..9919e23cf 100644 --- a/tests/plenary/ui/clock_spec.lua +++ b/tests/plenary/ui/clock_spec.lua @@ -244,6 +244,33 @@ describe('Clock', function() end end) + it('should not dispatch ClockedIn event when silent option is true', function() + local file = helpers.create_agenda_file({ + '* TODO Silent clock in test', + }) + + local received_event = nil + local listener = function(event) + received_event = event + end + EventManager.listen(events.ClockedIn, listener) + + vim.cmd('edit ' .. file.filename) + local headline = file:get_headlines()[1] + headline:clock_in({ silent = true }) + + assert.is_nil(received_event) + + -- cleanup listener + local listeners = EventManager._listeners[events.ClockedIn.type] + for i, l in ipairs(listeners) do + if l == listener then + table.remove(listeners, i) + break + end + end + end) + it('should dispatch ClockedOut event when clocking out', function() local file = helpers.create_agenda_file({ '* TODO Clock out event test', @@ -277,6 +304,36 @@ describe('Clock', function() end end) + it('should not dispatch ClockedOut event when silent option is true', function() + local file = helpers.create_agenda_file({ + '* TODO Silent clock out test', + }) + + vim.cmd('edit ' .. file.filename) + local headline = file:get_headlines()[1] + headline:clock_in() + vim.wait(100) + + local received_event = nil + local listener = function(event) + received_event = event + end + EventManager.listen(events.ClockedOut, listener) + + headline:clock_out({ silent = true }) + + assert.is_nil(received_event) + + -- cleanup listener + local listeners = EventManager._listeners[events.ClockedOut.type] + for i, l in ipairs(listeners) do + if l == listener then + table.remove(listeners, i) + break + end + end + end) + it('should not dispatch ClockedOut event when headline has no logbook', function() local file = helpers.create_agenda_file({ '* TODO No logbook headline',