.
ijq/Makefile 0000664 0000000 0000000 00000001520 15265734051 0013307 0 ustar 00root root 0000000 0000000 prefix = /usr/local
# Support upper case PREFIX
ifneq ($(PREFIX),)
prefix := $(PREFIX)
endif
bindir = $(prefix)/bin
mandir = $(prefix)/share/man
SRCS = $(filter-out %_test.go,$(wildcard *.go))
VERSION = 1.4.2
.PHONY: all
all: ijq docs
.PHONY: docs
docs: ijq.1
ijq: $(SRCS)
go build -ldflags="-s -w -X main.Version=$(VERSION)" -o $@
%.1: %.1.scd
scdoc < $< > $@
.PHONY: test
test:
go test -v -coverprofile=./cover.out ./...
.PHONY: viewcover
viewcover:
go tool cover -html=./cover.out
.PHONY: install
install: ijq ijq.1
install -d $(DESTDIR)$(bindir) $(DESTDIR)$(mandir)/man1
install -m 0755 ijq $(DESTDIR)$(bindir)
install -m 0644 ijq.1 $(DESTDIR)$(mandir)/man1
.PHONY: uninstall
uninstall:
rm $(DESTDIR)$(bindir)/ijq $(DESTDIR)$(mandir)/man1/ijq.1
.PHONY: clean
clean:
rm -f ijq ijq.1
demo/ijq.gif: demo/ijq.tape
vhs < $<
ijq/README.md 0000664 0000000 0000000 00000007006 15265734051 0013133 0 ustar 00root root 0000000 0000000 ijq
===
[](https://ci.codeberg.org/repos/14932)
Interactive `jq` tool. Like [jqplay] for the commandline.
[jqplay]: https://jqplay.org
Demo
----

Installation
------------
### Install from package manager
[](https://repology.org/project/ijq/versions)
### Build from source
Install [go]. To install `ijq` under `/usr/local/bin/` simply run
make install
from the root of the project. To install to another location, set the `PREFIX`
variable, e.g.
make PREFIX=~/.local install
To generate the man page you will also need to have [scdoc] installed.
[go]: https://golang.org/dl/
[scdoc]: https://sr.ht/~sircmpwn/scdoc
Usage
-----
ijq uses [jq] under the hood, so make sure you have that installed first.
Read from a file:
ijq file.json
Read from stdin:
curl -s https://api.github.com/users/gpanders | ijq
Press `Return` to close `ijq` and print the current filtered output to stdout.
This will also print the current filter to stderr. This allows you to save the
filter for re-use with `jq` in the future:
ijq file.json 2>filter.jq
# Same output as above
jq -f filter.jq file.json
Pressing `Return` also saves the filter to a history file
(`$XDG_DATA_HOME/ijq/history` by default). You can browse the history by
deleting everything in the filter field. Change the history file used with the
`history-file` setting in `$XDG_CONFIG_HOME/ijq/config` (or
`~/.config/ijq/config` when `$XDG_CONFIG_HOME` is unset). Set
`history-file` to an empty string to disable history entirely.
If `$XDG_DATA_HOME` is undefined, `~/.local/share/ijq/history` is used.
Press `Ctrl+s` to save the current filter to history without exiting. A
confirmation popup is shown and can be dismissed with any key.
Use `Shift` plus the arrow keys to move between the different windows. When
either of the input or output views have focus, you can use the arrow keys to
scroll up and down. Vi keys also work, i.e. you can use `j`/`k` to scroll up or
down, `g` to move to the top of the view, `G` to jump to the bottom of the
view, and `Ctrl-F`/`Ctrl-B` to scroll up or down a page at a time.
Press `Ctrl-/` to open an overlay menu (`Ctrl-?` and `Ctrl-_` are also
accepted) with some useful actions and configuration options.
Use `Esc` to go back to the root overlay menu (or close it from the root), and
use `q` to close the overlay from any subview.
Use `Ctrl-C` to close the overlay when it is open, or to exit `ijq`
immediately (discarding all filters and state) when the overlay is closed.
You can configure the colors by setting the `JQ_COLORS` environment variable.
See the [jq documentation][colors] for more details.
[jq]: https://jqlang.github.io/jq/
[colors]: https://jqlang.github.io/jq/manual/#colors
Contributing
------------
Bugs can be reported on the [issue tracker][issues]. PRs are accepted on
[GitHub][github] or [Codeberg][codeberg].
[issues]: https://codeberg.org/gpanders/ijq/issues
[github]: https://github.com/gpanders/ijq
[codeberg]: https://codeberg.org/gpanders/ijq
Note for Packagers
------------------
Subscribe to release announcements on [Codeberg][codeberg] to be notified of
new releases, or subscribe to the [RSS
feed](https://codeberg.org/gpanders/ijq/releases.rss).
Similar Work
------------
- [jqplay]
- [vim-jqplay]
[vim-jqplay]: https://github.com/bfrg/vim-jqplay
License
-------
[GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html)
ijq/README.srht.html 0000664 0000000 0000000 00000000121 15265734051 0014445 0 ustar 00root root 0000000 0000000 ijq has moved to Codeberg
ijq/clipboard.go 0000664 0000000 0000000 00000001747 15265734051 0014150 0 ustar 00root root 0000000 0000000 // Copyright (C) 2026 Gregory Anders
//
// SPDX-License-Identifier: GPL-3.0-or-later
package main
import (
"encoding/base64"
"errors"
"fmt"
"io"
"sync"
)
const maxClipboardBytes = 1 << 20
var errNoTTY = errors.New("terminal does not expose a tty")
type Clipboard struct {
mutex sync.Mutex
tty io.Writer
}
var _ io.Writer = &Clipboard{}
func NewClipboard(tty io.Writer) *Clipboard {
return &Clipboard{tty: tty}
}
func (c *Clipboard) Write(buf []byte) (int, error) {
if len(buf) == 0 {
return 0, nil
}
if len(buf) > maxClipboardBytes {
return 0, fmt.Errorf("clipboard content is %d bytes; limit is %d", len(buf), maxClipboardBytes)
}
c.mutex.Lock()
defer c.mutex.Unlock()
if c.tty == nil {
return 0, errNoTTY
}
out := fmt.Appendf(nil, "\x1b]52;c;%s\x1b\\", base64.StdEncoding.EncodeToString(buf))
if _, err := c.tty.Write(out); err != nil {
return 0, fmt.Errorf("failed to write OSC 52 escape sequence: %w", err)
}
return len(buf), nil
}
ijq/clipboard_test.go 0000664 0000000 0000000 00000001724 15265734051 0015202 0 ustar 00root root 0000000 0000000 package main
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClipboardWrite(t *testing.T) {
t.Parallel()
var buffer bytes.Buffer
clipboard := NewClipboard(&buffer)
n, err := clipboard.Write([]byte("hello"))
require.NoError(t, err)
require.Equal(t, len("hello"), n)
assert.Equal(t, "\x1b]52;c;aGVsbG8=\x1b\\", buffer.String())
}
func TestClipboardWriteWithoutTTY(t *testing.T) {
t.Parallel()
n, err := NewClipboard(nil).Write([]byte("hello"))
assert.Zero(t, n)
assert.ErrorIs(t, err, errNoTTY)
}
func TestClipboardWriteEmpty(t *testing.T) {
t.Parallel()
var buffer bytes.Buffer
n, err := NewClipboard(&buffer).Write(nil)
require.NoError(t, err)
assert.Zero(t, n)
assert.Empty(t, buffer.String())
}
func TestClipboardWriteTooLarge(t *testing.T) {
t.Parallel()
n, err := NewClipboard(&bytes.Buffer{}).Write(make([]byte, maxClipboardBytes+1))
assert.Zero(t, n)
assert.Error(t, err)
}
ijq/config.go 0000664 0000000 0000000 00000004003 15265734051 0013442 0 ustar 00root root 0000000 0000000 // Copyright (C) 2026 Gregory Anders
//
// SPDX-License-Identifier: GPL-3.0-or-later
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"codeberg.org/emersion/go-scfg"
"codeberg.org/gpanders/ijq/internal/options"
)
type Config struct {
HistoryFile options.HistoryFile `scfg:"history-file"`
JQCommand options.JQCommand `scfg:"jq-bin"`
HideInputPane options.HideInputPane `scfg:"hide-input-pane"`
LibraryPaths options.LibraryPaths `scfg:"library-paths"`
CopyFilterToClipboardOnExit bool `scfg:"copy-filter-to-clipboard-on-exit"`
Keymap Keymap `scfg:"keymaps"`
}
func DefaultConfig() Config {
var dataDir string
if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" {
dataDir = dataHome
} else if home, err := os.UserHomeDir(); err == nil {
dataDir = filepath.Join(home, ".local", "share")
}
var historyFile options.HistoryFile
if dataDir != "" {
historyFile = options.HistoryFile(filepath.Join(dataDir, "ijq", "history"))
}
return Config{
HistoryFile: historyFile,
JQCommand: "jq",
HideInputPane: false,
Keymap: DefaultKeymap(),
}
}
func NewConfig(path string) (Config, error) {
cfg := DefaultConfig()
f, err := os.Open(filepath.Clean(path))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return cfg, nil
}
return Config{}, fmt.Errorf("failed to open config file: %w", err)
}
defer f.Close()
if err := scfg.NewDecoder(f).Decode(&cfg); err != nil {
return Config{}, fmt.Errorf("failed to parse config file: %w", err)
}
return cfg, nil
}
func DefaultConfigPath() (string, error) {
var config string
if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" {
config = configHome
} else if home, err := os.UserHomeDir(); err == nil {
config = filepath.Join(home, ".config")
} else {
return "", errors.New("couldn't find path to config file")
}
return filepath.Join(config, "ijq", "config"), nil
}
ijq/config_test.go 0000664 0000000 0000000 00000006065 15265734051 0014513 0 ustar 00root root 0000000 0000000 package main
import (
"os"
"path/filepath"
"testing"
"github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/assert"
"codeberg.org/gpanders/ijq/internal/options"
)
// These tests change process-wide environment variables and must run serially.
func TestDefaultConfigPathUsesXDGConfigHome(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", "/tmp/ijq-test-config")
path, err := DefaultConfigPath()
assert.NoError(t, err)
assert.Equal(t, filepath.Join("/tmp/ijq-test-config", "ijq", "config"), path)
}
func TestDefaultConfigPathFallsBackToHome(t *testing.T) {
tmp := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("HOME", tmp)
path, err := DefaultConfigPath()
assert.NoError(t, err)
assert.Equal(t, filepath.Join(tmp, ".config", "ijq", "config"), path)
}
func TestLoadConfigMissingFile(t *testing.T) {
tmp := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", tmp)
t.Setenv("XDG_DATA_HOME", tmp)
path, err := DefaultConfigPath()
assert.NoError(t, err)
cfg, err := NewConfig(path)
assert.NoError(t, err)
// Ensure that config has default values when no config file exists
assert.Equal(t, filepath.Join(tmp, "ijq", "history"), string(cfg.HistoryFile))
assert.Equal(t, "jq", string(cfg.JQCommand))
assert.False(t, bool(cfg.HideInputPane))
assert.False(t, cfg.CopyFilterToClipboardOnExit)
}
func TestLoadConfig(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "config")
raw := `history-file ""
jq-bin /usr/local/bin/jq
hide-input-pane true
library-paths /tmp/modules /opt/jq/modules
copy-filter-to-clipboard-on-exit true
keymaps {
toggle-input-pane Ctrl-T
save-filter-history Alt+h
copy-filter-to-clipboard Alt+f
copy-output-to-clipboard Alt+p
toggle-menu Alt+o
scroll-to-bottom g
}
`
err := os.WriteFile(path, []byte(raw), 0o644)
assert.NoError(t, err)
cfg, err := NewConfig(path)
assert.NoError(t, err)
assert.Equal(t, "", string(cfg.HistoryFile))
assert.Equal(t, "/usr/local/bin/jq", string(cfg.JQCommand))
assert.True(t, bool(cfg.HideInputPane))
assert.True(t, cfg.CopyFilterToClipboardOnExit)
assert.Equal(t, options.LibraryPaths{"/tmp/modules", "/opt/jq/modules"}, cfg.LibraryPaths)
assert.Equal(t, KeyBindings{{key: tcell.KeyCtrlT}}, cfg.Keymap.ToggleInputPane)
assert.Equal(t, KeyBindings{{key: tcell.KeyRune, rune: 'h', mods: tcell.ModAlt}}, cfg.Keymap.SaveFilterHistory)
assert.Equal(t, KeyBindings{{key: tcell.KeyRune, rune: 'f', mods: tcell.ModAlt}}, cfg.Keymap.CopyFilterToClipboard)
assert.Equal(t, KeyBindings{{key: tcell.KeyRune, rune: 'p', mods: tcell.ModAlt}}, cfg.Keymap.CopyOutputToClipboard)
assert.Equal(t, KeyBindings{{key: tcell.KeyRune, rune: 'o', mods: tcell.ModAlt}}, cfg.Keymap.ToggleMenu)
assert.Equal(t, KeyBindings{{key: tcell.KeyRune, rune: 'g'}}, cfg.Keymap.ScrollToBottom)
assert.Equal(t, DefaultKeymap().SubmitFilter, cfg.Keymap.SubmitFilter)
}
func TestLoadConfigInvalidFile(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "config")
err := os.WriteFile(path, []byte("keymaps {"), 0o644)
assert.NoError(t, err)
_, err = NewConfig(path)
assert.Error(t, err)
}
ijq/demo/ 0000775 0000000 0000000 00000000000 15265734051 0012575 5 ustar 00root root 0000000 0000000 ijq/demo/ijq.gif 0000664 0000000 0000000 00014645661 15265734051 0014076 0 ustar 00root root 0000000 0000000 GIF89a€8÷ÿ ! * 2 < > G Q W&
d.1n5y:=
k9W1 '
+}C
I+2$3ŽN=#‡L(!j?J4&
$~J›X �T!M""""#"# ,#3+$$$$B2&X'I'(-(";((()e)Ÿ`*©e+R=+cE,<4,H9,±j,¾p---.0001h3q31;3Š[4/E4445WE5ªk6666vT6Ÿg6±o6¹r7gM8"}81M8C=::::>E;1[<7O<<<=){=>==½y>KM?¹wAAAAwZB"*BiTB¬sC.…D?VDDDFŠfG>dGRSI¹}Jz`JŸqKMKLEeL—oL¬yM4—N@yN²}O=‰O¥vRJlRLaRQQRŒmSf\T·‚U?™U{fV V©}W?£WžxXSTZZZ[Qy[Za\B¬]]_```a™{cK®cY€c®†eW“eeee£‚ffggL¼juokaˆkkkn