~bmp/hayom

50291d6d9058b97ef80b858537cc34983a6dc0ad — Benjamin Pollack 7 months ago e10d227
allow editing existing entries

Implements: https://todo.sr.ht/~bmp/hayom/4
4 files changed, 72 insertions(+), 26 deletions(-)

M cli.ts
M config.ts
M deps.ts
M mod.ts
M cli.ts => cli.ts +43 -9
@@ 1,13 1,15 @@
import { DateTime, parseDate } from "./deps.ts";
import { flags } from "./deps.ts";
import { flags, partition } from "./deps.ts";
import { loadConfig } from "./config.ts";
import {
  Entry,
  filterEntries,
  FilterParams,
  makeEntry,
  matchesFilter,
  parseEntries,
  printEntry,
  renderEntry,
  saveEntries,
} from "./mod.ts";



@@ 48,11 50,33 @@ async function edit(
  }
}

async function editFilteredEntries(
  entries: Entry[],
  filter: FilterParams,
  editor: string[],
): Promise<Entry[] | undefined> {
  const [toEdit, toKeep] = partition(entries, (e) => matchesFilter(e, filter));
  const temp = Deno.makeTempFileSync({ suffix: ".hayom" });
  try {
    Deno.writeTextFileSync(temp, toEdit.map(renderEntry).join("\n"));
    const proc = Deno.run({ cmd: [...editor, temp] });
    const status = await proc.status();
    if (status.success) {
      const rawEntries = Deno.readTextFileSync(temp).replace("\r", "");
      const newEntries = parseEntries(rawEntries);
      return [...toKeep, ...newEntries];
    }
  } finally {
    Deno.remove(temp);
  }
}

function printHelp() {
  console.log(`
usage: hayom [-j journal] ...
options:
   --count | -n:   number of entries to print
   --edit | -e:    edit entries
   --from | -f:    from timestamp
   --journal | -j: journal to use
   --on:           on timestamp


@@ 68,6 92,7 @@ async function main() {
  const opts = flags.parse(args, {
    boolean: ["summary"],
    alias: {
      "e": ["edit"],
      "f": ["from"],
      "j": ["journal"],
      "n": ["count"],


@@ 92,7 117,7 @@ async function main() {
    } else throw e;
  }

  const entries = parseEntries(Deno.readTextFileSync(path));
  const entries = parseEntries(Deno.readTextFileSync(path).replace("\r", ""));

  if (
    ["from", "f", "to", "t", "on", "count", "n"].some((arg) => arg in opts) ||


@@ 111,16 136,25 @@ async function main() {
    const tags = <string[]> opts._.filter((arg) =>
      typeof arg === "string" && arg.match(/^@./)
    );
    printFilteredEntries(entries, {
      from,
      to,
      tags,
      limit: opts.count,
    }, opts.summary);

    const filter = { from, to, tags, limit: opts.count };

    if (opts.edit) {
      const newEntries = await editFilteredEntries(
        entries,
        filter,
        config.editor,
      );
      if (newEntries != null) {
        saveEntries(path, newEntries);
      }
    } else {
      printFilteredEntries(entries, filter, opts.summary);
    }
  } else {
    const rawEntry = opts._.length > 0
      ? opts._.join(" ")
      : await edit("", config.editor.split(/\s/));
      : await edit("", config.editor);
    if (rawEntry && rawEntry.trim() !== "") {
      const entry = makeEntry(rawEntry);
      entries.push(entry);

M config.ts => config.ts +10 -6
@@ 1,7 1,7 @@
import { path, toml } from "./deps.ts";

export interface Config {
  editor: string;
  editor: string[];
  default: string;
  journals: {
    [name: string]: {


@@ 47,12 47,12 @@ function defaultJournalPath(): string {
  );
}

function defaultEditor(): string {
function defaultEditor(): string[] {
  const editor = Deno.env.get("EDITOR");
  if (editor != null) return editor;
  if (editor != null) return editor.split(" ");
  // FIXME: should do something more reasonable than this
  if (Deno.build.os == "windows") return "notepad.exe";
  else return "nano";
  if (Deno.build.os == "windows") return ["notepad.exe"];
  else return ["nano"];
}

export function loadConfig(): Config {


@@ 68,7 68,11 @@ export function loadConfig(): Config {

  try {
    const userConfig = toml.parse(Deno.readTextFileSync(defaultConfigPath()));
    return { ...defaultConfig, ...userConfig };
    const mergedConfig = { ...defaultConfig, ...userConfig };
    if (typeof userConfig.editor === "string") {
      mergedConfig.editor = userConfig.editor.split(" ");
    }
    return mergedConfig;
  } catch {
    return defaultConfig;
  }

M deps.ts => deps.ts +5 -2
@@ 1,8 1,11 @@
import * as flags from "https://deno.land/std@0.118.0/flags/mod.ts";
import * as path from "https://deno.land/std@0.118.0/path/mod.ts";
import * as toml from "https://deno.land/std@0.118.0/encoding/toml.ts";
import { chunk } from "https://deno.land/std@0.118.0/collections/mod.ts";
import {
  chunk,
  partition,
} from "https://deno.land/std@0.118.0/collections/mod.ts";
import { DateTime } from "https://esm.sh/luxon@2.2.0";
import parseDate from "https://esm.sh/date.js@0.3.3";

export { chunk, DateTime, flags, parseDate, path, toml };
export { chunk, DateTime, flags, parseDate, partition, path, toml };

M mod.ts => mod.ts +14 -9
@@ 26,8 26,8 @@ function parseEntry(header: string, body: string): Entry {
}

export function parseEntries(bodies: string): Entry[] {
  const entries = bodies.split(/^(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\] .*)\n/m)
    .slice(1);
  const entries = bodies.split(/^(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\] .*)\n/m);
  if (entries[0].trim() === "") entries.splice(0, 1);
  return chunk(entries, 2).map((e) => parseEntry(e[0], e[1].trim()));
}



@@ 46,6 46,7 @@ export function saveEntries(path: string, entries: Entry[]) {
  try {
    entries.sort((a, b) => a.date.valueOf() - b.date.valueOf());
    file = Deno.openSync(path, { write: true });
    Deno.ftruncateSync(file.rid);
    let first = true;
    const nl = new Uint8Array([10]);
    for (const entry of entries) {


@@ 75,13 76,17 @@ export interface FilterParams {
  tags?: string[];
}

export function filterEntries(entries: Entry[], params: FilterParams): Entry[] {
  const filtered = entries.filter((e) =>
    (params.from ? params.from <= e.date : true) &&
    (params.to ? params.to >= e.date : true) &&
export function matchesFilter(entry: Entry, params: FilterParams) {
  return (params.from ? params.from <= entry.date : true) &&
    (params.to ? params.to >= entry.date : true) &&
    (params.tags
      ? params.tags.every((t) => e.title.includes(t) || e.body.includes(t))
      : true)
  );
      ? params.tags.every((t) =>
        entry.title.includes(t) || entry.body.includes(t)
      )
      : true);
}

export function filterEntries(entries: Entry[], params: FilterParams): Entry[] {
  const filtered = entries.filter((e) => matchesFilter(e, params));
  return params.limit ? filtered.slice(params.limit) : filtered;
}