Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bin/
obj/
*.user
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>QuickSheetCase</RootNamespace>
<AssemblyName>CaseExtension</AssemblyName>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 cemheren

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System.Net.Http.Headers;
using System.Text.Json;

// QuickSheet case extension — US case law lookup via CourtListener's free API.
//
// Cell usage:
// case: Roe v Wade
// case: 410 U.S. 113
// case: "unreasonable search" fourth amendment
//
// Output: up to gridRows results. Columns: case name, citation, court, year, snippet.
// API: https://www.courtlistener.com/api/rest/v4/search/?type=o&q=<query>
// No auth required for basic search (rate-limited to ~100 req/hr unauthenticated).

var http = new HttpClient();
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("QuickSheetCaseExt", "1.0"));
http.Timeout = TimeSpan.FromSeconds(10);

Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(JsonSerializer.Serialize(new
{
type = "register",
prefix = "case",
name = "Case Law",
version = "1.0.0",
}));
Console.Out.Flush();

string? line;
while ((line = Console.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line)) continue;
try
{
using var doc = JsonDocument.Parse(line);
if (!doc.RootElement.TryGetProperty("type", out var t)) continue;
if (t.GetString() != "activate") continue;
HandleActivate(doc.RootElement);
}
catch { /* ignore malformed input */ }
}

void HandleActivate(JsonElement root)
{
string id = root.TryGetProperty("id", out var idEl) ? idEl.GetString() ?? "" : "";
int gridRows = root.TryGetProperty("gridRows", out var gr) ? gr.GetInt32() : 5;

string query = "";
if (root.TryGetProperty("params", out var p) && p.ValueKind == JsonValueKind.Array)
{
var en = p.EnumerateArray();
if (en.MoveNext()) query = en.Current.GetString() ?? "";
}
query = query.Trim();

if (string.IsNullOrEmpty(query))
{
Emit(id, new[] { new Cell(0, 0, "case: <citation or keywords>") });
return;
}

var results = Search(query, gridRows);
if (results.Count == 0)
{
Emit(id, new[] { new Cell(0, 0, "No results"), new Cell(0, 1, query) });
return;
}

var cells = new List<Cell>();
for (int r = 0; r < results.Count && r < gridRows; r++)
{
var c = results[r];
cells.Add(new Cell(r, 0, c.caseName));
cells.Add(new Cell(r, 1, c.citation));
cells.Add(new Cell(r, 2, c.court));
cells.Add(new Cell(r, 3, c.year));
cells.Add(new Cell(r, 4, c.snippet));
}
Emit(id, cells);
}

List<CaseResult> Search(string query, int limit)
{
var results = new List<CaseResult>();
try
{
string encoded = Uri.EscapeDataString(query);
string url = $"https://www.courtlistener.com/api/rest/v4/search/?type=o&q={encoded}&page_size={Math.Min(limit, 20)}";
var response = http.GetAsync(url).GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode) return results;

string json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
using var doc = JsonDocument.Parse(json);

if (!doc.RootElement.TryGetProperty("results", out var arr)) return results;

foreach (var item in arr.EnumerateArray())
{
string caseName = item.TryGetProperty("caseName", out var cn)
? cn.GetString() ?? "" : "";
string citation = item.TryGetProperty("citation", out var cit)
? FirstCitation(cit) : "";
string court = item.TryGetProperty("court_citation_string", out var ct)
? ct.GetString() ?? "" : "";
string year = item.TryGetProperty("dateFiled", out var df)
? ExtractYear(df.GetString()) : "";
string snippet = item.TryGetProperty("snippet", out var sn)
? StripHtml(sn.GetString() ?? "") : "";

if (caseName.Length == 0 && citation.Length == 0) continue;
results.Add(new CaseResult(
Truncate(caseName, 40),
Truncate(citation, 25),
Truncate(court, 15),
year,
Truncate(snippet, 60)
));
}
}
catch { /* network error — return empty */ }
return results;
}

static string FirstCitation(JsonElement el)
{
if (el.ValueKind == JsonValueKind.Array)
{
foreach (var c in el.EnumerateArray())
{
string? s = c.GetString();
if (!string.IsNullOrEmpty(s)) return s;
}
}
if (el.ValueKind == JsonValueKind.String) return el.GetString() ?? "";
return "";
}

static string ExtractYear(string? date)
{
if (date == null || date.Length < 4) return "";
return date[..4];
}

static string StripHtml(string s)
{
// Remove <mark>, </mark>, <em>, </em>, and other common HTML tags from snippets
var clean = System.Text.RegularExpressions.Regex.Replace(s, "<[^>]+>", "");
clean = clean.Replace("&amp;", "&").Replace("&lt;", "<").Replace("&gt;", ">");
clean = clean.Replace("&quot;", "\"").Replace("&#39;", "'");
clean = clean.Replace("\n", " ").Replace("\r", "");
// Collapse whitespace
clean = System.Text.RegularExpressions.Regex.Replace(clean, @"\s+", " ").Trim();
return clean;
}

static string Truncate(string s, int max) =>
s.Length <= max ? s : s[..(max - 1)] + "…";

void Emit(string id, IEnumerable<Cell> cells)
{
Console.WriteLine(JsonSerializer.Serialize(new { type = "write", id, cells }));
Console.Out.Flush();
}

record Cell(int r, int c, string v);
record CaseResult(string caseName, string citation, string court, string year, string snippet);
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# quicksheet-case-ext

A QuickSheet extension that looks up US case law citations and keywords via
[CourtListener](https://www.courtlistener.com/)'s free public API.

```
ext: github:cemheren/quicksheet-case-ext
case: Roe v Wade
case: 410 U.S. 113
case: "unreasonable search" fourth amendment
```

## What you get

Up to 5 results per query (configurable via grid rows). Each row shows:

| Case Name | Citation | Court | Year | Snippet |
|------------------------------|----------------|-------|------|--------------------------------------------------|
| Roe v. Wade | 410 U.S. 113 | SCOTUS| 1973 | right of privacy...broad enough to encompass... |
| Planned Parenthood v. Casey | 505 U.S. 833 | SCOTUS| 1992 | undue burden standard...liberty of the woman... |

## Features

- **Citation search**: type a standard reporter citation (`410 U.S. 113`, `347 U.S. 483`) for a direct hit.
- **Keyword search**: natural language queries across millions of opinions.
- **No API key required**: CourtListener's search endpoint is free and open (rate-limited to ~100 req/hr).
- **HTML-stripped snippets**: result excerpts are cleaned of markup for readable cell display.

## Use cases

- Law students reviewing cases for class prep — keep citations on your desktop.
- Paralegals quickly verifying citation accuracy without leaving their workspace.
- Anyone who wants an ambient "legal research sticky note" on their wallpaper.

## Build

```bash
dotnet build CaseExtension.csproj
```

.NET 9, MIT, zero NuGet dependencies.

## Protocol

Standard QuickSheet JSON-lines protocol over stdin/stdout:

1. On startup, emit `{"type":"register","prefix":"case","name":"Case Law","version":"1.0.0"}`.
2. On each `{"type":"activate","id":"...","params":["<query>"],"gridRows":N}`, search CourtListener and reply with `{"type":"write","id":"...","cells":[...]}`.

See [docs/extension-protocol.md](https://github.com/cemheren/QuickSheet/blob/main/docs/extension-protocol.md) in the main repo.

## API

Uses CourtListener REST API v4: `https://www.courtlistener.com/api/rest/v4/search/?type=o&q=<query>`

CourtListener is operated by [Free Law Project](https://free.law/), a 501(c)(3) non-profit. The API is free for reasonable use. If you plan heavy usage, consider [registering for a token](https://www.courtlistener.com/sign-in/).

## License

MIT.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "case",
"version": "1.0.0",
"prefix": "case",
"description": "US case law lookup — search citations or keywords via CourtListener's free API",
"entry": "dotnet run --project CaseExtension.csproj",
"minProtocolVersion": 1
}
24 changes: 24 additions & 0 deletions .claude/skills/grow-quicksheet/log.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
## 2026-06-16 (docs: accountant landing page)

- Stars: 0 (Δ 0).
- Action: Created `docs/for-accountants.md` landing page + `examples/accountant-dashboard.csv` starter sheet. Targets freelance accountants/bookkeepers with 1099, mileage, depreciation, margin, salestax, payroll extensions. Added README cross-link.
- Bucket: A (audience landing page).
- Outcome: PR #431.
- Follow-up: None queued — all backlog landing pages now have PRs (homelab merged, traders/sre/students on main, dms/writers/artists/accountants as open PRs).

## 2026-06-16 (feat: stdin pipe support)

- Stars: 0 (Δ 0).
- Action: Implemented `-` as file argument to read CSV from stdin in headless export modes. Reads stdin to temp file, processes, cleans up. Added error for non-headless use. Updated --help.
- Bucket: E (issue fix — #320).
- Outcome: PR #396 updated (commit 48f4104). Closes #320.
- Follow-up: #318 (NO_COLOR) or #158 (usage pattern/tabs) are next open issues.

## 2026-06-16 (feat: shell completions for bash/zsh/fish)

- Stars: 0 (Δ 0).
- Action: Rewrote shell completion scripts (bash, zsh, fish) on existing PR #393 branch. Covers all CLI flags + .csv file completion. Added docs/shell-completions.md with install instructions.
- Bucket: E (issue fix — #372).
- Outcome: PR #393 updated (force-pushed commit d92d1d5). Closes #372.
- Follow-up: #319 (--delimiter flag) or #320 (stdin pipe) are next smallest qualifying issues with existing PRs. No new unaddressed issues remain.

## 2026-05-19 (no-op #31 — #139 still open, hold)

- Stars: 0 (Δ 0). PR #139 still open, no other action items. Hold.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ tls: github.com, 1, 4
| `caniuse:` | Browser compat lookup via caniuse-db — `caniuse: css-grid` · `caniuse: flexbox` · `caniuse: webassembly`. Shows support across Chrome/Firefox/Safari/Edge + usage %. No API key. | `ext: github:Deskworks/quicksheet-caniuse` |
| `gem:` | Ruby gem lookup via rubygems.org — version, total downloads, authors, license, homepage, description. `gem: rails` (detail) · `gem: search json parser` (top 5). No API key. | `ext: github:Deskworks/quicksheet-rubygems` |
| `hackage:` | Haskell package lookup from Hackage — `hackage: aeson` (version, synopsis, author, category, license, homepage) · `hackage: search json` (top results). No API key. | `ext: github:Deskworks/quicksheet-hackage` |
| `case:` | US case law lookup — search by citation or keywords via CourtListener. `case: Roe v Wade` · `case: 410 U.S. 113` · `case: "fourth amendment" search`. No API key. | `ext: github:cemheren/quicksheet-case-ext` |

See [docs/extensions.md](docs/extensions.md) for the full directory with descriptions and protocol docs.

Expand Down
1 change: 1 addition & 0 deletions docs/tour.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Currently public:
- [`quicksheet-qtr`](https://github.com/Deskworks/quicksheet-qtr) — IRS quarterly tax deadline countdown.
- [`quicksheet-fx`](https://github.com/Deskworks/quicksheet-fx) — live currency conversion (200+ currencies, ECB rates).
- [`quicksheet-rate`](https://github.com/Deskworks/quicksheet-rate) — freelance hourly rate calculator (taxes, benefits, billable time).
- [`quicksheet-case-ext`](https://github.com/cemheren/quicksheet-case-ext) — US case law lookup via CourtListener (citation or keyword search).

[Write your own](../README.md#build-your-own) in whatever language you want — the protocol is two message types.

Expand Down