Skip to content
Merged
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
23 changes: 23 additions & 0 deletions Formula/moji.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Moji < Formula
desc "Cozy terminal font finder with safe downloads"
homepage "https://github.com/Microck/moji"
url "https://registry.npmjs.org/@microck/moji/-/moji-0.7.0.tgz"
sha256 "2430859658fc26fe4da37bff6b27b0900dc28b9f6c92c16ebf26f083a2939afb"
license "MIT"

def install
platform = OS.mac? ? "darwin" : "linux"
architecture = if Hardware::CPU.arm64?
"arm64"
elsif Hardware::CPU.intel?
"x64"
else
odie "Unsupported CPU architecture: #{Hardware::CPU.arch}"
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install the binary from Homebrew's staged archive root

When this formula is installed, Homebrew unpacks the npm tarball and enters its sole top-level package directory before evaluating install (upstream Resource#stage implementation). The build path therefore contains binaries/..., not package/binaries/..., so this bin.install source is missing on every supported platform and the documented brew install fails. Remove the package/ prefix and make the release test exercise the staged layout rather than only matching the formula text.

Useful? React with 👍 / 👎.

bin.install "binaries/#{platform}-#{architecture}/moji"
end

test do
assert_equal version.to_s, shell_output("#{bin}/moji --version").strip
end
end
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ pnpm add -g @microck/moji
bun add -g @microck/moji
```

or install the same released native binary from this repository's Homebrew
formula:

```bash
brew tap microck/moji https://github.com/Microck/moji
brew install microck/moji/moji
```

The explicit tap URL is required because this source repository is not named
`homebrew-moji`. Upgrade later with `brew upgrade microck/moji/moji`.

search for a family:

```bash
Expand Down Expand Up @@ -86,10 +97,12 @@ or another script without relying on its filename.

## providers

the default GetFonts, Fontshare, and registry providers work without an
account. Font Squirrel is available through `--provider fontsquirrel`, but is
off by default because its site protection can block API clients. Both catalog
providers inspect official download archives for the formats you requested. GitHub's
the default GetFonts, DaFont, Fontshare, and registry providers work without an
account. DaFont search results are parsed from its public catalog and direct ZIP
downloads pass through Moji's normal archive safety limits. Font Squirrel is
available through `--provider fontsquirrel`, but is off by default because its
site protection can block API clients. All catalog providers inspect source
download archives for the formats you requested. GitHub's
repository, tree, and release search also uses its small unauthenticated
allowance. Moji automatically uses an existing authenticated `gh` session when
GitHub CLI is installed. `GITHUB_TOKEN` and `github_token` take precedence and
Expand Down Expand Up @@ -164,6 +177,8 @@ providers:
enabled: true
getfonts:
enabled: true
dafont:
enabled: true
fontsquirrel:
enabled: false
fontshare:
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/explanation/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ download, or TUI code.
## Providers own external contracts

Each provider implements the same interface but owns its request format,
response parsing, and source-specific errors. GitHub, GetFonts, and SearXNG
response parsing, and source-specific errors. GitHub, GetFonts, DaFont, and SearXNG
live in separate modules so changes to one external contract do not tangle the
others.

Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/how-to/search-and-filter.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Use a comma-separated provider list:

```sh
moji "Futura" --provider getfonts
moji "Peanuts" --provider dafont
moji "Futura" --provider github,getfonts
moji "Futura" --provider fontsquirrel,fontshare
moji "Futura" --provider registry,websearch
Expand Down
8 changes: 8 additions & 0 deletions docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ bun add -g @microck/moji
All three commands install the same npm-registry package. It includes binaries
for Linux, macOS, and Windows on x64 and arm64.

On macOS or Linux, the repository-owned Homebrew formula installs the native
binary from that same release archive:

```sh
brew tap microck/moji https://github.com/Microck/moji
brew install microck/moji/moji
```

## Two ways to work

Run a search in a terminal to open the live interface:
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ Delete cached provider responses and recreate the cache directory.
| `--format <list>` | `-f` | config default | Comma-separated `otf`, `ttf`, `woff`, `woff2`, `dfont`, `pfb`, or `pfm` |
| `--weight <name>` | `-w` | any | Keep only a normalized font weight |
| `--max <count>` | `-n` | all in TUI / 10 output / 1 get | Maximum results or downloads |
| `--provider <list>` | | all enabled | Comma-separated `github`, `getfonts`, `fontsquirrel`, `fontshare`, `registry`, `plugins`, or `websearch` |
| `--provider <list>` | | all enabled | Comma-separated `github`, `getfonts`, `dafont`, `fontsquirrel`, `fontshare`, `registry`, `plugins`, or `websearch` |
| `--json` | | off | Print machine-readable JSON |
| `--dry-run` | | off | Preview a `get` selection without downloading |
| `--download-dir <path>` | `-d` | `~/Downloads/moji` | Download destination |
Expand Down
5 changes: 5 additions & 0 deletions docs/content/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ rate_limits:
getfonts:
timeout_seconds: 15
retries: 1
dafont:
timeout_seconds: 15
retries: 1
fontsquirrel:
timeout_seconds: 15
retries: 1
Expand All @@ -57,6 +60,8 @@ providers:
enabled: true
getfonts:
enabled: true
dafont:
enabled: true
fontsquirrel:
enabled: false
instance: ""
Expand Down
5 changes: 5 additions & 0 deletions docs/content/docs/reference/errors-and-exit-codes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ Check the provider-specific explanations that follow the first sentence.
Verify connectivity and provider configuration, then retry. GetFonts remains
the fallback when GitHub authentication is unavailable.

In the TUI, press `H` when the results screen says a provider needs attention.
The Health screen keeps the provider's actual error, wraps long messages, and
names the setting or authentication command to fix. Use `j`/`k` to scroll when
the terminal is too short to show every failure.

### GitHub search is not configured

Set `GITHUB_TOKEN`, set `github_token` in the config, or pass
Expand Down
15 changes: 15 additions & 0 deletions docs/content/docs/reference/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ description: Availability, authentication, trust, and fallback behavior.

GetFonts is the zero-configuration default.

## DaFont

- Name: `dafont`
- Default: enabled
- Authentication: none
- Endpoint: DaFont's public search catalog and direct ZIP downloads
- Result trust: untrusted download bytes
- License: `unknown`; check the author and license information before use

Moji parses bounded catalog results, chooses at most three closest family
matches, and sends the direct ZIP downloads through the same archive path,
entry-count, expanded-size, format, and HTTPS checks as other providers. This
also covers names that do not exist in the structured registries, such as a
`Peanuts` search matching the Peanuts Gang family.

## Font Squirrel

- Name: `fontsquirrel`
Expand Down
11 changes: 9 additions & 2 deletions docs/content/docs/tutorial.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ icon: GraduationCap
---

This tutorial takes you from installation to a downloaded font. You need a
terminal and npm, pnpm, or Bun. You do not need Go.
terminal and npm, pnpm, Bun, or Homebrew. You do not need Go.

## 1. Install the CLI

Expand All @@ -29,13 +29,20 @@ pnpm add -g @microck/moji
bun add -g @microck/moji
```

### Homebrew

```sh
brew tap microck/moji https://github.com/Microck/moji
brew install microck/moji/moji
```

Confirm that the launcher can find its bundled binary:

```sh
moji --version
```

The command prints `0.2.1`.
The command prints the installed release version.

## 2. Search interactively

Expand Down
25 changes: 25 additions & 0 deletions docs/release-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,31 @@ After verification passes, the command publishes the exact verified tarball,
creates and pushes an annotated `v<version>` tag, then creates the matching
latest GitHub release with generated notes and the tarball attached.

## Update the Homebrew formula

`Formula/moji.rb` is the repository-owned tap formula. It deliberately installs
the native binary from the same immutable npm archive verified by the release
command instead of introducing another binary build or upload path.

After `npm run release:publish` has published the new version, download that
archive, calculate its SHA-256 checksum, and update the formula's `url` and
`sha256`. Then verify the formula on Homebrew before committing the formula
update:

```console
VERSION="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
curl -fsSLo moji.tgz https://registry.npmjs.org/@microck/moji/-/moji-$VERSION.tgz
shasum -a 256 moji.tgz
brew install --build-from-source ./Formula/moji.rb
brew test ./Formula/moji.rb
```

The formula lives in this repository, so users first run
`brew tap microck/moji https://github.com/Microck/moji` and then install it as
`microck/moji/moji`. The explicit URL is required because this repository is
not named `homebrew-moji`; no external tap repository or Homebrew/core change
is required.

The publish path is resumable. If npm publication succeeded but a later tag or
GitHub operation failed, rerun `npm run release:publish`. Moji compares npm's
published integrity with the newly verified archive, refuses any mismatch, and
Expand Down
2 changes: 1 addition & 1 deletion e2e/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestMojiBinaryEndToEnd(t *testing.T) {

configPath := filepath.Join(root, "config.yaml")
downloadDirectory := filepath.Join(root, "downloads")
configBody := fmt.Sprintf("download_dir: %s\nsearch_timeout_seconds: 2\ncache_ttl_seconds: 60\ndefault_formats: [otf]\nproviders:\n github:\n enabled: false\n getfonts:\n enabled: true\n instance: %s\n fontsquirrel:\n enabled: false\n fontshare:\n enabled: false\n registry:\n enabled: false\n websearch:\n enabled: false\n", downloadDirectory, server.URL+"/api/search")
configBody := fmt.Sprintf("download_dir: %s\nsearch_timeout_seconds: 2\ncache_ttl_seconds: 60\ndefault_formats: [otf]\nproviders:\n github:\n enabled: false\n getfonts:\n enabled: true\n instance: %s\n dafont:\n enabled: false\n fontsquirrel:\n enabled: false\n fontshare:\n enabled: false\n registry:\n enabled: false\n websearch:\n enabled: false\n", downloadDirectory, server.URL+"/api/search")
if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil {
t.Fatal(err)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestRunSearchJSONAndGetDownload(t *testing.T) {

root := t.TempDir()
configPath := filepath.Join(root, "config.yaml")
configBody := fmt.Sprintf("download_dir: %s\nsearch_timeout_seconds: 2\ncache_ttl_seconds: 60\ndefault_formats: [otf]\nproviders:\n github:\n enabled: false\n getfonts:\n enabled: true\n instance: %s\n fontsquirrel:\n enabled: false\n fontshare:\n enabled: false\n registry:\n enabled: false\n websearch:\n enabled: false\n", filepath.Join(root, "fonts"), server.URL)
configBody := fmt.Sprintf("download_dir: %s\nsearch_timeout_seconds: 2\ncache_ttl_seconds: 60\ndefault_formats: [otf]\nproviders:\n github:\n enabled: false\n getfonts:\n enabled: true\n instance: %s\n dafont:\n enabled: false\n fontsquirrel:\n enabled: false\n fontshare:\n enabled: false\n registry:\n enabled: false\n websearch:\n enabled: false\n", filepath.Join(root, "fonts"), server.URL)
if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -187,7 +187,7 @@ func TestRunGetKeepsCandidatesBeyondRequestedMaximumForFallback(t *testing.T) {
defer server.Close()
root := t.TempDir()
configPath := filepath.Join(root, "config.yaml")
configBody := fmt.Sprintf("download_dir: %s\nsearch_timeout_seconds: 2\ncache_ttl_seconds: 60\ndefault_formats: [otf, ttf]\nproviders:\n github:\n enabled: false\n getfonts:\n enabled: true\n instance: %s\n fontsquirrel:\n enabled: false\n fontshare:\n enabled: false\n registry:\n enabled: false\n websearch:\n enabled: false\n", filepath.Join(root, "fonts"), server.URL)
configBody := fmt.Sprintf("download_dir: %s\nsearch_timeout_seconds: 2\ncache_ttl_seconds: 60\ndefault_formats: [otf, ttf]\nproviders:\n github:\n enabled: false\n getfonts:\n enabled: true\n instance: %s\n dafont:\n enabled: false\n fontsquirrel:\n enabled: false\n fontshare:\n enabled: false\n registry:\n enabled: false\n websearch:\n enabled: false\n", filepath.Join(root, "fonts"), server.URL)
if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -339,7 +339,7 @@ func TestRunRejectsUnsupportedProviderAndBadUsage(t *testing.T) {

func TestArchiveCatalogProviderNamesAreAccepted(t *testing.T) {
t.Parallel()
if err := validateProviderNames("fontsquirrel,fontshare"); err != nil {
if err := validateProviderNames("dafont,fontsquirrel,fontshare"); err != nil {
t.Fatal(err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/app/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Flags:
-f, --format <list> otf, ttf, woff, woff2, dfont, pfb, pfm
-w, --weight <name> Filter by font weight
-n, --max <count> Maximum results or downloads
--provider <list> github,getfonts,fontsquirrel,fontshare,registry,plugins,websearch
--provider <list> github,getfonts,dafont,fontsquirrel,fontshare,registry,plugins,websearch
--json Machine-readable output
--dry-run Preview get downloads
-d, --download-dir <path> Download destination
Expand Down
5 changes: 3 additions & 2 deletions internal/app/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func (application App) searchEvents(ctx context.Context, current config.Config,
invalidURLs, _ := store.InvalidURLs()
available := map[string]provider.Provider{
"getfonts": provider.GetFonts{Client: application.Client, Endpoint: current.Providers["getfonts"].Instance},
"dafont": provider.DaFont{Client: application.Client, Endpoint: current.Providers["dafont"].Instance},
"fontsquirrel": provider.FontSquirrel{Client: application.Client, Endpoint: current.Providers["fontsquirrel"].Instance},
"fontshare": provider.Fontshare{Client: application.Client, Endpoint: current.Providers["fontshare"].Instance},
"registry": provider.RegistrySearch{
Expand Down Expand Up @@ -244,8 +245,8 @@ func validateProviderNames(value string) error {
}
for _, raw := range strings.Split(value, ",") {
name := strings.TrimSpace(strings.ToLower(raw))
if name != "github" && name != "getfonts" && name != "fontsquirrel" && name != "fontshare" && name != "registry" && name != "plugins" && name != "websearch" {
return fmt.Errorf("unknown provider %q (choose github, getfonts, fontsquirrel, fontshare, registry, plugins, or websearch)", name)
if name != "github" && name != "getfonts" && name != "dafont" && name != "fontsquirrel" && name != "fontshare" && name != "registry" && name != "plugins" && name != "websearch" {
return fmt.Errorf("unknown provider %q (choose github, getfonts, dafont, fontsquirrel, fontshare, registry, plugins, or websearch)", name)
}
}
return nil
Expand Down
3 changes: 2 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ func Default() Config {
RateLimits: map[string]RateLimitConfig{
"github": {TimeoutSeconds: 15, Retries: 2},
"getfonts": {TimeoutSeconds: 15, Retries: 1},
"dafont": {TimeoutSeconds: 15, Retries: 1},
"fontsquirrel": {TimeoutSeconds: 15, Retries: 1},
"fontshare": {TimeoutSeconds: 15, Retries: 1},
"registry": {TimeoutSeconds: 15, Retries: 1},
"plugins": {TimeoutSeconds: 20, Retries: 0},
"websearch": {TimeoutSeconds: 20, Retries: 0},
},
Providers: map[string]ProviderConfig{
"github": {Enabled: true}, "getfonts": {Enabled: true}, "fontsquirrel": {Enabled: false},
"github": {Enabled: true}, "getfonts": {Enabled: true}, "dafont": {Enabled: true}, "fontsquirrel": {Enabled: false},
"fontshare": {Enabled: true}, "registry": {Enabled: true}, "plugins": {Enabled: true},
"websearch": {Enabled: true},
},
Expand Down
5 changes: 4 additions & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ func TestDefaultConfiguresArchiveCatalogProviders(t *testing.T) {
if !current.Providers["fontshare"].Enabled {
t.Fatal("Fontshare is not enabled")
}
for _, name := range []string{"fontsquirrel", "fontshare"} {
if !current.Providers["dafont"].Enabled {
t.Fatal("DaFont is not enabled")
}
for _, name := range []string{"dafont", "fontsquirrel", "fontshare"} {
if current.RateLimits[name].TimeoutSeconds <= 0 {
t.Fatalf("provider %q has no timeout policy", name)
}
Expand Down
Loading