diff options
author | Marc Coquand <marc@mccd.space> | 2024-05-13 11:00:47 -0500 |
---|---|---|
committer | Marc Coquand <marc@mccd.space> | 2024-05-13 11:00:47 -0500 |
commit | 121a6a376209de3f5a9474bf03721e2032a73e01 (patch) | |
tree | 6f02ffa1695510aa9227a9290abac11811b25373 | |
download | stitch-121a6a376209de3f5a9474bf03721e2032a73e01.tar.gz stitch-121a6a376209de3f5a9474bf03721e2032a73e01.tar.bz2 stitch-121a6a376209de3f5a9474bf03721e2032a73e01.zip |
initial commit
Diffstat (limited to '')
-rw-r--r-- | .envrc | 3 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .ocamlformat | 3 | ||||
-rw-r--r-- | README.md | 76 | ||||
l--------- | bin/.#main.ml | 1 | ||||
-rw-r--r-- | bin/common.ml | 166 | ||||
-rw-r--r-- | bin/dune | 12 | ||||
-rw-r--r-- | bin/main.ml | 77 | ||||
-rw-r--r-- | dune-project | 24 | ||||
-rw-r--r-- | flake.lock | 189 | ||||
-rw-r--r-- | flake.nix | 63 | ||||
-rw-r--r-- | lib/dune | 7 | ||||
-rw-r--r-- | lib/stitch.ml | 69 | ||||
-rw-r--r-- | lib/zipper.ml | 22 | ||||
-rw-r--r-- | stitch.opam | 32 | ||||
-rw-r--r-- | test/dune | 2 | ||||
-rw-r--r-- | test/test_apbox.ml | 0 |
17 files changed, 748 insertions, 0 deletions
@@ -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 |