Skip to content

Commit

Permalink
feature: JSON runtime config (#41)
Browse files Browse the repository at this point in the history
* feature: add json config unconnected

* refactor: config

* remove: deprecated config

* remove: config module

* working: fm & gen

* working: sb

* feature: working json config

* format: config

* docs: config

* docs: config note

* docs: config tabs

* docs: jsonc

* docs: remove bloat doc

* fix: keybind parser

* docs: small readme changes
  • Loading branch information
freref authored Feb 18, 2025
1 parent a416f62 commit d60874e
Show file tree
Hide file tree
Showing 8 changed files with 442 additions and 75 deletions.
48 changes: 40 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,46 @@
</p>

![demo](https://github.com/user-attachments/assets/b1edc9d2-3b1f-437d-9b48-c196d22fcbbd)

## Usage

```sh
fancy-cat <path-to-pdf> <optional-page-number>
```
Keymappings and other options can be found and changed in ``src/config.zig``.

### Configuration

fancy-cat can be configured through a JSON config file located at `~/.config/fancy-cat/config.json`. The file is automatically created on the first run with default settings.

The default `config.json` can be found [here](./src/config/config.json) and documentation on the config options can be found [here](./docs/config.md)

## Installation

### Arch Linux
[fancy-cat](https://aur.archlinux.org/packages/fancy-cat) is available as a package in the AUR. You can install it using an AUR helper (e.g., paru):

fancy-cat is available as a package in the AUR ([link](https://aur.archlinux.org/packages/fancy-cat)). You can install it using an AUR helper (e.g., paru):

```sh
paru -S fancy-cat
```

## Build Instructions

### Requirements
- Zig version ``0.13.0``

- Zig version `0.13.0`
- Terminal emulator with the Kitty image protocol (e.g. Kitty, Ghostty, WezTerm, etc.)
- [MuPDF](https://mupdf.readthedocs.io/en/latest/quick-start-guide.html)

#### MacOS
``` sh

```sh
brew install mupdf
```

#### Linux
``` sh

```sh
apt install \
libmupdf-dev \
libharfbuzz-dev \
Expand All @@ -43,44 +61,58 @@ apt install \
libmujs-dev \
zlib1g-dev
```

> [!IMPORTANT]
> On some Linux distributions (e.g., Fedora, Arch), replace `mupdf-third` with `mupdf` in ``build.zig`` to compile successfully.
> On some Linux distributions (e.g., Fedora, Arch), replace `mupdf-third` with `mupdf` in `build.zig` to compile successfully.
### Build

1. Fetch dependencies:

```sh
zig build --fetch
```

2. Build the project:

```sh
zig build --release=fast
```

> [!NOTE]
> There is a [known issue](https://github.com/freref/fancy-cat/issues/18) with some processors; if the build fails on step 7/10 with the error `LLVM ERROR: Do not know how to expand the result of this operator!` then try the command below instead:
>
> ```
> zig build --release=fast -Dcpu="skylake"
> ```
3. Install:
3. Install:
```
# Add to your PATH
# Linux
mv zig-out/bin/fancy-cat ~/.local/bin/

# macOS
# macOS
mv zig-out/bin/fancy-cat /usr/local/bin/
```
### Run
```
zig build run -- <path-to-pdf> <optional-page-number>
```
## Features
- ✅ Filewatch (hot-reload)
- ✅ Custom keymapping
- ✅ Dark-mode
- ✅ Zoom
- ✅ Status bar
- 🚧 Cache
- 🚧 Search
## Contributing
Contributions are welcome.
1 change: 0 additions & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ pub fn build(b: *std.Build) void {
exe.root_module.addImport("fastb64z", deps.fastb64z.module("fastb64z"));
exe.root_module.addImport("vaxis", deps.vaxis.module("vaxis"));
exe.root_module.addImport("fzwatch", deps.fzwatch.module("fzwatch"));
exe.root_module.addImport("config", b.addModule("config", .{ .root_source_file = b.path("src/config.zig") }));

addMupdfDeps(exe, target.result);
b.installArtifact(exe);
Expand Down
61 changes: 61 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Configuration

Configuration file location: `~/.config/fancy-cat/config.json`

## KeyMap

Each binding requires:

- `key`: `u8` (single character) - The main key to trigger the action
- `modifiers`: Array of strings - Optional modifier keys. Available modifiers:
- `shift`
- `alt`
- `ctrl`
- `super`
- `hyper`
- `meta`
- `caps_lock`
- `num_lock`

```jsonc
{
"next": { "key": "n" }, // Next page
"prev": { "key": "p" }, // Previous page
"scroll_up": { "key": "k" }, // Move viewport up
"scroll_down": { "key": "j" }, // Move viewport down
"scroll_left": { "key": "h" }, // Move viewport left
"scroll_right": { "key": "l" }, // Move viewport right
"zoom_in": { "key": "i" }, // Increase zoom level
"zoom_out": { "key": "o" }, // Decrease zoom level
"colorize": { "key": "z" }, // Toggle color inversion
"quit": { "key": "c", "modifiers": ["ctrl"] } // Exit program
}
```

## FileMonitor

Controls automatic reloading when PDF file changes. Useful for live previewing:

- `enabled`: `bool` - Toggle file change detection
- `latency`: `f16` - Time between file checks in seconds

## General

> [!NOTE]
> fancy-cat uses color inversion for better terminal viewing. By default, it runs in dark mode where white pixels are displayed as black (0x000000) and black pixels as white (0xffffff). You can customize these colors to match your terminal theme - set white to your terminal's background color and black to your desired text.
- `colorize`: `bool` - Toggle color inversion for dark/light mode
- `white`: `i32` - Hex color code for white pixels in colorized mode
- `black`: `i32` - Hex color code for black pixels in colorized mode
- `size`: `f32` - PDF size relative to screen (0.0-1.0)
- `zoom_step`: `f32` - How much to zoom in/out per keystroke
- `scroll_step`: `f32` - Pixels to move per scroll command

## StatusBar

Configure the information bar at screen bottom:

- `enabled`: `bool` - Show/hide the status bar
- `style`: Status bar appearance
- `bg`: Array of 3 `u8` values [r, g, b] - Text color (0-255)
- `fg`: Array of 3 `u8` values [r, g, b] - Status bar color (0-255)
24 changes: 12 additions & 12 deletions src/PdfHandler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const Self = @This();
const std = @import("std");
const fastb64z = @import("fastb64z");
const vaxis = @import("vaxis");
const config = @import("config");
const Config = @import("config/Config.zig");
const c = @cImport({
@cInclude("mupdf/fitz.h");
@cInclude("mupdf/pdf.h");
Expand Down Expand Up @@ -40,9 +40,9 @@ y_offset: f32,
x_offset: f32,
width: f32,
height: f32,
colorize: bool,
config: Config,

pub fn init(allocator: std.mem.Allocator, path: []const u8, initial_page: ?u16) !Self {
pub fn init(allocator: std.mem.Allocator, path: []const u8, initial_page: ?u16, config: Config) !Self {
const ctx = c.fz_new_context(null, null, c.FZ_STORE_UNLIMITED) orelse {
std.debug.print("Failed to create mupdf context\n", .{});
return PdfError.FailedToCreateContext;
Expand Down Expand Up @@ -82,7 +82,7 @@ pub fn init(allocator: std.mem.Allocator, path: []const u8, initial_page: ?u16)
.x_offset = 0,
.width = 0,
.height = 0,
.colorize = config.General.colorize,
.config = config,
};
}

Expand Down Expand Up @@ -129,7 +129,7 @@ pub fn renderPage(

self.width = @as(f32, bound.x1);
self.height = @as(f32, bound.y1);
if (self.size == 0) self.size = scale * config.General.size;
if (self.size == 0) self.size = scale * self.config.general.size;
if (self.zoom == 0) self.zoom = self.size;

const bbox = c.fz_make_irect(
Expand Down Expand Up @@ -160,8 +160,8 @@ pub fn renderPage(
c.fz_run_page(self.ctx, page, dev, c.fz_identity, null);
c.fz_close_device(self.ctx, dev);

if (self.colorize) {
c.fz_tint_pixmap(self.ctx, pix, config.General.black, config.General.white);
if (self.config.general.colorize) {
c.fz_tint_pixmap(self.ctx, pix, self.config.general.black, self.config.general.white);
} else {
c.fz_tint_pixmap(self.ctx, pix, 0x000000, 0xffffff);
}
Expand Down Expand Up @@ -206,24 +206,24 @@ pub fn changePage(self: *Self, delta: i32) bool {
}

pub fn adjustZoom(self: *Self, increase: bool) void {
const factor = self.size * config.General.zoom_step / 2;
const factor = self.size * self.config.general.zoom_step / 2;
if (increase) {
self.zoom *= (config.General.zoom_step + 1);
self.zoom *= (self.config.general.zoom_step + 1);
self.x_offset -= factor * self.width / self.zoom;
self.y_offset -= factor * self.height / self.zoom;
} else {
self.x_offset += factor * self.width / self.zoom;
self.y_offset += factor * self.height / self.zoom;
self.zoom /= (config.General.zoom_step + 1);
self.zoom /= (self.config.general.zoom_step + 1);
}
}

pub fn toggleColor(self: *Self) void {
self.colorize = !self.colorize;
self.config.general.colorize = !self.config.general.colorize;
}

pub fn scroll(self: *Self, direction: ScrollDirection) void {
const step = config.General.scroll_step / self.zoom;
const step = self.config.general.scroll_step / self.zoom;
switch (direction) {
.Up => {
const translation = self.y_offset + step;
Expand Down
28 changes: 16 additions & 12 deletions src/View.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const Self = @This();
const std = @import("std");
const vaxis = @import("vaxis");
const fzwatch = @import("fzwatch");
const config = @import("config");
const Config = @import("config/Config.zig");
const PdfHandler = @import("PdfHandler.zig");

pub const panic = vaxis.panic_handler;
Expand All @@ -25,6 +25,7 @@ current_page: ?vaxis.Image,
watcher: ?fzwatch.Watcher,
thread: ?std.Thread,
reload: bool,
config: Config,

pub fn init(allocator: std.mem.Allocator, args: [][]const u8) !Self {
const path = args[1];
Expand All @@ -33,11 +34,13 @@ pub fn init(allocator: std.mem.Allocator, args: [][]const u8) !Self {
else
null;

var pdf_handler = try PdfHandler.init(allocator, path, initial_page);
const config = try Config.init(allocator);

var pdf_handler = try PdfHandler.init(allocator, path, initial_page, config);
errdefer pdf_handler.deinit();

var watcher: ?fzwatch.Watcher = null;
if (config.FileMonitor.enabled) {
if (config.file_monitor.enabled) {
watcher = try fzwatch.Watcher.init(allocator);
if (watcher) |*w| try w.addFile(path);
}
Expand All @@ -54,6 +57,7 @@ pub fn init(allocator: std.mem.Allocator, args: [][]const u8) !Self {
.mouse = null,
.thread = null,
.reload = false,
.config = config,
};
}

Expand All @@ -78,8 +82,8 @@ fn callback(context: ?*anyopaque, event: fzwatch.Event) void {
}
}

fn watcherThread(watcher: *fzwatch.Watcher) !void {
try watcher.start(.{ .latency = config.FileMonitor.latency });
fn watcherThread(self: *Self, watcher: *fzwatch.Watcher) !void {
try watcher.start(.{ .latency = self.config.file_monitor.latency });
}

pub fn run(self: *Self) !void {
Expand All @@ -94,10 +98,10 @@ pub fn run(self: *Self) !void {
try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s);
try self.vx.setMouseMode(self.tty.anyWriter(), true);

if (config.FileMonitor.enabled) {
if (self.config.file_monitor.enabled) {
if (self.watcher) |*w| {
w.setCallback(callback, &loop);
self.thread = try std.Thread.spawn(.{}, watcherThread, .{w});
self.thread = try std.Thread.spawn(.{}, watcherThread, .{ self, w });
}
}

Expand All @@ -124,7 +128,7 @@ fn resetCurrentPage(self: *Self) void {
}

fn handleKeyStroke(self: *Self, key: vaxis.Key) !void {
const km = config.KeyMap;
const km = self.config.key_map;
// non reload keys
if (key.matches(km.quit.key, km.quit.modifiers)) {
self.should_quit = true;
Expand Down Expand Up @@ -214,10 +218,10 @@ pub fn drawStatusBar(self: *Self, win: vaxis.Window) !void {
.height = 1,
});

status_bar.fill(vaxis.Cell{ .style = config.StatusBar.style });
status_bar.fill(vaxis.Cell{ .style = self.config.status_bar.style });

_ = status_bar.print(
&.{.{ .text = self.pdf_handler.path, .style = config.StatusBar.style }},
&.{.{ .text = self.pdf_handler.path, .style = self.config.status_bar.style }},
.{ .col_offset = 1 },
);

Expand All @@ -232,7 +236,7 @@ pub fn drawStatusBar(self: *Self, win: vaxis.Window) !void {
);

_ = status_bar.print(
&.{.{ .text = self.page_info_text, .style = config.StatusBar.style }},
&.{.{ .text = self.page_info_text, .style = self.config.status_bar.style }},
.{ .col_offset = @intCast(win.width - self.page_info_text.len - 1) },
);
}
Expand All @@ -242,7 +246,7 @@ pub fn draw(self: *Self) !void {
win.clear();

try self.drawCurrentPage(win);
if (config.StatusBar.enabled) {
if (self.config.status_bar.enabled) {
try self.drawStatusBar(win);
}
}
Loading

0 comments on commit d60874e

Please sign in to comment.