aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc Coquand <marc@mccd.space>2024-05-13 11:00:47 -0500
committerMarc Coquand <marc@mccd.space>2024-05-13 11:00:47 -0500
commit121a6a376209de3f5a9474bf03721e2032a73e01 (patch)
tree6f02ffa1695510aa9227a9290abac11811b25373
downloadstitch-121a6a376209de3f5a9474bf03721e2032a73e01.tar.gz
stitch-121a6a376209de3f5a9474bf03721e2032a73e01.tar.bz2
stitch-121a6a376209de3f5a9474bf03721e2032a73e01.zip
initial commit
-rw-r--r--.envrc3
-rw-r--r--.gitignore2
-rw-r--r--.ocamlformat3
-rw-r--r--README.md76
l---------bin/.#main.ml1
-rw-r--r--bin/common.ml166
-rw-r--r--bin/dune12
-rw-r--r--bin/main.ml77
-rw-r--r--dune-project24
-rw-r--r--flake.lock189
-rw-r--r--flake.nix63
-rw-r--r--lib/dune7
-rw-r--r--lib/stitch.ml69
-rw-r--r--lib/zipper.ml22
-rw-r--r--stitch.opam32
-rw-r--r--test/dune2
-rw-r--r--test/test_apbox.ml0
17 files changed, 748 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..025e9f8
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,3 @@
+use flake .
+
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8267c6a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+_build/*
+.direnv
diff --git a/.ocamlformat b/.ocamlformat
new file mode 100644
index 0000000..6bf68c9
--- /dev/null
+++ b/.ocamlformat
@@ -0,0 +1,3 @@
+profile = janestreet
+parse-docstrings = true
+let-binding-spacing = sparse
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7af7768
--- /dev/null
+++ b/README.md
@@ -0,0 +1,76 @@
+Stitch - Note Composer
+
+Stitch is a minimal grep-based CLI tool for composing notes.
+It is built around the idea of writing notes separately and then
+using tags to compose these notes together.
+
+Stitch does not have any opinion about which file
+format you use for file capturing, use org, markdown, whatever you
+want. You can customize the grep command.
+
+Stitch limits itself only to note composing. For capturing notes, you
+will have to set up your own system.
+
+AIMS
+
+- Single binary
+- Minimal
+- Work with any file format
+
+CREDIT
+
+Stitch is based and largely a stripped down copy of the note composing
+system of Howm for Emacs.
+
+INSTALLATION
+
+Chmod +x the binary and put in PATH.
+
+Set the environment variables:
+
+STICH_DIRECTORY
+STICH_GREP_CMD (default "grep")
+STITCH_HEADLINE_PATTERN (default "^\* ")
+STITCH_TAG_PATTERN (default ":[a-z]:")
+
+SPEED UP
+
+Stitch uses grep by default. As your notes grow, this will
+become rather slow. To speed it up, you can replace it grep with ugrep.
+
+DEVELOPMENT
+
+To set up the project, easiest way is to just install Nix, direnv and
+enable nix flakes. Then to compile:
+
+dune build
+
+And to run:
+
+dune exec -- stitch
+
+
+RECIPES
+
+Building a journaling system
+---
+
+You can build a basic capture command using $EDITOR and date command:
+
+alias capture="JRNL=\"$STITCH_DIRECTORY/$(date +'%Y-%m-%d %H:%M').org\" echo '* :journal:' > $JRNL_FILE && $EDITOR $JRNL_FILE
+
+and then you can find your journal entries, automatically sorted by creation date with
+stitch and the journal tag:
+
+alias jrnl="stitch -t journal"
+
+LICENSE
+
+License is BSD-3-Clause
+
+KNOWN ISSUES
+
+- Resizing the screen when editor is open causes panic
+
+
+
diff --git a/bin/.#main.ml b/bin/.#main.ml
new file mode 120000
index 0000000..5a91e42
--- /dev/null
+++ b/bin/.#main.ml
@@ -0,0 +1 @@
+mccd@void.21115:1715189760 \ No newline at end of file
diff --git a/bin/common.ml b/bin/common.ml
new file mode 100644
index 0000000..adfeee6
--- /dev/null
+++ b/bin/common.ml
@@ -0,0 +1,166 @@
+(* Copyright (c) 2016-2017 David Kaloper Meršinjak. All rights reserved.
+ See LICENSE.md. *)
+
+open Notty
+open Notty.Infix
+
+let pow n e = int_of_float (float n ** float e)
+
+module List = struct
+ include List
+
+ let rec replicate n a = if n < 1 then [] else a :: replicate (n - 1) a
+ let rec range a b = if a > b then [] else a :: range (a + 1) b
+
+ let rec intersperse a = function
+ | ([] | [ _ ]) as t -> t
+ | x :: xs -> x :: a :: intersperse a xs
+
+
+ let rec take n = function
+ | x :: xs when n > 0 -> x :: take (pred n) xs
+ | _ -> []
+
+
+ let rec splitat n = function
+ | x :: xs when n > 0 ->
+ let a, b = splitat (pred n) xs in
+ x :: a, b
+ | xs -> [], xs
+
+
+ let rec chunks n xs =
+ match splitat n xs with
+ | a, [] -> [ a ]
+ | a, b -> a :: chunks n b
+
+
+ let rec zip xs ys =
+ match xs, ys with
+ | [], _ | _, [] -> []
+ | x :: xs, y :: ys -> (x, y) :: zip xs ys
+end
+
+module String = struct
+ include String
+
+ let repeat n str =
+ let b = Buffer.create 16 in
+ for _ = 1 to n do
+ Buffer.add_string b str
+ done;
+ Buffer.contents b
+end
+
+let tile w h i = I.tabulate w h (fun _ _ -> i)
+
+(** A few images used in several places. *)
+module Images = struct
+ let i1 =
+ I.(string A.(fg lightblack) "omgbbq" <-> string A.(fg white ++ bg red) "@")
+ <|> I.(pad ~t:2 @@ string A.(fg green) "xo")
+
+
+ let i2 = I.(hpad 1 1 (hcrop 1 1 @@ tile 3 3 i1) <|> i1)
+ let i3 = tile 5 5 i2
+
+ let i4 =
+ let i = I.(i3 <|> crop ~t:1 i3 <|> i3) in
+ I.(crop ~l:1 i <-> crop ~r:1 i <-> crop ~b:2 i)
+
+
+ let i5 = tile 5 1 List.(range 0 15 |> map (fun i -> I.pad ~t:i ~l:(i * 2) i2) |> I.zcat)
+ let c_gray_ramp = I.tabulate 24 1 (fun g _ -> I.string A.(bg (gray g)) " ")
+
+ let c_cube_ix =
+ I.tabulate 6 1
+ @@ fun r _ ->
+ I.hpad 0 1 @@ I.tabulate 6 6 @@ fun b g -> I.string A.(bg (rgb ~r ~g ~b)) " "
+
+
+ let c_cube_rgb =
+ let f x = [| 0x00; 0x5f; 0x87; 0xaf; 0xd7; 0xff |].(x) in
+ I.tabulate 6 1
+ @@ fun r _ ->
+ I.hpad 0 1
+ @@ I.tabulate 6 6
+ @@ fun b g -> I.string A.(bg (rgb_888 ~r:(f r) ~g:(f g) ~b:(f b))) " "
+
+
+ let c_rainbow w h =
+ let pi2 = 2. *. 3.14159 in
+ let pi2_3 = pi2 /. 3.
+ and f t off = (sin (t +. off) *. 128.) +. 128. |> truncate in
+ let color t = A.rgb_888 ~r:(f t (-.pi2_3)) ~g:(f t 0.) ~b:(f t pi2_3) in
+ I.tabulate (w - 1) 1
+ @@ fun x _ ->
+ let t = (pi2 *. float x /. float w) +. 3.7 in
+ I.char A.(bg (color t)) ' ' 1 h
+
+
+ (* U+25CF BLACK CIRCLE *)
+ let dot color = I.string (A.fg color) "●"
+
+ (* U+25AA BLACK SMALL SQUARE *)
+ let square color = I.string (A.fg color) "▪"
+
+ let rec cantor = function
+ | 0 -> square A.lightblue
+ | n ->
+ let sub = cantor (pred n) in
+ I.hcat (List.replicate (pow 3 n) (square A.lightblue))
+ <-> (sub <|> I.void (pow 3 (n - 1)) 0 <|> sub)
+
+
+ let checker n m i =
+ let w = I.width i in
+ I.(tile (n / 2) (m / 2) (hpad 0 w i <-> hpad w 0 i))
+
+
+ let checker1 = checker 20 20 I.(char A.(bg magenta) ' ' 2 1)
+
+ let rec sierp c n =
+ I.(
+ if n > 1
+ then (
+ let ss = sierp c (pred n) in
+ ss <-> (ss <|> ss))
+ else hpad 1 0 (square c))
+
+
+ let grid xxs = xxs |> List.map I.hcat |> I.vcat
+
+ let outline attr i =
+ let w, h = I.(width i, height i) in
+ let chr x = I.uchar attr (Uchar.of_int x) 1 1
+ and hbar = I.uchar attr (Uchar.of_int 0x2500) w 1
+ and vbar = I.uchar attr (Uchar.of_int 0x2502) 1 h in
+ let a, b, c, d = chr 0x256d, chr 0x256e, chr 0x256f, chr 0x2570 in
+ grid [ [ a; hbar; b ]; [ vbar; i; vbar ]; [ d; hbar; c ] ]
+end
+
+let halfblock = "▄"
+
+let pxmatrix w h f =
+ I.tabulate w h
+ @@ fun x y ->
+ let y = y * 2 in
+ I.string A.(bg (f x y) ++ fg (f x (y + 1))) halfblock
+
+
+module Term = Notty_unix.Term
+
+let simpleterm ~imgf ~f ~s =
+ let term = Term.create () in
+ let imgf (w, h) s = I.(string A.(fg lightblack) "[ESC quits.]" <-> imgf (w, h - 1) s) in
+ let rec go s =
+ Term.image term (imgf (Term.size term) s);
+ match Term.event term with
+ | `End | `Key (`Escape, []) | `Key (`ASCII 'C', [ `Ctrl ]) -> ()
+ | `Resize _ -> go s
+ | #Unescape.event as e ->
+ (match f s e with
+ | Some s -> go s
+ | _ -> ())
+ in
+ go s
diff --git a/bin/dune b/bin/dune
new file mode 100644
index 0000000..962b989
--- /dev/null
+++ b/bin/dune
@@ -0,0 +1,12 @@
+(executable
+ (public_name stitch)
+ (name main)
+ (libraries
+ stitch
+ unix
+ cmdliner
+ str
+ notty
+ notty.unix
+ shexp.process
+ lambda-term))
diff --git a/bin/main.ml b/bin/main.ml
new file mode 100644
index 0000000..404c63e
--- /dev/null
+++ b/bin/main.ml
@@ -0,0 +1,77 @@
+open Notty
+open Common
+
+let content = Stitch.get_headlines () |> Stitch.parse_headlines
+let content_pretty = content |> Stitch.pretty_format
+
+let rec main t (((x, y) as pos), scroll) =
+ let img =
+ let dot = I.string A.(fg black) ">" |> I.pad ~l:0 ~t:(y - scroll)
+ and elements =
+ Array.mapi
+ (fun i el -> I.strf ~attr:A.(fg black) "%s" el |> I.pad ~l:2 ~t:i)
+ (Array.to_seq content_pretty |> Seq.drop scroll |> Array.of_seq)
+ in
+ let open I in
+ Array.fold_left (fun sum el -> el </> sum) dot elements
+ in
+ let _, size_y = Term.size t in
+ Term.image t img;
+ let content_length = Array.length content_pretty in
+ let scroll_up () =
+ let scroll = if y - scroll = 0 then max (scroll - 1) 0 else scroll in
+ main t @@ ((x, max (y - 1) 0), scroll)
+ in
+ let scroll_down () =
+ let scroll = if y - scroll >= size_y - 1 then scroll + 1 else scroll in
+ main t @@ ((x, min (y + 1) content_length), scroll)
+ in
+ match Term.event t with
+ | `End | `Key (`Escape, []) | `Key (`ASCII 'q', []) | `Key (`ASCII 'C', [ `Ctrl ]) -> ()
+ | `Mouse (`Press (`Scroll s), _, _) ->
+ (match s with
+ | `Down -> scroll_down ()
+ | `Up -> scroll_up ())
+ | `Resize _ -> main t (pos, scroll)
+ | `Mouse ((`Press _ | `Drag), (_, y), _) -> main t ((0, min y content_length), scroll)
+ | `Key (`ASCII 'j', []) | `Key (`ASCII 'N', [ `Ctrl ]) -> scroll_down ()
+ | `Key (`ASCII 'k', []) | `Key (`ASCII 'P', [ `Ctrl ]) ->
+ let scroll = if y - scroll = 0 then max (scroll - 1) 0 else scroll in
+ main t @@ ((x, max (y - 1) 0), scroll)
+ | `Key (`Arrow d, _) ->
+ (match d with
+ | `Up -> scroll_up ()
+ | `Down -> scroll_down ()
+ | _ -> main t ((x, y), scroll))
+ | `Key (`ASCII 'e', []) | `Key (`Enter, []) ->
+ (* Editor might be set with extra args, in that case we need to separate these *)
+ let[@warning "-8"] (editor :: args) =
+ String.split_on_char ' ' (Sys.getenv "EDITOR")
+ in
+ let selected_file, _ = Array.get content y in
+ let full_path_file = Stitch.execution_directory ^ "/" ^ selected_file in
+ let full_args = Array.append (Array.of_list args) [| full_path_file |] in
+ Term.cursor t (Some (0, 0));
+ let _ =
+ Unix.create_process_env
+ editor
+ full_args
+ (Unix.environment ())
+ Unix.stdin
+ Unix.stdout
+ Unix.stderr
+ in
+ let rec run_editor () =
+ match Unix.wait () with
+ | _, _ ->
+ Term.cursor t None;
+ main t ((x, y), scroll)
+ (* Capture resizing events *)
+ | exception Unix.Unix_error (Unix.EINTR, _, _) -> run_editor ()
+ | exception Unix.Unix_error (_, _, _) -> failwith "ERROR"
+ in
+ run_editor ()
+ | _ -> main t (pos, scroll)
+
+
+let () = main (Term.create ()) ((0, 0), 0)
diff --git a/dune-project b/dune-project
new file mode 100644
index 0000000..b15e4b7
--- /dev/null
+++ b/dune-project
@@ -0,0 +1,24 @@
+(lang dune 3.11)
+
+(name stitch)
+
+(generate_opam_files true)
+
+
+(authors "Marc Coquand")
+
+(maintainers "Marc Coquand")
+
+(license BSD-3-Clause)
+
+(documentation https://url/to/documentation)
+
+(package
+ (name stitch)
+ (synopsis "A Note Composer")
+ (description "A minimal CLI tool that allows you to compose notes together. Useful as part of a bigger system for building a PKM.")
+ (depends ocaml dune cmdliner notty lambda-term shexp)
+ (tags
+ (productivity minimal)))
+
+; See the complete stanza docs at https://dune.readthedocs.io/en/stable/dune-files.html#dune-project
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..f50da5c
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,189 @@
+{
+ "nodes": {
+ "flake-compat": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1627913399,
+ "narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1710146030,
+ "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_2": {
+ "locked": {
+ "lastModified": 1638122382,
+ "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "74f7e4319258e287b0f9cb95426c9853b282730b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "mirage-opam-overlays": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1661959605,
+ "narHash": "sha256-CPTuhYML3F4J58flfp3ZbMNhkRkVFKmBEYBZY5tnQwA=",
+ "owner": "dune-universe",
+ "repo": "mirage-opam-overlays",
+ "rev": "05f1c1823d891ce4d8adab91f5db3ac51d86dc0b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "dune-universe",
+ "repo": "mirage-opam-overlays",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1682362401,
+ "narHash": "sha256-/UMUHtF2CyYNl4b60Z2y4wwTTdIWGKhj9H301EDcT9M=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "884ac294018409e0d1adc0cae185439a44bd6b0b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "opam-nix": {
+ "inputs": {
+ "flake-compat": "flake-compat",
+ "flake-utils": "flake-utils_2",
+ "mirage-opam-overlays": "mirage-opam-overlays",
+ "nixpkgs": "nixpkgs",
+ "opam-overlays": "opam-overlays",
+ "opam-repository": "opam-repository",
+ "opam2json": "opam2json"
+ },
+ "locked": {
+ "lastModified": 1715087815,
+ "narHash": "sha256-FjIg+rO+aIfVFzSbvNznVhCn/s2MS8HkPhht7LqzLlk=",
+ "owner": "tweag",
+ "repo": "opam-nix",
+ "rev": "d42a3b8f234dd8c922020f8b2f4e326406bf23d1",
+ "type": "github"
+ },
+ "original": {
+ "owner": "tweag",
+ "repo": "opam-nix",
+ "type": "github"
+ }
+ },
+ "opam-overlays": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1654162756,
+ "narHash": "sha256-RV68fUK+O3zTx61iiHIoS0LvIk0E4voMp+0SwRg6G6c=",
+ "owner": "dune-universe",
+ "repo": "opam-overlays",
+ "rev": "c8f6ef0fc5272f254df4a971a47de7848cc1c8a4",
+ "type": "github"
+ },
+ "original": {
+ "owner": "dune-universe",
+ "repo": "opam-overlays",
+ "type": "github"
+ }
+ },
+ "opam-repository": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1705008664,
+ "narHash": "sha256-TTjTal49QK2U0yVOmw6rJhTGYM7tnj3Kv9DiEEiLt7E=",
+ "owner": "ocaml",
+ "repo": "opam-repository",
+ "rev": "fa77046c6497f8ca32926acdb7eb1e61777d4c17",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ocaml",
+ "repo": "opam-repository",
+ "type": "github"
+ }
+ },
+ "opam2json": {
+ "inputs": {
+ "nixpkgs": [
+ "opam-nix",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1671540003,
+ "narHash": "sha256-5pXfbUfpVABtKbii6aaI2EdAZTjHJ2QntEf0QD2O5AM=",
+ "owner": "tweag",
+ "repo": "opam2json",
+ "rev": "819d291ea95e271b0e6027679de6abb4d4f7f680",
+ "type": "github"
+ },
+ "original": {
+ "owner": "tweag",
+ "repo": "opam2json",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": [
+ "opam-nix",
+ "nixpkgs"
+ ],
+ "opam-nix": "opam-nix"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..8f15492
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,63 @@
+{
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs";
+ opam-nix.url = "github:tweag/opam-nix";
+ flake-utils.url = "github:numtide/flake-utils";
+ nixpkgs.follows = "opam-nix/nixpkgs";
+ };
+ outputs = { self, flake-utils, opam-nix, nixpkgs }:
+ let package = "stitch";
+ in flake-utils.lib.eachDefaultSystem (system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ on = opam-nix.lib.${system};
+ devPackagesQuery = {
+ # You can add "development" packages here. They will get added to the devShell automatically.
+ ocaml-lsp-server = "*";
+ ocamlformat = "*";
+ ocamlfind = "*";
+ utop = "*";
+ odoc = "*";
+ };
+ query = devPackagesQuery // {
+ ## You can force versions of certain packages here, e.g:
+ ## - force the ocaml compiler to be taken from opam-repository:
+ ocaml-base-compiler = "*";
+ ## - or force the compiler to be taken from nixpkgs and be a certain version:
+ # ocaml-system = "4.14.0";
+ ## - or force ocamlfind to be a certain version:
+ };
+ scope = on.buildOpamProject' { } ./. query;
+ overlay = final: prev: {
+ # You can add overrides here
+ ${package} = prev.${package}.overrideAttrs (_: {
+ # Prevent the ocaml dependencies from leaking into dependent environments
+ doNixSupport = false;
+ buildInputs =
+ [ pkgs."gmp" pkgs."libev" pkgs."openssl" pkgs."libargon2" ];
+ DUNE_PROFILE = "release";
+ });
+ };
+ scope' = scope.overrideScope' overlay;
+ # The main package containing the executable
+ main = scope'.${package};
+ # Packages from devPackagesQuery
+ devPackages = builtins.attrValues
+ (pkgs.lib.getAttrs (builtins.attrNames devPackagesQuery) scope');
+ in {
+ legacyPackages = scope';
+
+ packages.default = main;
+
+ apps.stitch.default = {
+ type = "app";
+ program = "${main}/bin/stitch";
+ };
+
+ devShells.default = pkgs.mkShell {
+ inputsFrom = [ main ];
+ buildInputs = devPackages;
+ };
+ });
+}
+
diff --git a/lib/dune b/lib/dune
new file mode 100644
index 0000000..0ddfdce
--- /dev/null
+++ b/lib/dune
@@ -0,0 +1,7 @@
+(library
+ (name stitch)
+ (libraries
+ str
+ shexp.process
+ cmdliner
+ lambda-term))
diff --git a/lib/stitch.ml b/lib/stitch.ml
new file mode 100644
index 0000000..a6ba300
--- /dev/null
+++ b/lib/stitch.ml
@@ -0,0 +1,69 @@
+let execution_directory =
+ Sys.getenv_opt "STICH_DIRECTORY" |> Option.value ~default:"/home/mccd/notes-example"
+
+
+let grep_cmd = Sys.getenv_opt "STICH_GREP_CMD" |> Option.value ~default:"ugrep"
+
+let run_print ~dir args =
+ let open Shexp_process in
+ let open Shexp_process.Infix in
+ eval (chdir dir (call args |- read_all))
+
+
+let get_headlines () =
+ run_print
+ ~dir:execution_directory
+ [ grep_cmd; "^\\*"; "-H"; "-r"; "-n"; "--separator=|" ]
+
+
+exception Not_A_Tuple of string * string
+
+(** Returns a tuple of file name and Content *)
+let parse_headlines s =
+ String.split_on_char '\n' s
+ (* Testing in utop it seems like there is maybe a bug with bounded_split, 1 doesn't work for ':'. Therefore using a slower implementation. *)
+ |> List.filter_map (fun message ->
+ if String.equal message ""
+ then None
+ else (
+ let split = Str.bounded_split (Str.regexp "|") message 3 in
+ match split with
+ (* file, line, content *)
+ | [ file_name; _; content ] -> Some (file_name, content)
+ | _ -> raise (Not_A_Tuple (String.concat " SPLIT " split, message))))
+ |> Array.of_list
+
+
+(** Used for pretty printing *)
+let get_padding list =
+ Array.fold_left (fun n (file_name, _) -> Int.max n (String.length file_name)) 0 list
+
+
+let pad str n =
+ let padding = n - String.length str in
+ String.concat "" [ str; String.make padding ' ' ]
+
+
+(** Turns "2024-03-05.org:* Hello world" into "2024-03-05 | * Hello world" *)
+let pretty_format parsed_headlines =
+ let padding = get_padding parsed_headlines in
+ Array.map
+ (fun (file_name, content) -> String.concat " | " [ pad file_name padding; content ])
+ parsed_headlines
+
+
+(** Full body parsing *)
+
+let get_full_content () =
+ run_print
+ ~dir:execution_directory
+ [ grep_cmd; "^\\*"; "-h"; "-r"; "-n"; "-C"; "9999"; "--separator='|'" ]
+
+(* let parse_file_headline collection full = *)
+(* match full with *)
+(* | s :: r -> *)
+(* let split = Str.bounded_split (Str.regexp ":1:") s 1 in *)
+(* (match split with *)
+(* (\* file, line, content *\) *)
+(* | [ file_name; content ] -> file_name, content *)
+(* | rest -> *)
diff --git a/lib/zipper.ml b/lib/zipper.ml
new file mode 100644
index 0000000..37e14de
--- /dev/null
+++ b/lib/zipper.ml
@@ -0,0 +1,22 @@
+type zipper =
+ { above : string list
+ ; selected : string
+ ; below : string list
+ }
+
+let up (zipper : zipper) =
+ match zipper.above with
+ | [] -> zipper
+ | a :: rest -> { above = rest; selected = a; below = zipper.selected :: zipper.below }
+
+
+let down (zipper : zipper) =
+ match zipper.below with
+ | [] -> zipper
+ | a :: rest -> { below = rest; selected = a; above = zipper.selected :: zipper.above }
+
+
+let selected zipper = zipper.selected
+
+let to_array zipper =
+ List.concat [ zipper.above; [ zipper.selected ]; zipper.below ] |> Array.of_list
diff --git a/stitch.opam b/stitch.opam
new file mode 100644
index 0000000..0a77db1
--- /dev/null
+++ b/stitch.opam
@@ -0,0 +1,32 @@
+# This file is generated by dune, edit dune-project instead
+opam-version: "2.0"
+synopsis: "A Terminal Note Taking App"
+description: ""
+maintainer: ["Marc Coquand"]
+authors: ["Marc Coquand"]
+license: "GPL-3.0-only"
+tags: ["productivity" "minimal"]
+doc: "https://url/to/documentation"
+depends: [
+ "ocaml"
+ "dune" {>= "3.11"}
+ "cmdliner"
+ "notty"
+ "lambda-term"
+ "shexp"
+ "odoc" {with-doc}
+]
+build: [
+ ["dune" "subst"] {dev}
+ [
+ "dune"
+ "build"
+ "-p"
+ name
+ "-j"
+ jobs
+ "@install"
+ "@runtest" {with-test}
+ "@doc" {with-doc}
+ ]
+]
diff --git a/test/dune b/test/dune
new file mode 100644
index 0000000..d392dd2
--- /dev/null
+++ b/test/dune
@@ -0,0 +1,2 @@
+(test
+ (name test_apbox))
diff --git a/test/test_apbox.ml b/test/test_apbox.ml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/test_apbox.ml