mirror of
https://github.com/agresdominik/cheat_sheet.git
synced 2026-07-21 16:10:54 +00:00
Compare commits
21 Commits
fix-flow
...
648412d550
| Author | SHA1 | Date | |
|---|---|---|---|
| 648412d550 | |||
| f0056b9722 | |||
| 11541d532f | |||
| 39361150a5 | |||
| 4b21757e16 | |||
| 45f66a07ad | |||
| 74304b926d | |||
| 3c5ae1ef2f | |||
| 6f92c3b1e7 | |||
| b3e6a430cb | |||
| 2ad2fa52db | |||
| 527ef5e15a | |||
| aa17c8abd7 | |||
| 2fc4ea34f7 | |||
| c268ce51d0 | |||
| 6508479b98 | |||
| c6f677b169 | |||
| 5bb45ca959 | |||
| 3bd7eff5e9 | |||
| 54e1d5bbd4 | |||
| 1cfc374d70 |
@@ -0,0 +1,119 @@
|
||||
# .github/workflows/ci.yml
|
||||
#
|
||||
# Runs on every push to main and on every pull request targeting main.
|
||||
# Pipeline: golangci-lint → go test → make build
|
||||
# If all three pass on main, the release workflow is triggered automatically.
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**'] # CI on every branch
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Validate command JSON files
|
||||
run: |
|
||||
jq empty data/commands.json
|
||||
jq empty data/commands_template.json
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=5m ./src/...
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v -race -coverprofile=coverage.out ./src/...
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
path: coverage.out
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Build binary
|
||||
run: make build
|
||||
|
||||
- name: Smoke test binary
|
||||
run: ./bin/cheatsh --help
|
||||
|
||||
- name: Upload binary artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cheatsh-${{ runner.os }}
|
||||
path: bin/cheatsh
|
||||
|
||||
# Only fires on pushes to main (not PRs) after build passes.
|
||||
# Checks whether the VERSION file was changed — if so, triggers a release.
|
||||
trigger-release:
|
||||
name: Trigger Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
outputs:
|
||||
version_changed: ${{ steps.check.outputs.changed }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2 # need the previous commit to diff
|
||||
|
||||
- name: Check if VERSION changed
|
||||
id: check
|
||||
run: |
|
||||
if git diff HEAD~1 HEAD --name-only | grep -q '^VERSION$'; then
|
||||
VERSION=$(cat VERSION)
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "VERSION changed to $VERSION — release will be created."
|
||||
else
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
echo "VERSION unchanged — skipping release."
|
||||
fi
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: [trigger-release]
|
||||
if: needs.trigger-release.outputs.version_changed == 'true'
|
||||
uses: ./.github/workflows/release.yml
|
||||
with:
|
||||
version: ${{ needs.trigger-release.outputs.version }}
|
||||
secrets: inherit
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -0,0 +1,107 @@
|
||||
# .github/workflows/release.yml
|
||||
#
|
||||
# Called by ci.yml when VERSION changes on main.
|
||||
# Can also be triggered manually via workflow_dispatch for hotfixes.
|
||||
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (without v prefix, e.g. 1.2.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Tag, Build & Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate version matches VERSION file
|
||||
run: |
|
||||
FILE_VERSION=$(cat VERSION)
|
||||
INPUT_VERSION="${{ inputs.version }}"
|
||||
if [ "$FILE_VERSION" != "$INPUT_VERSION" ]; then
|
||||
echo "ERROR: VERSION file ($FILE_VERSION) does not match input ($INPUT_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check tag availability
|
||||
run: |
|
||||
TAG="v${{ inputs.version }}"
|
||||
if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then
|
||||
echo "ERROR: Tag $TAG already exists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Build
|
||||
run: make build
|
||||
|
||||
- name: Create git tag
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "v${{ inputs.version }}" -m "Release v${{ inputs.version }}"
|
||||
git push origin "v${{ inputs.version }}"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ inputs.version }}
|
||||
name: v${{ inputs.version }}
|
||||
generate_release_notes: true
|
||||
files: bin/cheatsh
|
||||
|
||||
- name: Compute tarball sha256
|
||||
id: sha
|
||||
run: |
|
||||
URL="https://github.com/${{ github.repository }}/archive/refs/tags/v${{ inputs.version }}.tar.gz"
|
||||
echo "Waiting briefly for GitHub to process the release tarball..."
|
||||
sleep 10
|
||||
SHA=$(curl -sL "$URL" | sha256sum | cut -d' ' -f1)
|
||||
echo "sha=$SHA" >> $GITHUB_OUTPUT
|
||||
echo "url=$URL" >> $GITHUB_OUTPUT
|
||||
echo "Tarball URL : $URL"
|
||||
echo "SHA256 : $SHA"
|
||||
|
||||
- name: Checkout homebrew tap
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: agresdominik/repo
|
||||
token: ${{ secrets.TAP_GITHUB_TOKEN }}
|
||||
path: tap
|
||||
|
||||
- name: Patch cheatsh formula
|
||||
run: |
|
||||
# version is inferred by Homebrew from the URL tag, so only url + sha256 are patched.
|
||||
FORMULA="tap/Formula/cheatsh.rb"
|
||||
sed -i "s|url .*|url \"${{ steps.sha.outputs.url }}\"|" "$FORMULA"
|
||||
sed -i "s|sha256 .*|sha256 \"${{ steps.sha.outputs.sha }}\"|" "$FORMULA"
|
||||
|
||||
- name: Commit and push tap update
|
||||
run: |
|
||||
cd tap
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git diff --quiet && echo "Formula already up to date." && exit 0
|
||||
git commit -am "chore: bump cheatsh to v${{ inputs.version }}"
|
||||
git push
|
||||
@@ -0,0 +1,8 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-json
|
||||
name: Check if JSON is valid
|
||||
files: ^data/.*\.json$
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
EXECUTABLE = cheatsh
|
||||
PACKAGES = ./src/.
|
||||
BINDIR = bin
|
||||
SYSCONFDIR = $(HOME)/.config/cheatsh
|
||||
PREFIX ?= $(HOME)/.local
|
||||
DATADIR = $(PREFIX)/share/cheatsh
|
||||
|
||||
all: build
|
||||
|
||||
@@ -12,16 +12,16 @@ build:
|
||||
|
||||
install: build
|
||||
|
||||
@echo "Installing binary in $(PREFIX)"
|
||||
@echo "Installing binary in $(PREFIX)/bin"
|
||||
install -Dm755 bin/$(EXECUTABLE) $(PREFIX)/bin/$(EXECUTABLE)
|
||||
|
||||
@echo "Copying config files to $(SYSCONFDIR)"
|
||||
install -Dm644 data/commands.json $(SYSCONFDIR)/commands.json
|
||||
install -Dm644 data/commands_template.json $(SYSCONFDIR)/commands_template.json
|
||||
@echo "Installing command lists in $(DATADIR)"
|
||||
install -Dm644 data/commands.json $(DATADIR)/commands.json
|
||||
install -Dm644 data/commands_template.json $(DATADIR)/commands_template.json
|
||||
|
||||
uninstall:
|
||||
rm -f $(PREFIX)/bin/$(EXECUTABLE)
|
||||
rm -rf $(SYSCONFDIR)
|
||||
rm -rf $(DATADIR)
|
||||
|
||||
clean:
|
||||
rm -rf bin/
|
||||
|
||||
@@ -1,2 +1,89 @@
|
||||
# cheat_sheet
|
||||
A man-style cli cheatsheet tool for commands I use and forget a lot
|
||||
# Cheat Sheet
|
||||
|
||||
A small TUI tool that displays a pre-configured set of commands in a searchable
|
||||
menu. Pick one and it's copied to your system clipboard — paste it where you
|
||||
need it and edit before running.
|
||||
|
||||
I wrote this to look up commands I use often enough to need repeatedly, but not
|
||||
often enough to remember exactly. Written in Go with [bubbletea](https://github.com/charmbracelet/bubbletea).
|
||||
|
||||
## Install
|
||||
|
||||
### Homebrew (recommended)
|
||||
|
||||
```bash
|
||||
brew install agresdominik/repo/cheatsh
|
||||
```
|
||||
|
||||
### From source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/agresdominik/cheat_sheet
|
||||
cd cheat_sheet
|
||||
make install # builds and installs to ~/.local/bin
|
||||
```
|
||||
|
||||
Make sure `~/.local/bin` is in your `PATH` (or change `PREFIX` in the `Makefile`).
|
||||
|
||||
## Configuration
|
||||
|
||||
cheatsh ships with a maintained command list and uses it by default. It lives
|
||||
next to the binary and is **replaced on `brew upgrade`**, so updating the list
|
||||
in this repo and releasing is all it takes to get the new commands on your
|
||||
machines:
|
||||
|
||||
```
|
||||
<prefix>/share/cheatsh/commands.json # Homebrew, e.g. /opt/homebrew/...
|
||||
~/.local/share/cheatsh/commands.json # make install
|
||||
data/commands.json # running from a source checkout
|
||||
```
|
||||
|
||||
To maintain the default list, edit `data/commands.json` and release. The format
|
||||
is a map of categories to command lists:
|
||||
|
||||
```json
|
||||
{
|
||||
"git": [
|
||||
{ "command": "git reset --soft HEAD~1", "desc": "Undo last commit, keep changes" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Custom list
|
||||
|
||||
If you'd rather keep your own list (one that survives upgrades untouched), copy
|
||||
the template and pass it explicitly — `--config` always wins over the bundled
|
||||
default:
|
||||
|
||||
```bash
|
||||
cp "$(brew --prefix)/share/cheatsh/commands_template.json" ~/my-commands.json
|
||||
cheatsh --config ~/my-commands.json
|
||||
```
|
||||
|
||||
## Clipboard dependency
|
||||
|
||||
Copying to the clipboard needs a backend:
|
||||
|
||||
- **macOS** — works out of the box (`pbcopy`).
|
||||
- **Linux/Wayland** — `wl-clipboard`.
|
||||
- **Linux/X11** — `xsel` or `xclip`.
|
||||
|
||||
If no backend is found, cheatsh prints the command to stdout instead.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cheatsh # browse and select; selection is copied to clipboard
|
||||
cheatsh --config FILE # use a specific config file
|
||||
cheatsh --help
|
||||
```
|
||||
|
||||
In the TUI: `/` to filter, `enter` to descend / select, `b` to go back, `q` to quit.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
make build # build to bin/cheatsh
|
||||
make local # build and run against data/commands.json
|
||||
go test ./src/...
|
||||
```
|
||||
|
||||
+89
-1
@@ -39,6 +39,10 @@
|
||||
{
|
||||
"command": "zip -r folder_name.zip folder_name",
|
||||
"desc": "Zip a folder and all of its contents into a archive"
|
||||
},
|
||||
{
|
||||
"command": "ln -s /path/to/file file",
|
||||
"desc": "Create a symbolic link pointing to a file on the system"
|
||||
}
|
||||
],
|
||||
"python": [
|
||||
@@ -119,13 +123,25 @@
|
||||
"desc": "Rebase commits from source branch to target branch"
|
||||
},
|
||||
{
|
||||
"command": "git stash",
|
||||
"command": "git merge <target_branch>",
|
||||
"desc": "Merge commits from another branch into the one command is run on"
|
||||
},
|
||||
{
|
||||
"command": "git stash push -m 'Message/Stash Name'",
|
||||
"desc": "Stash changes without commiting"
|
||||
},
|
||||
{
|
||||
"command": "git stash pop",
|
||||
"desc": "Stash changes without commiting (add stash@{n} for specific stash from 'git stash list'"
|
||||
},
|
||||
{
|
||||
"command": "git reflog",
|
||||
"desc": "Show history of all changes"
|
||||
},
|
||||
{
|
||||
"command": "git status",
|
||||
"desc": "See what is changed right now"
|
||||
},
|
||||
{
|
||||
"command": "git remote set-url origin git@github.com:username/repo.git",
|
||||
"desc": "Change origin url (example from https to ssh)"
|
||||
@@ -190,5 +206,77 @@
|
||||
"command": "arch-audit",
|
||||
"desc": "Audit installed packages for known vulnerabilities (pacman)"
|
||||
}
|
||||
],
|
||||
"package managers": [
|
||||
{
|
||||
"command": "pacman -Syu / yay -Syu / dnf update / apt update && apt upgrade",
|
||||
"desc": "Linux base package managers"
|
||||
},
|
||||
{
|
||||
"command": "flatpak update",
|
||||
"desc": "Flathub extra sandboxed applications"
|
||||
},
|
||||
{
|
||||
"command": "npm update -g",
|
||||
"desc": "Npm installed packages on system"
|
||||
},
|
||||
{
|
||||
"command": "pipx upgrade-all",
|
||||
"desc": "Pip installed packages on system"
|
||||
},
|
||||
{
|
||||
"command": "brew update",
|
||||
"desc": "Brew installed own software"
|
||||
}
|
||||
],
|
||||
"other installs": [
|
||||
{
|
||||
"command": "glow",
|
||||
"desc": "Terminal Markdown Renderer"
|
||||
},
|
||||
{
|
||||
"command": "markserv .",
|
||||
"desc": "Serve a webserver with rendered Markdown"
|
||||
},
|
||||
{
|
||||
"command": "pinta",
|
||||
"desc": "Image editor"
|
||||
},
|
||||
{
|
||||
"command": "bottles",
|
||||
"desc": "Wine wrapper"
|
||||
},
|
||||
{
|
||||
"command": "zathura",
|
||||
"desc": "PDF File viewer"
|
||||
},
|
||||
{
|
||||
"command": "sqlitebrowser",
|
||||
"desc": "SQL File browser"
|
||||
},
|
||||
{
|
||||
"command": "lact",
|
||||
"desc": "GPU Overclock Utility"
|
||||
},
|
||||
{
|
||||
"command": "baobab",
|
||||
"desc": "Directory usage analysis"
|
||||
},
|
||||
{
|
||||
"command": "gsmartcontrol",
|
||||
"desc": "Disk S.M.A.R.T analysis"
|
||||
},
|
||||
{
|
||||
"command": "pavucontrol",
|
||||
"desc": "Audio Control"
|
||||
},
|
||||
{
|
||||
"command": "pdfunite file.pdf file.pdf united.pdf",
|
||||
"desc": "Unite multiple pdf files into one"
|
||||
},
|
||||
{
|
||||
"command": "qpdf --password='' --decrypt encrypted_file.pdf decrypted_file.pdf",
|
||||
"desc": "Decrypt (or encrypt with --encrypt) a pdf file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ module github.com/dominikagres/cheat_sheet
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
|
||||
+40
-42
@@ -40,56 +40,54 @@ func main() {
|
||||
|
||||
flag.Parse()
|
||||
|
||||
var configFile string
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
userConf := filepath.Join(home, ".config", "cheatsh")
|
||||
if info, err := os.Stat(userConf); err == nil && info.IsDir() {
|
||||
configFile = filepath.Join(userConf, "commands.json")
|
||||
} else {
|
||||
configFile = "/etc/cheatsh/commands.json"
|
||||
}
|
||||
}
|
||||
|
||||
var commands CmdList
|
||||
|
||||
if *configFlag != "" {
|
||||
commands, err = loadCommands(*configFlag)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot load commands file: %v", err)
|
||||
}
|
||||
StartTui(commands)
|
||||
return
|
||||
} else if len(os.Args) == 1 {
|
||||
commands, err = loadCommands(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot load commands file: %v", err)
|
||||
}
|
||||
StartTui(commands)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
|
||||
case *helpFlag:
|
||||
printHelp()
|
||||
|
||||
case *newFlag:
|
||||
HandleInput()
|
||||
|
||||
default:
|
||||
printHelp()
|
||||
os.Exit(1)
|
||||
|
||||
case *helpFlag:
|
||||
printHelp()
|
||||
return
|
||||
case *newFlag:
|
||||
HandleInput()
|
||||
return
|
||||
}
|
||||
|
||||
configFile := *configFlag
|
||||
if configFile == "" {
|
||||
configFile = defaultConfigPath()
|
||||
}
|
||||
|
||||
commands, err := loadCommands(configFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Fatalf("No config found at %s\nPass a config explicitly with --config <file>.", configFile)
|
||||
}
|
||||
log.Fatalf("Cannot load commands file: %v", err)
|
||||
}
|
||||
|
||||
StartTui(commands)
|
||||
}
|
||||
|
||||
// defaultConfigPath returns the bundled command list installed next to the
|
||||
// binary (<prefix>/share/cheatsh/commands.json, replaced on upgrade). When
|
||||
// running from a source checkout that file doesn't exist, so it falls back to
|
||||
// data/commands.json relative to the working directory.
|
||||
func defaultConfigPath() string {
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
if real, err := filepath.EvalSymlinks(exe); err == nil {
|
||||
exe = real
|
||||
}
|
||||
p := filepath.Join(filepath.Dir(exe), "..", "share", "cheatsh", "commands.json")
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return filepath.Join("data", "commands.json")
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
fmt.Println(`Usage: cheatsh [options]
|
||||
Select a command to copy it to the system clipboard.
|
||||
Without --config the bundled command list is used; pass --config to use your own.
|
||||
Options:
|
||||
--config <file> Specify a config file
|
||||
--config <file> Use a custom config file
|
||||
--help Show this help message
|
||||
--new Add a new command to the config file`)
|
||||
}
|
||||
|
||||
+7
-1
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@@ -128,7 +129,12 @@ func StartTui(commands CmdList) {
|
||||
}
|
||||
|
||||
if m, ok := finalModel.(model); ok && m.selectedCmd != "" {
|
||||
fmt.Println(m.selectedCmd)
|
||||
if err := clipboard.WriteAll(m.selectedCmd); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Could not copy to clipboard:", err)
|
||||
fmt.Println(m.selectedCmd)
|
||||
return
|
||||
}
|
||||
fmt.Println("Copied to clipboard:", m.selectedCmd)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user