mirror of
https://github.com/anotherhadi/jwt-tui.git
synced 2026-06-26 01:02:33 +02:00
init
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- binary: jwt-tui
|
||||||
|
main: ./cmd/jwt-tui
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X main.version={{.Version}}
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- formats:
|
||||||
|
- tar.gz
|
||||||
|
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: checksums.txt
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- "^docs:"
|
||||||
|
- "^test:"
|
||||||
|
- "^ci:"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Everybody is invited and welcome to contribute. There is a lot to do... Check the issues!
|
||||||
|
|
||||||
|
The process is straight-forward.
|
||||||
|
|
||||||
|
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1)
|
||||||
|
- Fork this git repository
|
||||||
|
- Write your changes (bug fixes, new features, ...).
|
||||||
|
- Create a Pull Request against the main branch.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ko_fi: anotherhadi
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PATTERN = re.compile(r"<!-- exec: (.+?) -->.*?<!-- endexec -->", re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def replace(match):
|
||||||
|
cmd = match.group(1).strip()
|
||||||
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||||
|
output = result.stdout
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(
|
||||||
|
f"[inject-exec] command failed ({result.returncode}): {cmd}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
output = re.sub(r"<!-- exec: .+? -->\n?|<!-- endexec -->\n?", "", output)
|
||||||
|
if output and not output.endswith("\n"):
|
||||||
|
output += "\n"
|
||||||
|
return f"<!-- exec: {cmd} -->\n{output}<!-- endexec -->"
|
||||||
|
|
||||||
|
|
||||||
|
def process(path):
|
||||||
|
content = Path(path).read_text()
|
||||||
|
new_content = PATTERN.sub(replace, content)
|
||||||
|
if new_content != content:
|
||||||
|
Path(path).write_text(new_content)
|
||||||
|
print(f"[inject-exec] updated {path}")
|
||||||
|
|
||||||
|
|
||||||
|
for p in sys.argv[1:]:
|
||||||
|
process(p)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
version: "~> v2"
|
||||||
|
args: release --clean --config .github/.goreleaser.yaml
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.claude/
|
||||||
|
CLAUDE.md
|
||||||
|
result/
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
.mypy_cache/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Hadi
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Jwt-tui
|
||||||
|
|
||||||
|
A terminal UI for inspecting, editing, and signing JSON Web Tokens (JWTs).
|
||||||
|
|
||||||
|
Built with [Bubbletea](https://charm.land/bubbletea) and [Lipgloss](https://charm.land/lipgloss).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Decode**: paste a JWT and instantly see the pretty-printed header and payload
|
||||||
|
- **Encode**: edit the header or payload JSON and get a freshly signed token
|
||||||
|
- **Verify**: real-time signature validation against a secret (HS256 / HS384 / HS512 / none)
|
||||||
|
- **4-panel layout**: header, payload, JWT, and secret all visible and editable at once
|
||||||
|
- **Themeable**: Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once.
|
||||||
|
- **Rebindable keys**: customize keybindings in the config file
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Go install</summary>
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install github.com/anotherhadi/jwt-tui/cmd/jwt-tui@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Go 1.22+. The binary will be placed in `$GOPATH/bin` (or `~/go/bin`).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Nix (temporary run, no install)</summary>
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run github:anotherhadi/jwt-tui
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>NixOS (flake)</summary>
|
||||||
|
|
||||||
|
Add jwt-tui to your flake inputs:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
inputs.jwt-tui.url = "github:anotherhadi/jwt-tui";
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add the package to your system or home-manager packages:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
environment.systemPackages = [ inputs.jwt-tui.packages.${pkgs.system}.default ];
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jwt-tui # launch with default config
|
||||||
|
jwt-tui -t <token> # pre-fill the JWT token
|
||||||
|
jwt-tui -t <token> -s mysecret # pre-fill token and secret key
|
||||||
|
jwt-tui -t $(cat token.txt) -s mysecret # read token from a file
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keybindings
|
||||||
|
|
||||||
|
<!-- exec: echo '```' && go run ./cmd/jwt-tui -h && echo '```' -->
|
||||||
|
```
|
||||||
|
Usage: jwt-tui [flags]
|
||||||
|
|
||||||
|
--add-default-config copy the default config file to the config path and exit
|
||||||
|
-c, --config string path to config file
|
||||||
|
-s, --secret string pre-fill the secret key
|
||||||
|
-t, --token string pre-fill the encoded JWT token
|
||||||
|
-v, --version print version
|
||||||
|
```
|
||||||
|
<!-- endexec -->
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The config file lives at `~/.config/jwt-tui/config.yaml` by default. Run `--add-default-config` to generate it.
|
||||||
|
|
||||||
|
Exemple:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
keybindings:
|
||||||
|
quit: "ctrl+c"
|
||||||
|
cycle_focus: "tab"
|
||||||
|
```
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"github.com/anotherhadi/jwt-tui/internal/config"
|
||||||
|
"github.com/anotherhadi/jwt-tui/internal/keys"
|
||||||
|
"github.com/anotherhadi/jwt-tui/internal/ui"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version is overwritten at build time by goreleaser/ldflag with the current version tag, or "dev" if not set.
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if version != "dev" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" && info.Main.Version != "(devel)" {
|
||||||
|
version = info.Main.Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
flagConfig = pflag.StringP("config", "c", "", "path to config file")
|
||||||
|
flagAddDefaultConfig = pflag.Bool("add-default-config", false, "copy the default config file to the config path and exit")
|
||||||
|
flagToken = pflag.StringP("token", "t", "", "pre-fill the encoded JWT token")
|
||||||
|
flagSecret = pflag.StringP("secret", "s", "", "pre-fill the secret key")
|
||||||
|
flagVersion = pflag.BoolP("version", "v", false, "print version")
|
||||||
|
)
|
||||||
|
pflag.CommandLine.SetOutput(os.Stdout)
|
||||||
|
pflag.Usage = func() {
|
||||||
|
fmt.Println("Usage: jwt-tui [flags]")
|
||||||
|
fmt.Println("")
|
||||||
|
pflag.PrintDefaults()
|
||||||
|
}
|
||||||
|
pflag.Parse()
|
||||||
|
|
||||||
|
if *flagVersion {
|
||||||
|
fmt.Println(version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept a bare positional argument as the token
|
||||||
|
if *flagToken == "" && pflag.NArg() > 0 {
|
||||||
|
t := pflag.Arg(0)
|
||||||
|
flagToken = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
cfgPath := filepath.Join(home, ".config", "jwt-tui", "config.yaml")
|
||||||
|
if *flagConfig != "" {
|
||||||
|
cfgPath = *flagConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
if *flagAddDefaultConfig {
|
||||||
|
if err := config.WriteDefaultConfig(cfgPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "write-config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("default config written to %s\n", cfgPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.Load(cfgPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.Init(config.Global)
|
||||||
|
|
||||||
|
m := ui.New(*flagToken, *flagSecret)
|
||||||
|
p := tea.NewProgram(m)
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "tui: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+142
@@ -0,0 +1,142 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767039857,
|
||||||
|
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"git-hooks": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778507602,
|
||||||
|
"narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"git-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gomod2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1770585520,
|
||||||
|
"narHash": "sha256-yBz9Ozd5Wb56i3e3cHZ8WcbzCQ9RlVaiW18qDYA/AzA=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"rev": "1201ddd1279c35497754f016ef33d5e060f3da8d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1779560665,
|
||||||
|
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"git-hooks": "git-hooks",
|
||||||
|
"gomod2nix": "gomod2nix",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
description = "A TUI for inspecting, editing, and signing JSON Web Tokens (JWTs).";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
gomod2nix = {
|
||||||
|
url = "github:nix-community/gomod2nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
git-hooks = {
|
||||||
|
url = "github:cachix/git-hooks.nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
gomod2nix,
|
||||||
|
git-hooks,
|
||||||
|
}: let
|
||||||
|
supportedSystems = ["x86_64-linux" "aarch64-linux"];
|
||||||
|
|
||||||
|
forAllSystems = f:
|
||||||
|
nixpkgs.lib.genAttrs supportedSystems
|
||||||
|
(system: f system (import nixpkgs {inherit system;}));
|
||||||
|
in {
|
||||||
|
packages = forAllSystems (system: pkgs:
|
||||||
|
import ./nix/package.nix {
|
||||||
|
inherit pkgs;
|
||||||
|
buildGoApplication = gomod2nix.legacyPackages.${system}.buildGoApplication;
|
||||||
|
});
|
||||||
|
devShells = forAllSystems (system: pkgs: {
|
||||||
|
default = import ./nix/shell.nix {
|
||||||
|
inherit pkgs;
|
||||||
|
gitHooksLib = git-hooks.lib.${system};
|
||||||
|
gomod2nixPkgs = gomod2nix.legacyPackages.${system};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
module github.com/anotherhadi/jwt-tui
|
||||||
|
|
||||||
|
go 1.26.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
charm.land/bubbles/v2 v2.1.0
|
||||||
|
charm.land/bubbletea/v2 v2.0.6
|
||||||
|
charm.land/glamour/v2 v2.0.0
|
||||||
|
charm.land/lipgloss/v2 v2.0.3
|
||||||
|
github.com/anotherhadi/ilovetui v0.1.6
|
||||||
|
github.com/spf13/pflag v1.0.10
|
||||||
|
github.com/spf13/viper v1.21.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/net v0.39.0 // indirect
|
||||||
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
|
||||||
|
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
|
||||||
|
charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo=
|
||||||
|
charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g=
|
||||||
|
charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U=
|
||||||
|
charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w=
|
||||||
|
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
|
||||||
|
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
|
||||||
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
|
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||||
|
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||||
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
|
github.com/anotherhadi/ilovetui v0.1.6 h1:NKg+T1DpV08Q4r+iowFrXF+0bTd6Y2f4OFpFwhsfsyY=
|
||||||
|
github.com/anotherhadi/ilovetui v0.1.6/go.mod h1:HVai6u5NGKSMOpmioYpwrN0lSxQjc7HtISUc5hTwvOw=
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac=
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed default_config.yaml
|
||||||
|
var defaultConfig []byte
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Keybindings Keybindings `mapstructure:"keybindings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var Global *Config
|
||||||
|
|
||||||
|
func Load(path string) error {
|
||||||
|
var defaults map[string]any
|
||||||
|
if err := yaml.Unmarshal(defaultConfig, &defaults); err != nil {
|
||||||
|
return fmt.Errorf("default config: %w", err)
|
||||||
|
}
|
||||||
|
for k, v := range flatten("", defaults) {
|
||||||
|
viper.SetDefault(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
viper.SetConfigFile(path)
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Global = &Config{}
|
||||||
|
return viper.Unmarshal(Global)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteDefaultConfig(path string) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create config dir: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, defaultConfig, 0o600); err != nil {
|
||||||
|
return fmt.Errorf("write config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func flatten(prefix string, m map[string]any) map[string]any {
|
||||||
|
out := make(map[string]any)
|
||||||
|
for k, v := range m {
|
||||||
|
key := k
|
||||||
|
if prefix != "" {
|
||||||
|
key = prefix + "." + k
|
||||||
|
}
|
||||||
|
if nested, ok := v.(map[string]any); ok {
|
||||||
|
for nk, nv := range flatten(key, nested) {
|
||||||
|
out[nk] = nv
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out[key] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
keybindings:
|
||||||
|
quit: "ctrl+c,q"
|
||||||
|
cycle_focus: "tab"
|
||||||
|
edit: "e,enter"
|
||||||
|
edit_external: "E"
|
||||||
|
docs: "d"
|
||||||
|
help_toggle: "?"
|
||||||
|
clear: "x"
|
||||||
|
reset: "r"
|
||||||
|
copy: "y,ctrl+shift+c"
|
||||||
|
paste: "p,ctrl+shift+v"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Keybindings struct {
|
||||||
|
Quit string `mapstructure:"quit"`
|
||||||
|
CycleFocus string `mapstructure:"cycle_focus"`
|
||||||
|
Edit string `mapstructure:"edit"`
|
||||||
|
EditExternal string `mapstructure:"edit_external"`
|
||||||
|
Docs string `mapstructure:"docs"`
|
||||||
|
HelpToggle string `mapstructure:"help_toggle"`
|
||||||
|
Clear string `mapstructure:"clear"`
|
||||||
|
Reset string `mapstructure:"reset"`
|
||||||
|
Copy string `mapstructure:"copy"`
|
||||||
|
Paste string `mapstructure:"paste"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package highlight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
|
"image/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
func paint(c color.Color, s string) string {
|
||||||
|
return lipgloss.NewStyle().Foreground(c).Render(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON applies syntax coloring to a pretty-printed JSON string using ilovetui colors.
|
||||||
|
func JSON(s string) string {
|
||||||
|
var out strings.Builder
|
||||||
|
i, n := 0, len(s)
|
||||||
|
for i < n {
|
||||||
|
ch := s[i]
|
||||||
|
switch {
|
||||||
|
case ch == '"':
|
||||||
|
j := i + 1
|
||||||
|
for j < n {
|
||||||
|
if s[j] == '\\' {
|
||||||
|
j += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s[j] == '"' {
|
||||||
|
j++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
str := s[i:j]
|
||||||
|
k := j
|
||||||
|
for k < n && (s[k] == ' ' || s[k] == '\t') {
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
if k < n && s[k] == ':' {
|
||||||
|
out.WriteString(paint(ilovetui.S.Primary, str))
|
||||||
|
} else {
|
||||||
|
out.WriteString(paint(ilovetui.S.Success, str))
|
||||||
|
}
|
||||||
|
i = j
|
||||||
|
case (ch >= '0' && ch <= '9') || (ch == '-' && i+1 < n && s[i+1] >= '0' && s[i+1] <= '9'):
|
||||||
|
j := i
|
||||||
|
if s[j] == '-' {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
for j < n && ((s[j] >= '0' && s[j] <= '9') || s[j] == '.' || s[j] == 'e' || s[j] == 'E' || s[j] == '+' || s[j] == '-') {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
out.WriteString(paint(ilovetui.S.Warning, s[i:j]))
|
||||||
|
i = j
|
||||||
|
case i+4 <= n && s[i:i+4] == "true":
|
||||||
|
out.WriteString(paint(ilovetui.S.Error, "true"))
|
||||||
|
i += 4
|
||||||
|
case i+5 <= n && s[i:i+5] == "false":
|
||||||
|
out.WriteString(paint(ilovetui.S.Error, "false"))
|
||||||
|
i += 5
|
||||||
|
case i+4 <= n && s[i:i+4] == "null":
|
||||||
|
out.WriteString(paint(ilovetui.S.Muted, "null"))
|
||||||
|
i += 4
|
||||||
|
case ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == ':' || ch == ',':
|
||||||
|
out.WriteString(paint(ilovetui.S.Subtle, string(ch)))
|
||||||
|
i++
|
||||||
|
default:
|
||||||
|
out.WriteByte(ch)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT colors the three dot-separated parts of a JWT token in distinct colors.
|
||||||
|
func JWT(s string) string {
|
||||||
|
dot := paint(ilovetui.S.Subtle, ".")
|
||||||
|
parts := strings.SplitN(s, ".", 3)
|
||||||
|
switch len(parts) {
|
||||||
|
case 1:
|
||||||
|
return paint(ilovetui.S.Primary, parts[0])
|
||||||
|
case 2:
|
||||||
|
return paint(ilovetui.S.Primary, parts[0]) + dot + paint(ilovetui.S.Success, parts[1])
|
||||||
|
default:
|
||||||
|
return paint(ilovetui.S.Primary, parts[0]) + dot +
|
||||||
|
paint(ilovetui.S.Success, parts[1]) + dot +
|
||||||
|
paint(ilovetui.S.Warning, parts[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decode splits a JWT and returns pretty-printed header and payload JSON.
|
||||||
|
func Decode(token string) (header, payload string, err error) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return "", "", fmt.Errorf("expected 3 parts, got %d", len(parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
hdrBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("header: %w", err)
|
||||||
|
}
|
||||||
|
plBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hdrObj any
|
||||||
|
if err := json.Unmarshal(hdrBytes, &hdrObj); err != nil {
|
||||||
|
return "", "", fmt.Errorf("header JSON: %w", err)
|
||||||
|
}
|
||||||
|
var plObj any
|
||||||
|
if err := json.Unmarshal(plBytes, &plObj); err != nil {
|
||||||
|
return "", "", fmt.Errorf("payload JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hdrPretty, _ := json.MarshalIndent(hdrObj, "", " ")
|
||||||
|
plPretty, _ := json.MarshalIndent(plObj, "", " ")
|
||||||
|
|
||||||
|
return string(hdrPretty), string(plPretty), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode builds and signs a JWT from raw JSON header and payload strings.
|
||||||
|
func Encode(header, payload, secret string) (string, error) {
|
||||||
|
var hdrObj map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(header), &hdrObj); err != nil {
|
||||||
|
return "", fmt.Errorf("header JSON: %w", err)
|
||||||
|
}
|
||||||
|
var plObj any
|
||||||
|
if err := json.Unmarshal([]byte(payload), &plObj); err != nil {
|
||||||
|
return "", fmt.Errorf("payload JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hdrCompact, _ := json.Marshal(hdrObj)
|
||||||
|
plCompact, _ := json.Marshal(plObj)
|
||||||
|
|
||||||
|
hdrB64 := base64.RawURLEncoding.EncodeToString(hdrCompact)
|
||||||
|
plB64 := base64.RawURLEncoding.EncodeToString(plCompact)
|
||||||
|
signingInput := hdrB64 + "." + plB64
|
||||||
|
|
||||||
|
alg, _ := hdrObj["alg"].(string)
|
||||||
|
|
||||||
|
h, err := hashForAlg(alg)
|
||||||
|
if err != nil {
|
||||||
|
return signingInput + ".", fmt.Errorf("%w", err)
|
||||||
|
}
|
||||||
|
if h == nil {
|
||||||
|
return signingInput + ".", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mac := hmac.New(h, []byte(secret))
|
||||||
|
mac.Write([]byte(signingInput))
|
||||||
|
sig := mac.Sum(nil)
|
||||||
|
|
||||||
|
return signingInput + "." + base64.RawURLEncoding.EncodeToString(sig), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks whether the JWT signature is valid for the given secret.
|
||||||
|
// Returns (false, nil) for an invalid signature, (true, nil) for valid.
|
||||||
|
func Verify(token, secret string) (bool, error) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return false, fmt.Errorf("expected 3 parts, got %d", len(parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
hdrBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("header encoding: %w", err)
|
||||||
|
}
|
||||||
|
var hdrObj map[string]any
|
||||||
|
if err := json.Unmarshal(hdrBytes, &hdrObj); err != nil {
|
||||||
|
return false, fmt.Errorf("header JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
alg, _ := hdrObj["alg"].(string)
|
||||||
|
|
||||||
|
h, err := hashForAlg(alg)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if h == nil {
|
||||||
|
return parts[2] == "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
signingInput := parts[0] + "." + parts[1]
|
||||||
|
mac := hmac.New(h, []byte(secret))
|
||||||
|
mac.Write([]byte(signingInput))
|
||||||
|
expected := mac.Sum(nil)
|
||||||
|
|
||||||
|
actual, err := base64.RawURLEncoding.DecodeString(parts[2])
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("signature encoding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hmac.Equal(actual, expected), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Algorithm returns the "alg" claim from the JWT header, or "" if unreadable.
|
||||||
|
func Algorithm(token string) string {
|
||||||
|
parts := strings.SplitN(token, ".", 3)
|
||||||
|
if len(parts) < 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
hdrBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var hdrObj map[string]any
|
||||||
|
if err := json.Unmarshal(hdrBytes, &hdrObj); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
alg, _ := hdrObj["alg"].(string)
|
||||||
|
return alg
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashForAlg(alg string) (func() hash.Hash, error) {
|
||||||
|
switch strings.ToUpper(alg) {
|
||||||
|
case "HS256":
|
||||||
|
return sha256.New, nil
|
||||||
|
case "HS384":
|
||||||
|
return sha512.New384, nil
|
||||||
|
case "HS512":
|
||||||
|
return sha512.New, nil
|
||||||
|
case "NONE", "":
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported algorithm: %s", alg)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
|
"github.com/anotherhadi/jwt-tui/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeyMap struct {
|
||||||
|
Quit key.Binding
|
||||||
|
CycleFocus key.Binding
|
||||||
|
Edit key.Binding
|
||||||
|
EditExternal key.Binding
|
||||||
|
Docs key.Binding
|
||||||
|
HelpToggle key.Binding
|
||||||
|
Clear key.Binding
|
||||||
|
Reset key.Binding
|
||||||
|
Copy key.Binding
|
||||||
|
Paste key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
var Keys *KeyMap
|
||||||
|
|
||||||
|
func Init(cfg *config.Config) {
|
||||||
|
kb := cfg.Keybindings
|
||||||
|
Keys = &KeyMap{
|
||||||
|
Quit: binding(kb.Quit, "quit"),
|
||||||
|
CycleFocus: binding(kb.CycleFocus, "cycle focus"),
|
||||||
|
Edit: binding(kb.Edit, "edit"),
|
||||||
|
EditExternal: binding(kb.EditExternal, "edit in $EDITOR"),
|
||||||
|
Docs: binding(kb.Docs, "docs"),
|
||||||
|
HelpToggle: binding(kb.HelpToggle, "help"),
|
||||||
|
Clear: binding(kb.Clear, "clear"),
|
||||||
|
Reset: binding(kb.Reset, "reset"),
|
||||||
|
Copy: binding(kb.Copy, "copy"),
|
||||||
|
Paste: binding(kb.Paste, "paste"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKeys(s string) []string {
|
||||||
|
parts := strings.Split(s, ",")
|
||||||
|
out := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
if k := strings.TrimSpace(p); k != "" {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChunkByWidth(bindings []key.Binding, termWidth int) [][]key.Binding {
|
||||||
|
cols := termWidth / 26
|
||||||
|
if cols < 2 {
|
||||||
|
cols = 2
|
||||||
|
} else if cols > 7 {
|
||||||
|
cols = 7
|
||||||
|
}
|
||||||
|
perCol := (len(bindings) + cols - 1) / cols
|
||||||
|
var out [][]key.Binding
|
||||||
|
for i := 0; i < len(bindings); i += perCol {
|
||||||
|
end := i + perCol
|
||||||
|
if end > len(bindings) {
|
||||||
|
end = len(bindings)
|
||||||
|
}
|
||||||
|
out = append(out, bindings[i:end])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func binding(s, help string) key.Binding {
|
||||||
|
keys := parseKeys(s)
|
||||||
|
display := strings.Join(keys, "/")
|
||||||
|
return key.NewBinding(key.WithKeys(keys...), key.WithHelp(display, help))
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package style
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PanelContentH(totalH int) int {
|
||||||
|
h := totalH - 2
|
||||||
|
if h < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderWithTitle renders a bordered box with a title embedded in the top border.
|
||||||
|
// The title may contain ANSI color codes. width and height are the total outer dimensions.
|
||||||
|
func RenderWithTitle(border lipgloss.Style, title, content string, width, height int) string {
|
||||||
|
boxH := height - 1
|
||||||
|
if contentH := boxH - 1; contentH > 0 {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
if len(lines) > contentH {
|
||||||
|
content = strings.Join(lines[:contentH], "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
box := border.BorderTop(false).Width(width).Height(boxH).Render(content)
|
||||||
|
|
||||||
|
boxWidth := lipgloss.Width(strings.SplitN(box, "\n", 2)[0])
|
||||||
|
titleW := lipgloss.Width(title) // strips ANSI for measurement
|
||||||
|
fillW := boxWidth - titleW - 4 // 4 = "╭ " + " " + "╮"
|
||||||
|
if fillW < 0 {
|
||||||
|
fillW = 0
|
||||||
|
}
|
||||||
|
bc := lipgloss.NewStyle().Foreground(border.GetBorderTopForeground())
|
||||||
|
topLine := bc.Render("╭ ") + title + bc.Render(" "+strings.Repeat("─", fillW)+"╮")
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, topLine, box)
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# JWT Reference
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
A JSON Web Token is three Base64URL-encoded parts joined by dots:
|
||||||
|
|
||||||
|
```
|
||||||
|
header.payload.signature
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Header**: algorithm and token type
|
||||||
|
- **Payload**: claims (statements about an entity)
|
||||||
|
- **Signature**: HMAC or RSA/ECDSA over header + payload
|
||||||
|
|
||||||
|
The header and payload are readable by anyone. JWTs are _signed_, not _encrypted_.
|
||||||
|
Use JWE if you need confidentiality.
|
||||||
|
|
||||||
|
## Header
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"alg": "HS256",
|
||||||
|
"typ": "JWT"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common header parameters:
|
||||||
|
|
||||||
|
| Param | Description |
|
||||||
|
| ----- | ------------------------------------------ |
|
||||||
|
| `alg` | Signing algorithm (`HS256`, `RS256`, etc.) |
|
||||||
|
| `typ` | Token type: always `JWT` |
|
||||||
|
| `kid` | Key ID: hint for which key to use |
|
||||||
|
| `cty` | Content type: used for nested JWTs |
|
||||||
|
|
||||||
|
## Payload (Claims)
|
||||||
|
|
||||||
|
**Registered claims** (all optional, but recommended):
|
||||||
|
|
||||||
|
| Claim | Type | Description |
|
||||||
|
| ----- | ------ | --------------------------------------- |
|
||||||
|
| `iss` | string | Issuer: who created the token |
|
||||||
|
| `sub` | string | Subject: principal the token is about |
|
||||||
|
| `aud` | string | Audience: intended recipient(s) |
|
||||||
|
| `exp` | number | Expiration time (Unix timestamp) |
|
||||||
|
| `nbf` | number | Not before: token valid after this time |
|
||||||
|
| `iat` | number | Issued at (Unix timestamp) |
|
||||||
|
| `jti` | string | JWT ID: unique identifier |
|
||||||
|
|
||||||
|
**Private claims** are any additional fields agreed upon by the parties.
|
||||||
|
|
||||||
|
## Algorithms
|
||||||
|
|
||||||
|
| Algorithm | Type | Key type |
|
||||||
|
| --------- | -------------- | ------------------------- |
|
||||||
|
| `HS256` | HMAC + SHA-256 | Shared secret |
|
||||||
|
| `HS384` | HMAC + SHA-384 | Shared secret |
|
||||||
|
| `HS512` | HMAC + SHA-512 | Shared secret |
|
||||||
|
| `RS256` | RSA + SHA-256 | RSA key pair |
|
||||||
|
| `RS384` | RSA + SHA-384 | RSA key pair |
|
||||||
|
| `RS512` | RSA + SHA-512 | RSA key pair |
|
||||||
|
| `ES256` | ECDSA + P-256 | EC key pair |
|
||||||
|
| `ES384` | ECDSA + P-384 | EC key pair |
|
||||||
|
| `ES512` | ECDSA + P-521 | EC key pair |
|
||||||
|
| `none` | No signature | ⚠ Never use in production |
|
||||||
|
|
||||||
|
> This tool supports **HS256**, **HS384**, and **HS512**.
|
||||||
|
|
||||||
|
## Signature Computation
|
||||||
|
|
||||||
|
For HMAC algorithms:
|
||||||
|
|
||||||
|
```
|
||||||
|
signature = HMAC-SHA256(
|
||||||
|
base64url(header) + "." + base64url(payload),
|
||||||
|
secret
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The final token:
|
||||||
|
|
||||||
|
```
|
||||||
|
base64url(header) + "." + base64url(payload) + "." + base64url(signature)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **Never use `alg: none`**: disables signature verification entirely.
|
||||||
|
- Use **long, random secrets**: at least 256 bits (32 bytes) for HS256.
|
||||||
|
- Always validate **`exp`** (expiration) and **`nbf`** (not before).
|
||||||
|
- Validate **`iss`** and **`aud`** to prevent token reuse across services.
|
||||||
|
- The payload is **base64-encoded, not encrypted**: never store passwords or PII.
|
||||||
|
- Prefer **asymmetric algorithms** (RS256, ES256) for public-facing APIs.
|
||||||
|
- Store secrets in environment variables or a secrets manager, never in code.
|
||||||
|
|
||||||
|
## Brute-forcing a JWT Secret
|
||||||
|
|
||||||
|
If a token is signed with a weak HMAC secret, it can be recovered offline.
|
||||||
|
Both **hashcat** and **john** accept the raw JWT string as input:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# hashcat mode 16500 targets JWT (HS256/384/512)
|
||||||
|
hashcat -a 0 -m 16500 <token> wordlist.txt
|
||||||
|
|
||||||
|
# john the ripper
|
||||||
|
john --format=HMAC-SHA256 --wordlist=wordlist.txt jwt.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
This only works against **HS\*** algorithms where the secret is a simple password or passphrase.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
jwt-tui looks for a config file at `~/.config/jwt-tui/config.yaml`.
|
||||||
|
If the file does not exist the built-in defaults are used automatically.
|
||||||
|
|
||||||
|
To get a starting point you can edit, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt-tui --add-default-config
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes the default config to `~/.config/jwt-tui/config.yaml` (or to the path given with `--config`).
|
||||||
|
You can then open that file in any text editor and change the values you want.
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/help"
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
|
"charm.land/bubbles/v2/textarea"
|
||||||
|
"charm.land/bubbles/v2/viewport"
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/glamour/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
|
"github.com/anotherhadi/jwt-tui/internal/highlight"
|
||||||
|
"github.com/anotherhadi/jwt-tui/internal/jwt"
|
||||||
|
"github.com/anotherhadi/jwt-tui/internal/keys"
|
||||||
|
"github.com/anotherhadi/jwt-tui/internal/style"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed docs.md
|
||||||
|
var jwtDocsMD string
|
||||||
|
|
||||||
|
// Panel indices in clockwise order starting top-left:
|
||||||
|
//
|
||||||
|
// top-left (0=JWT) → top-right (1=Header)
|
||||||
|
// ↓
|
||||||
|
// bot-left (3=Secret) ← bot-right (2=Payload)
|
||||||
|
const (
|
||||||
|
panelJWT = 0
|
||||||
|
panelHeader = 1
|
||||||
|
panelPayload = 2
|
||||||
|
panelSecret = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const exampleJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
|
||||||
|
|
||||||
|
var panelPlaceholders = [4]string{
|
||||||
|
exampleJWT,
|
||||||
|
"{\n \"alg\": \"HS256\",\n \"typ\": \"JWT\"\n}",
|
||||||
|
"{\n \"sub\": \"1234567890\",\n \"name\": \"John Doe\",\n \"iat\": 1516239022\n}",
|
||||||
|
"your-256-bit-secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
var panelTAPlaceholders = [4]string{
|
||||||
|
exampleJWT,
|
||||||
|
"{\n \"alg\": \"HS256\",\n \"typ\": \"JWT\"\n}",
|
||||||
|
"{\n \"sub\": \"1234567890\",\n \"name\": \"John Doe\",\n \"iat\": 1516239022\n}",
|
||||||
|
"your-256-bit-secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
type panelState struct {
|
||||||
|
vp viewport.Model
|
||||||
|
ta textarea.Model
|
||||||
|
editing bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyMap struct {
|
||||||
|
CycleFocus key.Binding
|
||||||
|
Edit key.Binding
|
||||||
|
EditExternal key.Binding
|
||||||
|
Clear key.Binding
|
||||||
|
Reset key.Binding
|
||||||
|
Copy key.Binding
|
||||||
|
Paste key.Binding
|
||||||
|
Docs key.Binding
|
||||||
|
HelpToggle key.Binding
|
||||||
|
Quit key.Binding
|
||||||
|
width int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k keyMap) ShortHelp() []key.Binding {
|
||||||
|
return []key.Binding{k.CycleFocus, k.Edit, k.EditExternal, k.HelpToggle, k.Quit}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k keyMap) FullHelp() [][]key.Binding {
|
||||||
|
all := []key.Binding{k.CycleFocus, k.Edit, k.EditExternal, k.Copy, k.Paste, k.Clear, k.Reset, k.Docs, k.Quit}
|
||||||
|
return keys.ChunkByWidth(all, k.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
type docsKeyMap struct {
|
||||||
|
Close key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k docsKeyMap) ShortHelp() []key.Binding { return []key.Binding{k.Close} }
|
||||||
|
func (k docsKeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{{k.Close}} }
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
panels [4]panelState
|
||||||
|
initial [4]string // per-panel initial values (for reset)
|
||||||
|
focus int
|
||||||
|
|
||||||
|
showDocs bool
|
||||||
|
docsVP viewport.Model
|
||||||
|
|
||||||
|
pendingEditorPanel int
|
||||||
|
pendingPastePanel int
|
||||||
|
|
||||||
|
sigValid *bool
|
||||||
|
sigStatus string
|
||||||
|
errMsg string
|
||||||
|
|
||||||
|
help help.Model
|
||||||
|
keymap keyMap
|
||||||
|
docsKeys docsKeyMap
|
||||||
|
|
||||||
|
width, height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(initialToken, initialSecret string) Model {
|
||||||
|
token := strings.TrimSpace(initialToken)
|
||||||
|
secret := strings.TrimSpace(initialSecret)
|
||||||
|
|
||||||
|
var initVals [4]string
|
||||||
|
initVals[panelJWT] = token
|
||||||
|
initVals[panelSecret] = secret
|
||||||
|
if token != "" {
|
||||||
|
header, payload, _ := jwt.Decode(token)
|
||||||
|
initVals[panelHeader] = header
|
||||||
|
initVals[panelPayload] = payload
|
||||||
|
}
|
||||||
|
|
||||||
|
m := Model{
|
||||||
|
initial: initVals,
|
||||||
|
focus: panelJWT,
|
||||||
|
help: ilovetui.NewHelp(),
|
||||||
|
keymap: keyMap{
|
||||||
|
CycleFocus: keys.Keys.CycleFocus,
|
||||||
|
Edit: keys.Keys.Edit,
|
||||||
|
EditExternal: keys.Keys.EditExternal,
|
||||||
|
Clear: keys.Keys.Clear,
|
||||||
|
Reset: keys.Keys.Reset,
|
||||||
|
Copy: keys.Keys.Copy,
|
||||||
|
Paste: keys.Keys.Paste,
|
||||||
|
Docs: keys.Keys.Docs,
|
||||||
|
HelpToggle: keys.Keys.HelpToggle,
|
||||||
|
Quit: keys.Keys.Quit,
|
||||||
|
},
|
||||||
|
docsKeys: docsKeyMap{
|
||||||
|
Close: keys.Keys.Docs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range m.panels {
|
||||||
|
ta := ilovetui.NewTextarea(false)
|
||||||
|
ta.Placeholder = panelTAPlaceholders[i]
|
||||||
|
vp := ilovetui.NewViewport()
|
||||||
|
vp.SoftWrap = true
|
||||||
|
m.panels[i].ta = ta
|
||||||
|
m.panels[i].vp = vp
|
||||||
|
}
|
||||||
|
|
||||||
|
m.docsVP = ilovetui.NewViewport()
|
||||||
|
m.docsVP.SoftWrap = true
|
||||||
|
|
||||||
|
for i, val := range initVals {
|
||||||
|
m.panels[i].ta.SetValue(val)
|
||||||
|
if val != "" {
|
||||||
|
m.setViewportContent(i, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != "" {
|
||||||
|
m.revalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) helpHeight() int {
|
||||||
|
if !m.help.ShowAll {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
max := 0
|
||||||
|
for _, col := range m.keymap.FullHelp() {
|
||||||
|
if len(col) > max {
|
||||||
|
max = len(col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) setViewportContent(panel int, raw string) {
|
||||||
|
var content string
|
||||||
|
switch panel {
|
||||||
|
case panelHeader, panelPayload:
|
||||||
|
content = highlight.JSON(raw)
|
||||||
|
case panelJWT:
|
||||||
|
content = highlight.JWT(raw)
|
||||||
|
default:
|
||||||
|
content = lipgloss.NewStyle().Foreground(ilovetui.S.Text).Render(raw)
|
||||||
|
}
|
||||||
|
m.panels[panel].vp.SetContent(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) recalcSizes() {
|
||||||
|
if m.width == 0 || m.height == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
leftW := m.width / 2
|
||||||
|
rightW := m.width - leftW
|
||||||
|
helpH := m.helpHeight()
|
||||||
|
availH := m.height - helpH - 1 // -1 for the error line
|
||||||
|
topH := availH / 2
|
||||||
|
bottomH := availH - topH
|
||||||
|
|
||||||
|
setPanel := func(idx, w, h int) {
|
||||||
|
cw := max(1, w-2)
|
||||||
|
ch := max(1, h-2)
|
||||||
|
m.panels[idx].vp.SetWidth(cw)
|
||||||
|
m.panels[idx].vp.SetHeight(ch)
|
||||||
|
m.panels[idx].ta.SetWidth(cw)
|
||||||
|
m.panels[idx].ta.SetHeight(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPanel(panelJWT, leftW, topH)
|
||||||
|
setPanel(panelHeader, rightW, topH)
|
||||||
|
setPanel(panelPayload, rightW, bottomH)
|
||||||
|
setPanel(panelSecret, leftW, bottomH)
|
||||||
|
|
||||||
|
m.help.SetWidth(m.width)
|
||||||
|
|
||||||
|
docsAvailH := m.height - 1
|
||||||
|
m.docsVP.SetHeight(max(1, docsAvailH-2))
|
||||||
|
m.docsVP.SetWidth(max(1, m.width-4))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) revalidate() {
|
||||||
|
jwtVal := m.panels[panelJWT].ta.Value()
|
||||||
|
secVal := m.panels[panelSecret].ta.Value()
|
||||||
|
if jwtVal == "" {
|
||||||
|
m.sigValid = nil
|
||||||
|
m.sigStatus = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
valid, err := jwt.Verify(jwtVal, secVal)
|
||||||
|
if err != nil {
|
||||||
|
m.sigValid = nil
|
||||||
|
m.sigStatus = ""
|
||||||
|
m.errMsg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.errMsg = ""
|
||||||
|
m.sigValid = &valid
|
||||||
|
if valid {
|
||||||
|
m.sigStatus = "✓ Signature Verified"
|
||||||
|
} else {
|
||||||
|
m.sigStatus = "✗ Invalid Signature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) decodeJWT() {
|
||||||
|
token := m.panels[panelJWT].ta.Value()
|
||||||
|
if token == "" {
|
||||||
|
m.sigValid = nil
|
||||||
|
m.sigStatus = ""
|
||||||
|
m.errMsg = ""
|
||||||
|
m.panels[panelHeader].ta.SetValue("")
|
||||||
|
m.panels[panelHeader].vp.SetContent("")
|
||||||
|
m.panels[panelPayload].ta.SetValue("")
|
||||||
|
m.panels[panelPayload].vp.SetContent("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
header, payload, err := jwt.Decode(token)
|
||||||
|
if err != nil {
|
||||||
|
m.sigValid = nil
|
||||||
|
m.sigStatus = ""
|
||||||
|
m.errMsg = err.Error()
|
||||||
|
m.panels[panelHeader].ta.SetValue("")
|
||||||
|
m.panels[panelHeader].vp.SetContent("")
|
||||||
|
m.panels[panelPayload].ta.SetValue("")
|
||||||
|
m.panels[panelPayload].vp.SetContent("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.errMsg = ""
|
||||||
|
m.panels[panelHeader].ta.SetValue(header)
|
||||||
|
m.panels[panelPayload].ta.SetValue(payload)
|
||||||
|
m.setViewportContent(panelHeader, header)
|
||||||
|
m.setViewportContent(panelPayload, payload)
|
||||||
|
m.revalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) rebuildJWT() {
|
||||||
|
header := m.panels[panelHeader].ta.Value()
|
||||||
|
payload := m.panels[panelPayload].ta.Value()
|
||||||
|
if header == "" && payload == "" {
|
||||||
|
m.panels[panelJWT].ta.SetValue("")
|
||||||
|
m.panels[panelJWT].vp.SetContent("")
|
||||||
|
m.sigValid = nil
|
||||||
|
m.sigStatus = ""
|
||||||
|
m.errMsg = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token, err := jwt.Encode(header, payload, m.panels[panelSecret].ta.Value())
|
||||||
|
if err != nil {
|
||||||
|
m.sigValid = nil
|
||||||
|
m.sigStatus = ""
|
||||||
|
m.errMsg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.errMsg = ""
|
||||||
|
m.panels[panelJWT].ta.SetValue(token)
|
||||||
|
m.setViewportContent(panelJWT, token)
|
||||||
|
m.revalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) exitEditMode() {
|
||||||
|
p := &m.panels[m.focus]
|
||||||
|
if !p.editing {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.editing = false
|
||||||
|
p.ta.Placeholder = panelTAPlaceholders[m.focus]
|
||||||
|
p.ta.Blur()
|
||||||
|
raw := p.ta.Value()
|
||||||
|
if raw != "" {
|
||||||
|
m.setViewportContent(m.focus, raw)
|
||||||
|
}
|
||||||
|
switch m.focus {
|
||||||
|
case panelHeader, panelPayload:
|
||||||
|
m.rebuildJWT()
|
||||||
|
case panelJWT:
|
||||||
|
m.decodeJWT()
|
||||||
|
case panelSecret:
|
||||||
|
m.revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) enterEditMode() tea.Cmd {
|
||||||
|
p := &m.panels[m.focus]
|
||||||
|
p.editing = true
|
||||||
|
p.ta.Placeholder = ""
|
||||||
|
return p.ta.Focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearPanel empties the focused panel and enters edit mode.
|
||||||
|
func (m *Model) clearPanel() tea.Cmd {
|
||||||
|
m.exitEditMode()
|
||||||
|
m.panels[m.focus].ta.SetValue("")
|
||||||
|
m.panels[m.focus].vp.SetContent("")
|
||||||
|
// Clearing JWT also wipes derived header/payload
|
||||||
|
if m.focus == panelJWT {
|
||||||
|
m.panels[panelHeader].ta.SetValue("")
|
||||||
|
m.panels[panelHeader].vp.SetContent("")
|
||||||
|
m.panels[panelPayload].ta.SetValue("")
|
||||||
|
m.panels[panelPayload].vp.SetContent("")
|
||||||
|
m.sigValid = nil
|
||||||
|
m.sigStatus = ""
|
||||||
|
} else {
|
||||||
|
switch m.focus {
|
||||||
|
case panelHeader, panelPayload:
|
||||||
|
m.rebuildJWT()
|
||||||
|
case panelSecret:
|
||||||
|
m.revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.enterEditMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetPanel restores the focused panel to its initial value.
|
||||||
|
func (m *Model) resetPanel() {
|
||||||
|
m.exitEditMode()
|
||||||
|
val := m.initial[m.focus]
|
||||||
|
m.panels[m.focus].ta.SetValue(val)
|
||||||
|
if val != "" {
|
||||||
|
m.setViewportContent(m.focus, val)
|
||||||
|
} else {
|
||||||
|
m.panels[m.focus].vp.SetContent("")
|
||||||
|
}
|
||||||
|
// Resetting JWT also restores derived header/payload from initial
|
||||||
|
if m.focus == panelJWT {
|
||||||
|
m.panels[panelHeader].ta.SetValue(m.initial[panelHeader])
|
||||||
|
if m.initial[panelHeader] != "" {
|
||||||
|
m.setViewportContent(panelHeader, m.initial[panelHeader])
|
||||||
|
} else {
|
||||||
|
m.panels[panelHeader].vp.SetContent("")
|
||||||
|
}
|
||||||
|
m.panels[panelPayload].ta.SetValue(m.initial[panelPayload])
|
||||||
|
if m.initial[panelPayload] != "" {
|
||||||
|
m.setViewportContent(panelPayload, m.initial[panelPayload])
|
||||||
|
} else {
|
||||||
|
m.panels[panelPayload].vp.SetContent("")
|
||||||
|
}
|
||||||
|
m.revalidate()
|
||||||
|
} else {
|
||||||
|
switch m.focus {
|
||||||
|
case panelHeader, panelPayload:
|
||||||
|
m.rebuildJWT()
|
||||||
|
case panelSecret:
|
||||||
|
m.revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) renderDocs() {
|
||||||
|
width := max(40, m.docsVP.Width())
|
||||||
|
renderer, err := glamour.NewTermRenderer(
|
||||||
|
glamour.WithStyles(ilovetui.GlamourStyleConfig()),
|
||||||
|
glamour.WithWordWrap(width),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
m.docsVP.SetContent(jwtDocsMD)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rendered, err := renderer.Render(jwtDocsMD)
|
||||||
|
if err != nil {
|
||||||
|
m.docsVP.SetContent(jwtDocsMD)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.docsVP.SetContent(rendered)
|
||||||
|
m.docsVP.SetYOffset(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) borderFor(panel int) lipgloss.Style {
|
||||||
|
if panel == m.focus && !m.showDocs {
|
||||||
|
return ilovetui.S.PanelFocused
|
||||||
|
}
|
||||||
|
return ilovetui.S.Panel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) panelTitle(panel int, name string) string {
|
||||||
|
bc := lipgloss.NewStyle().Foreground(m.borderFor(panel).GetBorderTopForeground())
|
||||||
|
title := bc.Render(name)
|
||||||
|
if panel == m.focus && m.panels[panel].editing {
|
||||||
|
title += ilovetui.S.Faint.Render(" [edit]")
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) secretTitle() string {
|
||||||
|
name := m.panelTitle(panelSecret, "Secret")
|
||||||
|
|
||||||
|
var sigStr string
|
||||||
|
if m.sigValid == nil {
|
||||||
|
sigStr = ilovetui.S.Faint.Render("·")
|
||||||
|
} else if *m.sigValid {
|
||||||
|
sigStr = lipgloss.NewStyle().Foreground(ilovetui.S.Success).Render("✓")
|
||||||
|
} else {
|
||||||
|
sigStr = lipgloss.NewStyle().Foreground(ilovetui.S.Error).Render("✗")
|
||||||
|
}
|
||||||
|
|
||||||
|
return name + ilovetui.S.Faint.Render(" · ") + sigStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) renderPanelContent(panel int) string {
|
||||||
|
p := &m.panels[panel]
|
||||||
|
if p.editing {
|
||||||
|
return p.ta.View()
|
||||||
|
}
|
||||||
|
if p.ta.Value() == "" {
|
||||||
|
return ilovetui.S.Faint.Render(panelPlaceholders[panel])
|
||||||
|
}
|
||||||
|
return ilovetui.ViewportView(&p.vp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) renderPanel(panel int, title string, w, h int) string {
|
||||||
|
return style.RenderWithTitle(m.borderFor(panel), title, m.renderPanelContent(panel), w, h)
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"github.com/anotherhadi/jwt-tui/internal/keys"
|
||||||
|
"github.com/anotherhadi/jwt-tui/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
m.keymap.width = msg.Width
|
||||||
|
m.recalcSizes()
|
||||||
|
if m.showDocs {
|
||||||
|
m.renderDocs()
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tea.ClipboardMsg:
|
||||||
|
content := msg.String()
|
||||||
|
if content != "" {
|
||||||
|
panel := m.pendingPastePanel
|
||||||
|
m.panels[panel].ta.SetValue(content)
|
||||||
|
m.setViewportContent(panel, content)
|
||||||
|
switch panel {
|
||||||
|
case panelHeader, panelPayload:
|
||||||
|
m.rebuildJWT()
|
||||||
|
case panelJWT:
|
||||||
|
m.decodeJWT()
|
||||||
|
case panelSecret:
|
||||||
|
m.revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case util.EditorFinishedMsg:
|
||||||
|
if msg.Err == nil && msg.Content != "" {
|
||||||
|
panel := m.pendingEditorPanel
|
||||||
|
m.panels[panel].ta.SetValue(msg.Content)
|
||||||
|
m.setViewportContent(panel, msg.Content)
|
||||||
|
switch panel {
|
||||||
|
case panelHeader, panelPayload:
|
||||||
|
m.rebuildJWT()
|
||||||
|
case panelJWT:
|
||||||
|
m.decodeJWT()
|
||||||
|
case panelSecret:
|
||||||
|
m.revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tea.KeyPressMsg:
|
||||||
|
// Docs overlay: only scrolling, d or esc to close, ctrl+c to quit
|
||||||
|
if m.showDocs {
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, keys.Keys.Quit):
|
||||||
|
return m, tea.Quit
|
||||||
|
case key.Matches(msg, keys.Keys.Docs), msg.String() == "esc":
|
||||||
|
m.showDocs = false
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.docsVP, cmd = m.docsVP.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// In edit mode: esc and ctrl+c exit edit mode, everything else goes to the textarea
|
||||||
|
if m.panels[m.focus].editing {
|
||||||
|
if msg.String() == "esc" || msg.String() == "ctrl+c" {
|
||||||
|
m.exitEditMode()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
p := &m.panels[m.focus]
|
||||||
|
prev := p.ta.Value()
|
||||||
|
var cmd tea.Cmd
|
||||||
|
p.ta, cmd = p.ta.Update(msg)
|
||||||
|
if p.ta.Value() != prev {
|
||||||
|
switch m.focus {
|
||||||
|
case panelHeader, panelPayload:
|
||||||
|
m.rebuildJWT()
|
||||||
|
case panelJWT:
|
||||||
|
m.decodeJWT()
|
||||||
|
case panelSecret:
|
||||||
|
m.revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// View mode shortcuts
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, keys.Keys.Quit):
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.HelpToggle):
|
||||||
|
m.help.ShowAll = !m.help.ShowAll
|
||||||
|
m.recalcSizes()
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.Docs):
|
||||||
|
m.help.ShowAll = false
|
||||||
|
m.showDocs = true
|
||||||
|
m.renderDocs()
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.CycleFocus):
|
||||||
|
m.focus = (m.focus + 1) % 4
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.Edit):
|
||||||
|
return m, m.enterEditMode()
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.EditExternal):
|
||||||
|
m.pendingEditorPanel = m.focus
|
||||||
|
return m, util.OpenExternalEditor(m.panels[m.focus].ta.Value())
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.Copy):
|
||||||
|
return m, tea.SetClipboard(m.panels[m.focus].ta.Value())
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.Paste):
|
||||||
|
m.pendingPastePanel = m.focus
|
||||||
|
return m, tea.ReadClipboard
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.Clear):
|
||||||
|
return m, m.clearPanel()
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.Reset):
|
||||||
|
m.resetPanel()
|
||||||
|
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.panels[m.focus].vp, cmd = m.panels[m.focus].vp.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
if m.panels[m.focus].editing {
|
||||||
|
p := &m.panels[m.focus]
|
||||||
|
var cmd tea.Cmd
|
||||||
|
p.ta, cmd = p.ta.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.panels[m.focus].vp, cmd = m.panels[m.focus].vp.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m Model) View() tea.View {
|
||||||
|
var content string
|
||||||
|
if m.width == 0 {
|
||||||
|
content = ""
|
||||||
|
} else if m.showDocs {
|
||||||
|
content = m.renderDocsView()
|
||||||
|
} else {
|
||||||
|
content = m.renderMainView()
|
||||||
|
}
|
||||||
|
v := tea.NewView(content)
|
||||||
|
v.AltScreen = true
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderMainView() string {
|
||||||
|
leftW := m.width / 2
|
||||||
|
rightW := m.width - leftW
|
||||||
|
helpH := m.helpHeight()
|
||||||
|
availH := m.height - helpH - 1
|
||||||
|
topH := availH / 2
|
||||||
|
bottomH := availH - topH
|
||||||
|
|
||||||
|
// Layout (clockwise from top-left):
|
||||||
|
// top-left=JWT top-right=Header
|
||||||
|
// bot-left=Secret bot-right=Payload
|
||||||
|
jwtPanel := m.renderPanel(panelJWT, m.panelTitle(panelJWT, "Encoded"), leftW, topH)
|
||||||
|
headerPanel := m.renderPanel(panelHeader, m.panelTitle(panelHeader, "Header"), rightW, topH)
|
||||||
|
payloadPanel := m.renderPanel(panelPayload, m.panelTitle(panelPayload, "Payload"), rightW, bottomH)
|
||||||
|
secretPanel := m.renderPanel(panelSecret, m.secretTitle(), leftW, bottomH)
|
||||||
|
|
||||||
|
left := lipgloss.JoinVertical(lipgloss.Left, jwtPanel, secretPanel)
|
||||||
|
right := lipgloss.JoinVertical(lipgloss.Left, headerPanel, payloadPanel)
|
||||||
|
main := lipgloss.JoinHorizontal(lipgloss.Top, left, right)
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, main, m.renderErrorLine(), m.renderHelpBar())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderDocsView() string {
|
||||||
|
docsBorder := lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(ilovetui.S.Subtle).
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
window := docsBorder.Render(ilovetui.ViewportView(&m.docsVP))
|
||||||
|
helpStr := m.help.View(m.docsKeys)
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, window, helpStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderErrorLine() string {
|
||||||
|
if m.errMsg == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return lipgloss.NewStyle().Foreground(ilovetui.S.Error).Render(" " + m.errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderHelpBar() string {
|
||||||
|
helpStr := m.help.View(m.keymap)
|
||||||
|
|
||||||
|
var sigStr string
|
||||||
|
if m.sigValid == nil {
|
||||||
|
sigStr = ilovetui.S.Faint.Render("-")
|
||||||
|
} else if *m.sigValid {
|
||||||
|
sigStr = lipgloss.NewStyle().Foreground(ilovetui.S.Success).Bold(true).Render(m.sigStatus)
|
||||||
|
} else {
|
||||||
|
sigStr = lipgloss.NewStyle().Foreground(ilovetui.S.Error).Bold(true).Render(m.sigStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align sig status to the right of the last line of helpStr
|
||||||
|
helpLines := strings.Split(helpStr, "\n")
|
||||||
|
lastLine := helpLines[len(helpLines)-1]
|
||||||
|
lastLineW := lipgloss.Width(lastLine)
|
||||||
|
sigW := lipgloss.Width(sigStr)
|
||||||
|
pad := m.width - lastLineW - sigW
|
||||||
|
if pad < 1 {
|
||||||
|
pad = 1
|
||||||
|
}
|
||||||
|
helpLines[len(helpLines)-1] = lastLine + strings.Repeat(" ", pad) + sigStr
|
||||||
|
return strings.Join(helpLines, "\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EditorFinishedMsg struct {
|
||||||
|
Content string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenExternalEditor(content string) tea.Cmd {
|
||||||
|
editor := os.Getenv("EDITOR")
|
||||||
|
if editor == "" {
|
||||||
|
editor = os.Getenv("VISUAL")
|
||||||
|
}
|
||||||
|
if editor == "" {
|
||||||
|
editor = "vi"
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.CreateTemp("", "jwt-tui-*.json")
|
||||||
|
if err != nil {
|
||||||
|
return func() tea.Msg { return EditorFinishedMsg{Err: err} }
|
||||||
|
}
|
||||||
|
tmpPath := f.Name()
|
||||||
|
if _, werr := f.WriteString(content); werr != nil {
|
||||||
|
f.Close()
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
return func() tea.Msg { return EditorFinishedMsg{Err: werr} }
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
return tea.ExecProcess(exec.Command(editor, tmpPath), func(err error) tea.Msg {
|
||||||
|
defer os.Remove(tmpPath)
|
||||||
|
if err != nil {
|
||||||
|
return EditorFinishedMsg{Err: err}
|
||||||
|
}
|
||||||
|
data, readErr := os.ReadFile(tmpPath)
|
||||||
|
if readErr != nil {
|
||||||
|
return EditorFinishedMsg{Err: readErr}
|
||||||
|
}
|
||||||
|
return EditorFinishedMsg{Content: string(data)}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
schema = 3
|
||||||
|
|
||||||
|
[mod]
|
||||||
|
[mod.'charm.land/bubbles/v2']
|
||||||
|
version = 'v2.1.0'
|
||||||
|
hash = 'sha256-2OmqpBrl+taOJzAhVM6OReLmoYRxZOXx9JqFNjQjsPA='
|
||||||
|
|
||||||
|
[mod.'charm.land/bubbletea/v2']
|
||||||
|
version = 'v2.0.6'
|
||||||
|
hash = 'sha256-1jxXmcnI4peUE0Xs3HGe57pIhRONx235aAaeqm2r434='
|
||||||
|
|
||||||
|
[mod.'charm.land/glamour/v2']
|
||||||
|
version = 'v2.0.0'
|
||||||
|
hash = 'sha256-CZYlNGw2MihqnSHf1Xxqz55NnqW9fVpLxyvLItryIw4='
|
||||||
|
|
||||||
|
[mod.'charm.land/lipgloss/v2']
|
||||||
|
version = 'v2.0.3'
|
||||||
|
hash = 'sha256-/RFkSUscU3NwymzT+PfizGf3XyQIdVGQlX7vxktCUGk='
|
||||||
|
|
||||||
|
[mod.'github.com/alecthomas/chroma/v2']
|
||||||
|
version = 'v2.14.0'
|
||||||
|
hash = 'sha256-d+zcIobMS5Y0/Ym9Uxubf20uyw0aBCr0f1oEOAGHlEA='
|
||||||
|
|
||||||
|
[mod.'github.com/anotherhadi/ilovetui']
|
||||||
|
version = 'v0.1.6'
|
||||||
|
hash = 'sha256-7E+7UFks5vM3XWCvX2joFmHGcW7qqnoox6ZPFglaLO4='
|
||||||
|
|
||||||
|
[mod.'github.com/atotto/clipboard']
|
||||||
|
version = 'v0.1.4'
|
||||||
|
hash = 'sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk='
|
||||||
|
|
||||||
|
[mod.'github.com/aymerick/douceur']
|
||||||
|
version = 'v0.2.0'
|
||||||
|
hash = 'sha256-NiBX8EfOvLXNiK3pJaZX4N73YgfzdrzRXdiBFe3X3sE='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/colorprofile']
|
||||||
|
version = 'v0.4.3'
|
||||||
|
hash = 'sha256-y+QDUxGOKhugEMQLRUTZYT2C+wKqYHnMLJ44jbh7+JA='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/ultraviolet']
|
||||||
|
version = 'v0.0.0-20260416155717-489999b90468'
|
||||||
|
hash = 'sha256-HAex/0iEd42Wk1t+AR0O8J+F2ZAYU2sTw9ea0EfmKEU='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/x/ansi']
|
||||||
|
version = 'v0.11.7'
|
||||||
|
hash = 'sha256-q8BZJq4K7NE5ETocN9/G/EoV0dUyD703ONSfHiUYzWQ='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/x/exp/slice']
|
||||||
|
version = 'v0.0.0-20250327172914-2fdc97757edf'
|
||||||
|
hash = 'sha256-C1tksnevc/RdytJRQg5LQ0+VVSWlTwbNGic649m6E1Q='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/x/term']
|
||||||
|
version = 'v0.2.2'
|
||||||
|
hash = 'sha256-KF7IU1Luxl/sZP6XjomWB2e3lxSUS4/5AahhapGir/4='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/x/termios']
|
||||||
|
version = 'v0.1.1'
|
||||||
|
hash = 'sha256-sri3LpHCBhGvnJldDzBxwbbZpeSGZVCJFOUL45uBFds='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/x/windows']
|
||||||
|
version = 'v0.2.2'
|
||||||
|
hash = 'sha256-CvmE8kAC5wlPSeWjl2hc5xizvGS2FeOLHw84froldkk='
|
||||||
|
|
||||||
|
[mod.'github.com/clipperhouse/displaywidth']
|
||||||
|
version = 'v0.11.0'
|
||||||
|
hash = 'sha256-WokyTaofEy95xlshqK5YDzpemhXV5oaQifxS9YyfCXo='
|
||||||
|
|
||||||
|
[mod.'github.com/clipperhouse/uax29/v2']
|
||||||
|
version = 'v2.7.0'
|
||||||
|
hash = 'sha256-GO3az7WiGcwU0OvmocwdfR5ohGRL8NbjscIaMyhAdxE='
|
||||||
|
|
||||||
|
[mod.'github.com/dlclark/regexp2']
|
||||||
|
version = 'v1.11.0'
|
||||||
|
hash = 'sha256-iXBBgykYu9Dcd+7LMJyRYc3Ry47jmuLGZFW13zU6toU='
|
||||||
|
|
||||||
|
[mod.'github.com/fsnotify/fsnotify']
|
||||||
|
version = 'v1.9.0'
|
||||||
|
hash = 'sha256-WtpE1N6dpHwEvIub7Xp/CrWm0fd6PX7MKA4PV44rp2g='
|
||||||
|
|
||||||
|
[mod.'github.com/go-viper/mapstructure/v2']
|
||||||
|
version = 'v2.4.0'
|
||||||
|
hash = 'sha256-lLfcV9z4n94hDhgyXJlde4bFB0hfzlbh+polqcJCwGE='
|
||||||
|
|
||||||
|
[mod.'github.com/gorilla/css']
|
||||||
|
version = 'v1.0.1'
|
||||||
|
hash = 'sha256-6JwNHqlY2NpZ0pSQTyYPSpiNqjXOdFHqrUT10sv3y8A='
|
||||||
|
|
||||||
|
[mod.'github.com/lucasb-eyer/go-colorful']
|
||||||
|
version = 'v1.4.0'
|
||||||
|
hash = 'sha256-i/3GDHKEMLCy0kc3mtyk58UWYOPmKoUVaq6QCAWXKP0='
|
||||||
|
|
||||||
|
[mod.'github.com/mattn/go-runewidth']
|
||||||
|
version = 'v0.0.23'
|
||||||
|
hash = 'sha256-SmChZ2U1aR8pW3LPhdM7KcVF5TO6VcHgRzBtUXbBWJA='
|
||||||
|
|
||||||
|
[mod.'github.com/microcosm-cc/bluemonday']
|
||||||
|
version = 'v1.0.27'
|
||||||
|
hash = 'sha256-EZSya9FLPQ83CL7N2cZy21fdS35hViTkiMK5f3op8Es='
|
||||||
|
|
||||||
|
[mod.'github.com/muesli/cancelreader']
|
||||||
|
version = 'v0.2.2'
|
||||||
|
hash = 'sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ='
|
||||||
|
|
||||||
|
[mod.'github.com/pelletier/go-toml/v2']
|
||||||
|
version = 'v2.2.4'
|
||||||
|
hash = 'sha256-8qQIPldbsS5RO8v/FW/se3ZsAyvLzexiivzJCbGRg2Q='
|
||||||
|
|
||||||
|
[mod.'github.com/rivo/uniseg']
|
||||||
|
version = 'v0.4.7'
|
||||||
|
hash = 'sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo='
|
||||||
|
|
||||||
|
[mod.'github.com/sagikazarmark/locafero']
|
||||||
|
version = 'v0.11.0'
|
||||||
|
hash = 'sha256-PUX8dzJtkD8YDZFNqpHnl4qgb0tE1W/DLnL7V+/d1z4='
|
||||||
|
|
||||||
|
[mod.'github.com/sourcegraph/conc']
|
||||||
|
version = 'v0.3.1-0.20240121214520-5f936abd7ae8'
|
||||||
|
hash = 'sha256-AUNFlY6K7s1aoW/vb4pjK84ROdnVZY1i6cOmdeG+wN8='
|
||||||
|
|
||||||
|
[mod.'github.com/spf13/afero']
|
||||||
|
version = 'v1.15.0'
|
||||||
|
hash = 'sha256-LhcezbOqfuBzacytbqck0hNUxi6NbWNhifUc5/9uHQ8='
|
||||||
|
|
||||||
|
[mod.'github.com/spf13/cast']
|
||||||
|
version = 'v1.10.0'
|
||||||
|
hash = 'sha256-dQ6Qqf26IZsa6XsGKP7GDuCj+WmSsBmkBwGTDfue/rk='
|
||||||
|
|
||||||
|
[mod.'github.com/spf13/pflag']
|
||||||
|
version = 'v1.0.10'
|
||||||
|
hash = 'sha256-uDPnWjHpSrzXr17KEYEA1yAbizfcsfo5AyztY2tS6ZU='
|
||||||
|
|
||||||
|
[mod.'github.com/spf13/viper']
|
||||||
|
version = 'v1.21.0'
|
||||||
|
hash = 'sha256-A9A8i7HH/ge4j3hw7G++HNj8BjhhpZKvxHhfY+QAxkI='
|
||||||
|
|
||||||
|
[mod.'github.com/subosito/gotenv']
|
||||||
|
version = 'v1.6.0'
|
||||||
|
hash = 'sha256-LspbjTniiq2xAICSXmgqP7carwlNaLqnCTQfw2pa80A='
|
||||||
|
|
||||||
|
[mod.'github.com/xo/terminfo']
|
||||||
|
version = 'v0.0.0-20220910002029-abceb7e1c41e'
|
||||||
|
hash = 'sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU='
|
||||||
|
|
||||||
|
[mod.'github.com/yuin/goldmark']
|
||||||
|
version = 'v1.7.8'
|
||||||
|
hash = 'sha256-SNJMPPiXkRDLVOldrHN0ErC3bUB2VoWaLDkd9zmMATw='
|
||||||
|
|
||||||
|
[mod.'github.com/yuin/goldmark-emoji']
|
||||||
|
version = 'v1.0.5'
|
||||||
|
hash = 'sha256-GtMipzIcZ0Be7y8fhZ9VkT9dg6bqj9U+DhvliGcMkaU='
|
||||||
|
|
||||||
|
[mod.'go.yaml.in/yaml/v3']
|
||||||
|
version = 'v3.0.4'
|
||||||
|
hash = 'sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4='
|
||||||
|
|
||||||
|
[mod.'golang.org/x/net']
|
||||||
|
version = 'v0.39.0'
|
||||||
|
hash = 'sha256-IP29+yGphWKUT7wHTyzqA2rnRT4AJ7oWcT6NKLzkWcM='
|
||||||
|
|
||||||
|
[mod.'golang.org/x/sync']
|
||||||
|
version = 'v0.20.0'
|
||||||
|
hash = 'sha256-ybcjhCfK6lroUM0yswUvWooW8MOQZBXyiSqoxG6Uy0Y='
|
||||||
|
|
||||||
|
[mod.'golang.org/x/sys']
|
||||||
|
version = 'v0.43.0'
|
||||||
|
hash = 'sha256-aDQXqSTZES2l/132PBxhZN4ywldpPyfm7LByYCHzzwM='
|
||||||
|
|
||||||
|
[mod.'golang.org/x/text']
|
||||||
|
version = 'v0.28.0'
|
||||||
|
hash = 'sha256-8UlJniGK+km4Hmrw6XMxELnExgrih7+z8tU26Cntmto='
|
||||||
|
|
||||||
|
[mod.'gopkg.in/yaml.v3']
|
||||||
|
version = 'v3.0.1'
|
||||||
|
hash = 'sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU='
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
buildGoApplication,
|
||||||
|
}: let
|
||||||
|
pname = "jwt-tui";
|
||||||
|
version = "0.1.0";
|
||||||
|
ldflags = ["-s" "-w" "-X main.version=${version}"];
|
||||||
|
pkg = buildGoApplication {
|
||||||
|
inherit pname version ldflags;
|
||||||
|
src = ../.;
|
||||||
|
modules = ./gomod2nix.toml;
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "A TUI for inspecting, editing, and signing JSON Web Tokens (JWTs).";
|
||||||
|
homepage = "https://github.com/anotherhadi/jwt-tui";
|
||||||
|
platforms = platforms.unix;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
"${pname}" = pkg;
|
||||||
|
default = pkg;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
gitHooksLib,
|
||||||
|
gomod2nixPkgs,
|
||||||
|
}: let
|
||||||
|
hooks = gitHooksLib.run {
|
||||||
|
src = ../.;
|
||||||
|
hooks = {
|
||||||
|
gofmt.enable = true;
|
||||||
|
govet.enable = true;
|
||||||
|
gomod2nix = {
|
||||||
|
enable = true;
|
||||||
|
name = "gomod2nix";
|
||||||
|
entry = "gomod2nix --outdir ./nix";
|
||||||
|
language = "system";
|
||||||
|
files = "go\\.(mod|sum)$";
|
||||||
|
pass_filenames = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
inject-exec = {
|
||||||
|
enable = true;
|
||||||
|
name = "inject-exec";
|
||||||
|
entry = "python3 .github/scripts/inject-exec.py README.md";
|
||||||
|
language = "system";
|
||||||
|
files = "(README\\.md|cmd/)";
|
||||||
|
pass_filenames = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
pkgs.mkShell {
|
||||||
|
packages = with pkgs;
|
||||||
|
[
|
||||||
|
go
|
||||||
|
gomod2nixPkgs.gomod2nix
|
||||||
|
]
|
||||||
|
++ hooks.enabledPackages;
|
||||||
|
|
||||||
|
shellHook = hooks.shellHook;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user