a mood-tracker for myself

mood: init

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 2aecb91e

+4
.gitignore
··· 1 + dune.lock 2 + dev-tools.locks 3 + _build 4 + .direnv
.ocamlformat

This is a binary file and will not be displayed.

+6
bin/dune
··· 1 + (executable 2 + (public_name mood) 3 + (name main) 4 + (preprocess 5 + (pps ppx_let)) 6 + (libraries mood core core_unix.command_unix core_unix.sys_unix fpath))
+48
bin/main.ml
··· 1 + open Core 2 + open Mood 3 + 4 + let mood_file = 5 + let home = Fpath.v @@ Sys_unix.home_directory () in 6 + let f = Fpath.v ".mood" in 7 + Fpath.(to_string (home // f)) 8 + 9 + let parse_date d = 10 + let now = Date.of_time (Time_float.now ()) ~zone:Time_float.Zone.utc in 11 + match d with 12 + | "yesterday" | "y" -> Date.add_days now (-1) 13 + | "daybefore" | "db" -> Date.add_days now (-2) 14 + | d -> Date.of_string d 15 + 16 + let record = 17 + Command.basic ~summary:"record a mood" 18 + (let%map_open.Command value = anon ("value" %: int) 19 + and d = anon (maybe ("date" %: string)) in 20 + fun () -> 21 + Tracker.load mood_file 22 + |> Tracker.record 23 + ?day:(Option.map ~f:parse_date d) 24 + ~mood:(Val.of_int value) 25 + |> Tracker.store ~location:mood_file) 26 + 27 + let show = 28 + Command.basic ~summary:"show this year's mood graph" 29 + (let%map_open.Command () = return () in 30 + fun () -> Tracker.load mood_file |> Tracker.render |> Stdio.print_endline) 31 + 32 + let get = 33 + Command.basic ~summary:"get today's mood" 34 + (let%map_open.Command () = return () in 35 + fun () -> 36 + Tracker.load mood_file |> Tracker.today |> fun (_, v) -> 37 + Option.value_map ~default:"" ~f:Val.to_string v |> Stdio.print_endline) 38 + 39 + let random = 40 + Command.basic ~summary:"render a randomly generated mood graph" 41 + (let%map_open.Command () = return () in 42 + fun () -> Tracker.random () |> Tracker.render |> Stdio.print_endline) 43 + 44 + let mood = 45 + Command.group ~summary:"cli mood tracker; will this fad last? we shall see" 46 + [ ("record", record); ("show", show); ("random", random); ("get", get) ] 47 + 48 + let () = Command_unix.run ~version:"1.0" ~build_info:"RWO" mood
+23
dune-project
··· 1 + (lang dune 3.20) 2 + 3 + (name mood) 4 + 5 + (generate_opam_files true) 6 + 7 + (authors "Akshay Oppiliappan <me@oppi.li>") 8 + 9 + (maintainers "Akshay Oppiliappan <me@oppi.li>") 10 + 11 + (license LICENSE) 12 + 13 + (documentation https://url/to/documentation) 14 + 15 + (package 16 + (name mood) 17 + (synopsis "A short synopsis") 18 + (description "A longer description") 19 + (depends ocaml base core core_unix fpath) 20 + (tags 21 + ("add topics" "to describe" your project))) 22 + 23 + ; See the complete stanza docs at https://dune.readthedocs.io/en/stable/reference/dune-project/index.html
+48
flake.lock
··· 1 + { 2 + "nodes": { 3 + "gitignore": { 4 + "inputs": { 5 + "nixpkgs": [ 6 + "nixpkgs" 7 + ] 8 + }, 9 + "locked": { 10 + "lastModified": 1709087332, 11 + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 12 + "owner": "hercules-ci", 13 + "repo": "gitignore.nix", 14 + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 15 + "type": "github" 16 + }, 17 + "original": { 18 + "owner": "hercules-ci", 19 + "repo": "gitignore.nix", 20 + "type": "github" 21 + } 22 + }, 23 + "nixpkgs": { 24 + "locked": { 25 + "lastModified": 1767364772, 26 + "narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=", 27 + "owner": "nixos", 28 + "repo": "nixpkgs", 29 + "rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa", 30 + "type": "github" 31 + }, 32 + "original": { 33 + "owner": "nixos", 34 + "ref": "nixpkgs-unstable", 35 + "repo": "nixpkgs", 36 + "type": "github" 37 + } 38 + }, 39 + "root": { 40 + "inputs": { 41 + "gitignore": "gitignore", 42 + "nixpkgs": "nixpkgs" 43 + } 44 + } 45 + }, 46 + "root": "root", 47 + "version": 7 48 + }
+45
flake.nix
··· 1 + { 2 + inputs = { 3 + 4 + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 5 + 6 + gitignore = { 7 + url = "github:hercules-ci/gitignore.nix"; 8 + inputs.nixpkgs.follows = "nixpkgs"; 9 + }; 10 + 11 + }; 12 + 13 + outputs = 14 + { self 15 + , nixpkgs 16 + , gitignore 17 + }: 18 + let 19 + inherit (gitignore.lib) gitignoreSource; 20 + 21 + supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 22 + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 23 + nixpkgsFor = forAllSystems (system: 24 + import nixpkgs { 25 + inherit system; 26 + }); 27 + 28 + in 29 + { 30 + 31 + devShell = forAllSystems (system: 32 + let 33 + pkgs = nixpkgsFor."${system}"; 34 + in 35 + pkgs.mkShell { 36 + nativeBuildInputs = [ 37 + pkgs.ocaml 38 + pkgs.opam 39 + pkgs.ocamlPackages.utop 40 + pkgs.ocamlPackages.dune_3 41 + ]; 42 + }); 43 + }; 44 + } 45 +
+4
lib/dune
··· 1 + (library 2 + (name mood) 3 + (libraries base core) 4 + (preprocess (pps ppx_let)))
+19
lib/ordered_map.ml
··· 1 + open Base 2 + 3 + type ('a, 'b) t = { tree : ('a, 'b) Avltree.t; cmp : 'a -> 'a -> int } 4 + 5 + let create ~cmp = { tree = Avltree.empty; cmp } 6 + 7 + let add t ~key ~data = 8 + { 9 + t with 10 + tree = 11 + Avltree.add t.tree ~replace:true ~compare:t.cmp ~added:(ref true) ~key 12 + ~data; 13 + } 14 + 15 + let remove t ~key = 16 + { t with tree = Avltree.remove t.tree key ~removed:(ref true) ~compare:t.cmp } 17 + 18 + let find t ~key = Avltree.find t.tree key ~compare:t.cmp 19 + let iter t ~f = Avltree.iter t.tree ~f
+7
lib/ordered_map.mli
··· 1 + type ('a, 'b) t 2 + 3 + val create : cmp:('a -> 'a -> int) -> ('a, 'b) t 4 + val add : ('a, 'b) t -> key:'a -> data:'b -> ('a, 'b) t 5 + val remove : ('a, 'b) t -> key:'a -> ('a, 'b) t 6 + val find : ('a, 'b) t -> key:'a -> 'b option 7 + val iter : ('a, 'b) t -> f:(key:'a -> data:'b -> unit) -> unit
+112
lib/tracker.ml
··· 1 + open Core 2 + open Stdio 3 + open Base 4 + 5 + type mood = Val.t 6 + type t = (Date.t, mood) Ordered_map.t 7 + 8 + let now () = Date.today ~zone:Time_float.Zone.utc 9 + let mood_of_day t day = Ordered_map.find t ~key:day 10 + let create () = Ordered_map.create ~cmp:Core.Date.compare 11 + let record_day t ~day ~mood = Ordered_map.add t ~key:day ~data:mood 12 + 13 + let record ?day t ~mood = 14 + record_day t ~day:(Option.value day ~default:(now ())) ~mood 15 + 16 + let today t = 17 + let now = now () in 18 + (now, mood_of_day t now) 19 + 20 + let this_week t = 21 + let now = now () in 22 + let day_of_week = Day_of_week.to_int @@ Date.day_of_week now in 23 + let monday = Date.add_days now (-day_of_week) in 24 + let week = List.init 7 ~f:(fun i -> Date.add_days monday i) in 25 + List.map week ~f:(fun d -> (d, mood_of_day t d)) 26 + 27 + let this_month t = 28 + let now = now () in 29 + let y = Date.year now in 30 + let m = Date.month now in 31 + let first_day = Date.create_exn ~y ~m ~d:1 in 32 + let days_in_month = Date.days_in_month ~year:y ~month:m in 33 + let month = List.init days_in_month ~f:(fun i -> Date.add_days first_day i) in 34 + List.map month ~f:(fun d -> (d, mood_of_day t d)) 35 + 36 + let this_year t = 37 + let now = now () in 38 + let y = Date.year now in 39 + let first_day = Date.create_exn ~y ~m:Month.Jan ~d:1 in 40 + let days_in_year = if Date.is_leap_year ~year:y then 366 else 365 in 41 + let year = List.init days_in_year ~f:(fun i -> Date.add_days first_day i) in 42 + List.map year ~f:(fun d -> (d, mood_of_day t d)) 43 + 44 + let all t = 45 + let acc = ref [] in 46 + Ordered_map.iter t ~f:(fun ~key ~data -> acc := (key, data) :: !acc); 47 + List.rev !acc 48 + 49 + let entry_of_string l = 50 + match String.split_on_chars ~on:[ ',' ] l with 51 + | [ date; v ] -> Some (Date.of_string date, Val.of_string v) 52 + | _ -> None 53 + 54 + let entry_to_string (day, mood) = 55 + String.concat ~sep:"," [ Date.to_string day; Val.to_string mood ] 56 + 57 + let of_list ls = 58 + List.fold ~init:(create ()) 59 + ~f:(fun acc (day, mood) -> record_day acc ~day ~mood) 60 + ls 61 + 62 + let to_list = all 63 + 64 + let load location = 65 + try 66 + In_channel.with_file location ~f:(fun c -> 67 + List.filter_map (In_channel.input_lines c) ~f:entry_of_string) 68 + |> of_list 69 + with _ -> create () 70 + 71 + let store t ~location = 72 + Out_channel.write_lines location (List.map ~f:entry_to_string (all t)) 73 + 74 + let pp t = String.concat ~sep:"\n" @@ List.map ~f:entry_to_string (all t) 75 + 76 + let random () = 77 + Random.init 12334254; 78 + let now = now () in 79 + let y = Date.year now in 80 + let first_day = Date.create_exn ~y ~m:Month.Jan ~d:1 in 81 + let days_in_year = if Date.is_leap_year ~year:y then 366 else 365 in 82 + let year = List.init days_in_year ~f:(fun i -> Date.add_days first_day i) in 83 + List.map year ~f:(fun d -> (d, Val.of_int (Random.int 5))) |> of_list 84 + 85 + let render t = 86 + let vals = this_year t in 87 + let now = now () in 88 + let y = Date.year now in 89 + let start_date = Date.create_exn ~y ~m:Month.Jan ~d:1 in 90 + 91 + let start_dow = Date.day_of_week start_date |> Day_of_week.to_int in 92 + let pad_tracker n = 93 + List.init n ~f:(fun _ -> (Date.of_string "2000-01-01", None)) 94 + in 95 + let start_padding = pad_tracker start_dow in 96 + let total_len = List.length start_padding + List.length vals in 97 + let remainder = total_len % 7 in 98 + let end_padding = pad_tracker (7 - remainder) in 99 + let padded = start_padding @ vals @ end_padding in 100 + let weeks = List.chunks_of padded ~length:7 in 101 + let rows = List.transpose_exn weeks in 102 + let day_labels = [ "Sun"; ""; "Tue"; ""; "Thu"; ""; "Sat" ] in 103 + List.map2_exn day_labels rows ~f:(fun label row -> 104 + let blocks = 105 + List.map row ~f:(fun (_, mood) -> 106 + Option.value_map mood 107 + ~default:("\027[2m" ^ "·" ^ "\027[0m") 108 + ~f:Val.render) 109 + |> String.concat 110 + in 111 + sprintf "%-3s %s" label blocks) 112 + |> String.concat ~sep:"\n"
+19
lib/tracker.mli
··· 1 + open Base 2 + open Core 3 + 4 + type t 5 + 6 + val create : unit -> t 7 + val record : ?day:Date.t -> t -> mood:Val.t -> t 8 + val today : t -> Date.t * Val.t option 9 + val this_week : t -> (Date.t * Val.t option) list 10 + val this_month : t -> (Date.t * Val.t option) list 11 + val this_year : t -> (Date.t * Val.t option) list 12 + val of_list : (Date.t * Val.t) list -> t 13 + val to_list : t -> (Date.t * Val.t) list 14 + val all : t -> (Date.t * Val.t) list 15 + val load : string -> t 16 + val store : t -> location:string -> unit 17 + val pp : t -> string 18 + val render : t -> string 19 + val random : unit -> t
+25
lib/val.ml
··· 1 + open Base 2 + 3 + type t = int 4 + 5 + let of_int = Fn.id 6 + let to_int = Fn.id 7 + let of_string = Int.of_string 8 + let to_string = Int.to_string 9 + 10 + let render t = 11 + let value = to_int t in 12 + let scaled = Int.min 5 (Int.max 0 value) in 13 + let yellow = "\027[33m" in 14 + let reset = "\027[0m" in 15 + let block = 16 + match scaled with 17 + | 0 -> " " 18 + | 1 -> "░" 19 + | 2 -> "▒" 20 + | 3 -> "▓" 21 + | 4 -> "█" 22 + | 5 -> "█" 23 + | _ -> assert false 24 + in 25 + yellow ^ block ^ reset
+7
lib/val.mli
··· 1 + type t 2 + 3 + val of_int : int -> t 4 + val to_int : t -> int 5 + val of_string : string -> t 6 + val to_string : t -> string 7 + val render : t -> string
+33
mood.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "A short synopsis" 4 + description: "A longer description" 5 + maintainer: ["Akshay Oppiliappan <me@oppi.li>"] 6 + authors: ["Akshay Oppiliappan <me@oppi.li>"] 7 + license: "LICENSE" 8 + tags: ["add topics" "to describe" "your" "project"] 9 + doc: "https://url/to/documentation" 10 + depends: [ 11 + "dune" {>= "3.20"} 12 + "ocaml" 13 + "base" 14 + "core" 15 + "core_unix" 16 + "fpath" 17 + "odoc" {with-doc} 18 + ] 19 + build: [ 20 + ["dune" "subst"] {dev} 21 + [ 22 + "dune" 23 + "build" 24 + "-p" 25 + name 26 + "-j" 27 + jobs 28 + "@install" 29 + "@runtest" {with-test} 30 + "@doc" {with-doc} 31 + ] 32 + ] 33 + x-maintenance-intent: ["(latest)"]
+6
readme.txt
··· 1 + mood 2 + ---- 3 + 4 + i am attempting mood tracking this year. not sure if it 5 + really helps achieve anything, but i want to try it 6 + nonetheless!
+2
test/dune
··· 1 + (test 2 + (name test_mood))
test/test_mood.ml

This is a binary file and will not be displayed.